diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index 3953e878d..10c7b25d8 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -27,7 +27,7 @@ jobs: needs: run-tests strategy: matrix: - board: ['pi1', 'pi2', 'pi3', 'pi4'] + board: ['pi1', 'pi2', 'pi3', 'pi4', 'x86'] runs-on: ubuntu-latest steps: @@ -61,6 +61,7 @@ jobs: run: | export BUILD_TARGET=${{ matrix.board }} export PUSH=1 + export SKIP_TEST=1 ./bin/build_containers.sh balena: diff --git a/README.md b/README.md index 9b4f0462a..5fadc57db 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ We've tested Anthias and is known to work on the following Raspberry Pi models: * Raspberry Pi 3 Model B+ - 32-bit and 64-bit Bullseye, 64-bit Bookworm * Raspberry Pi 3 Model B - 64-bit Bookworm and Bullseye * Raspberry Pi 2 Model B - 32-bit Bookworm and Bullseye +* x86 Devices - 64-bit Bullseye + * These devices can be something similar to a NUC. We're still fixing the installer so that it'll work with Raspberry Pi Zero and Raspberry Pi 1. @@ -69,6 +71,13 @@ The image file looks something like `--
-raspberry.zip`. T If you'd like more control over your digital signage instance, try installing it on Raspberry Pi OS Lite. +Before you start, make sure that you have `curl` installed. If not, you can install it by running: + +```bash +$ sudo apt update +$ sudo apt install -y curl +``` + The tl;dr for on [Raspberry Pi OS](https://www.raspberrypi.com/software/) is: ``` diff --git a/ansible/roles/network/tasks/main.yml b/ansible/roles/network/tasks/main.yml index de6c8d85e..4b50c71d7 100644 --- a/ansible/roles/network/tasks/main.yml +++ b/ansible/roles/network/tasks/main.yml @@ -83,7 +83,10 @@ register: nm_pkla_path - name: Copy org.freedesktop.NetworkManager.pkla to 50-local.d - ansible.builtin.command: cp -f /var/lib/polkit-1/localauthority/10-vendor.d/org.freedesktop.NetworkManager.pkla /etc/polkit-1/localauthority/50-local.d + ansible.builtin.shell: | + mkdir -p /etc/polkit-1/localauthority/50-local.d + cp -f /var/lib/polkit-1/localauthority/10-vendor.d/org.freedesktop.NetworkManager.pkla \ + /etc/polkit-1/localauthority/50-local.d when: manage_network|bool changed_when: not nm_pkla_path.stat.exists diff --git a/ansible/roles/system/tasks/main.yml b/ansible/roles/system/tasks/main.yml index 15db9df91..7434f162b 100644 --- a/ansible/roles/system/tasks/main.yml +++ b/ansible/roles/system/tasks/main.yml @@ -99,6 +99,9 @@ - name: Notice for cmdline.txt.orig file ansible.builtin.debug: msg: "Use cmdline.txt.orig for boot parameters (don't remove this file)" + tags: + - touches-boot-partition + - raspberry-pi - name: Copy cmdline.txt.orig to cmdline.txt ansible.builtin.copy: @@ -206,15 +209,25 @@ update_cache: true when: not cdefs_exist -- name: Install Anthias dependencies +- name: Install Anthias dependencies (all platforms) ansible.builtin.apt: name: - - rpi-update - bc - python3 - python3-redis state: present +- name: Install Anthias dependencies (Raspberry Pi) + ansible.builtin.apt: + name: + - rpi-update + state: present + when: | + ansible_architecture == "aarch64" or + ansible_architecture == "armv7l" or + ansible_architecture == "armv6l" + + - name: Remove deprecated apt dependencies ansible.builtin.apt: name: @@ -244,21 +257,35 @@ - docker-compose state: absent -- name: Add docker apt key +- name: Add Docker apt key (x86) + ansible.builtin.apt_key: + url: https://download.docker.com/linux/debian/gpg + state: present + when: ansible_architecture == "x86_64" + +- name: Add Docker apt key (Raspberry Pi) ansible.builtin.apt_key: url: https://download.docker.com/linux/raspbian/gpg state: present + when: | + ansible_architecture == "aarch64" or + ansible_architecture == "armv7l" or + ansible_architecture == "armv6l" -- name: Get raspbian name +- name: Get Debian name ansible.builtin.command: lsb_release -cs - register: raspbian_name + register: debian_name changed_when: false +- name: Set architecture + ansible.builtin.set_fact: + architecture: "{{ 'amd64' if ansible_architecture == 'x86_64' else 'armhf' }}" + - name: Add Docker repo ansible.builtin.lineinfile: path: /etc/apt/sources.list.d/docker.list create: true - line: "deb [arch=armhf] https://download.docker.com/linux/debian {{ raspbian_name.stdout }} stable" + line: "deb [arch={{ architecture }}] https://download.docker.com/linux/debian {{ debian_name.stdout }} stable" state: present owner: root group: root @@ -267,13 +294,28 @@ - name: Install Docker ansible.builtin.apt: name: - - docker-ce:armhf - - docker-ce-cli:armhf - - docker-compose-plugin:armhf + - docker-ce:{{ architecture }} + - docker-ce-cli:{{ architecture }} + - docker-compose-plugin:{{ architecture }} update_cache: true install_recommends: false -- name: Add user to docker group +- name: Add user to Docker group (all platforms) + ansible.builtin.user: + name: "{{ lookup('env', 'USER') }}" + group: "{{ lookup('env', 'USER') }}" + groups: + - docker + - adm + - sudo + - video + - plugdev + - users + - input + - netdev + - dialout + +- name: Add user to Docker group (Raspberry Pi) ansible.builtin.user: name: "{{ lookup('env', 'USER') }}" group: "{{ lookup('env', 'USER') }}" @@ -288,6 +330,10 @@ - netdev - gpio - dialout + when: | + ansible_architecture == "aarch64" or + ansible_architecture == "armv7l" or + ansible_architecture == "armv6l" - name: Perform system upgrade ansible.builtin.apt: diff --git a/bin/build_containers.sh b/bin/build_containers.sh index 5460bdf21..29092dfa3 100755 --- a/bin/build_containers.sh +++ b/bin/build_containers.sh @@ -23,6 +23,8 @@ declare -a SERVICES=( 'test' ) +BUILD_TARGET=${BUILD_TARGET:-x86} + DOCKER_BUILD_ARGS=("buildx" "build" "--load") echo 'Make sure you ran `docker buildx create --use` before the command' @@ -31,9 +33,9 @@ if [ -n "${CLEAN_BUILD+x}" ]; then fi # Detect what platform -if [ ! -f /proc/device-tree/model ] && [ -z "${BUILD_TARGET+x}" ]; then - export BOARD="x86" +if [ ! -f /proc/device-tree/model ] && [ "$BUILD_TARGET" == 'x86' ]; then export BASE_IMAGE=debian + export BOARD="x86" export TARGET_PLATFORM=linux/amd64 elif grep -qF "Raspberry Pi 4" /proc/device-tree/model || [ "${BUILD_TARGET}" == 'pi4' ]; then export BASE_IMAGE=balenalib/raspberrypi3-debian @@ -73,23 +75,27 @@ for container in ${SERVICES[@]}; do fi if [ "$container" == 'viewer' ]; then - export QT_VERSION=5.15.14 - export WEBVIEW_GIT_HASH=4bd295c4a1197a226d537938e947773f4911ca24 - export WEBVIEW_BASE_URL="https://github.com/Screenly/Anthias/releases/download/WebView-v0.3.1" + export WEBVIEW_GIT_HASH=5e556681738a1fa918dc9f0bf5879ace2e603e12 + export WEBVIEW_BASE_URL="https://github.com/Screenly/Anthias/releases/download/WebView-v0.3.3" + + if [ "$BOARD" == 'x86' ]; then + export QT_MAJOR_VERSION=6 + export QT_VERSION=6.6.3 + else + export QT_MAJOR_VERSION=5 + export QT_VERSION=5.15.14 + fi elif [ "$container" == 'test' ]; then export CHROME_DL_URL="https://storage.googleapis.com/chrome-for-testing-public/123.0.6312.86/linux64/chrome-linux64.zip" export CHROMEDRIVER_DL_URL="https://storage.googleapis.com/chrome-for-testing-public/123.0.6312.86/linux64/chromedriver-linux64.zip" elif [ "$container" == 'wifi-connect' ]; then - # We don't support wifi-connect on x86 yet. - if [ "$BOARD" == 'x86' ]; then - continue - fi - # Logic for determining the correct architecture for the wifi-connect container if [ "$TARGET_PLATFORM" = 'linux/arm/v6' ]; then architecture=rpi - else + elif [ "$TARGET_PLATFORM" = 'linux/arm/v7' ] || [ "$TARGET_PLATFORM" = 'linux/arm/v8' ]; then architecture=armv7hf + elif [ "$TARGET_PLATFORM" = 'linux/amd64' ]; then + architecture=amd64 fi wc_download_url='https://api.github.com/repos/balena-os/wifi-connect/releases/93025295' @@ -114,13 +120,17 @@ for container in ${SERVICES[@]}; do SED_ARGS=(-i) fi - sed "${SED_ARGS[@]}" -e '/libraspberrypi0/d' $(find docker/ -maxdepth 1 -not -name "*.tmpl" -type f) + PACKAGES_TO_REMOVE=( + "libraspberrypi0" + "libgst-dev" + "libsqlite0-dev" + "libsrtp0-dev" + "libssl1.1" + ) - # Don't build the viewer container if we're on x86 - if [ "$container" == 'viewer' ]; then - echo "Skipping viewer container for x86 builds..." - continue - fi + for package in "${PACKAGES_TO_REMOVE[@]}"; do + sed "${SED_ARGS[@]}" -e "/$package/d" $(find docker/ -maxdepth 1 -not -name "*.tmpl" -type f) + done else if [ "$BOARD" == "pi1" ] && [ "$container" == "viewer" ]; then # Remove the libssl1.1 from Dockerfile.viewer diff --git a/bin/install.sh b/bin/install.sh index ac4e31ed2..ba99cb52e 100755 --- a/bin/install.sh +++ b/bin/install.sh @@ -14,6 +14,7 @@ GITHUB_RELEASES_URL="https://github.com/Screenly/Anthias/releases" GITHUB_RAW_URL="https://raw.githubusercontent.com/Screenly/Anthias" DOCKER_TAG="latest" UPGRADE_SCRIPT_PATH="${ANTHIAS_REPO_DIR}/bin/upgrade_containers.sh" +ARCHITECTURE=$(uname -m) INTRO_MESSAGE=( "Anthias requires a dedicated Raspberry Pi and an SD card." @@ -66,6 +67,8 @@ function install_prerequisites() { return fi + sudo apt -y update && sudo apt -y install gnupg + sudo mkdir -p /etc/apt/keyrings curl -fsSL https://repo.charm.sh/apt/gpg.key | \ sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg @@ -123,15 +126,15 @@ function initialize_locales() { function install_packages() { display_section "Install Packages via APT" - RASPBERRY_PI_OS_VERSION=$(lsb_release -rs) - APT_INSTALL_ARGS=( + local DISTRO_VERSION=$(lsb_release -rs) + local APT_INSTALL_ARGS=( "git" "libffi-dev" "libssl-dev" "whois" ) - if [ "$RASPBERRY_PI_OS_VERSION" -ge 12 ]; then + if [ "$DISTRO_VERSION" -ge 12 ]; then APT_INSTALL_ARGS+=( "python3-dev" "python3-full" @@ -149,8 +152,11 @@ function install_packages() { APT_INSTALL_ARGS+=("network-manager") fi - sudo sed -i 's/apt.screenlyapp.com/archive.raspbian.org/g' \ - /etc/apt/sources.list + if [ "$ARCHITECTURE" != "x86_64" ]; then + sudo sed -i 's/apt.screenlyapp.com/archive.raspbian.org/g' \ + /etc/apt/sources.list + fi + sudo apt update -y sudo apt-get install -y "${APT_INSTALL_ARGS[@]}" } @@ -185,9 +191,13 @@ function run_ansible_playbook() { sudo -u ${USER} ${SUDO_ARGS[@]} ansible localhost \ -m git \ - -a "repo=$REPOSITORY dest=${ANTHIAS_REPO_DIR} version=${BRANCH} force=no" + -a "repo=$REPOSITORY dest=${ANTHIAS_REPO_DIR} version=${BRANCH} force=yes" cd ${ANTHIAS_REPO_DIR}/ansible + if [ "$ARCHITECTURE" == "x86_64" ]; then + ANSIBLE_PLAYBOOK_ARGS+=("--skip-tags" "raspberry-pi") + fi + sudo -E -u ${USER} ${SUDO_ARGS[@]} \ ansible-playbook site.yml "${ANSIBLE_PLAYBOOK_ARGS[@]}" } @@ -374,6 +384,7 @@ function main() { install_packages install_ansible run_ansible_playbook + upgrade_docker_containers cleanup modify_permissions diff --git a/bin/upgrade_containers.sh b/bin/upgrade_containers.sh index 1bf8d1755..72717e66f 100755 --- a/bin/upgrade_containers.sh +++ b/bin/upgrade_containers.sh @@ -20,7 +20,9 @@ if [ -z "$DOCKER_TAG" ]; then fi # Detect Raspberry Pi version -if grep -qF "Raspberry Pi 4" /proc/device-tree/model; then +if [ ! -f /proc/device-tree/model ] && [ "$(uname -m)" = "x86_64" ]; then + export DEVICE_TYPE="x86" +elif grep -qF "Raspberry Pi 4" /proc/device-tree/model; then export DEVICE_TYPE="pi4" elif grep -qF "Raspberry Pi 3" /proc/device-tree/model; then export DEVICE_TYPE="pi3" @@ -47,6 +49,11 @@ cat /home/${USER}/screenly/docker-compose.yml.tmpl \ | envsubst \ > /home/${USER}/screenly/docker-compose.yml +if [ "$DEVICE_TYPE" = "x86" ]; then + sed -i '/devices:/ {N; /\n.*\/dev\/vchiq:\/dev\/vchiq/d}' \ + /home/${USER}/screenly/docker-compose.yml +fi + sudo -E docker compose \ -f /home/${USER}/screenly/docker-compose.yml \ pull diff --git a/docker/Dockerfile.base.tmpl b/docker/Dockerfile.base.tmpl index be8525d85..7c78bd01b 100644 --- a/docker/Dockerfile.base.tmpl +++ b/docker/Dockerfile.base.tmpl @@ -33,6 +33,7 @@ RUN --mount=type=cache,target=/var/cache/apt \ python3-setuptools \ python3-simplejson \ python-is-python3 \ + sudo \ sqlite3 # Works around issue with `curl` diff --git a/docker/Dockerfile.viewer.tmpl b/docker/Dockerfile.viewer.tmpl index 5cce0748a..1d58353b1 100644 --- a/docker/Dockerfile.viewer.tmpl +++ b/docker/Dockerfile.viewer.tmpl @@ -9,6 +9,7 @@ RUN --mount=type=cache,target=/var/cache/apt \ apt-get -y install --no-install-recommends \ build-essential \ ca-certificates \ + curl \ dbus-daemon \ fonts-arphic-uming \ git-core \ @@ -97,6 +98,7 @@ RUN --mount=type=cache,target=/var/cache/apt \ libzmq5-dev \ libzmq5 \ net-tools \ + procps \ psmisc \ python3-dev \ python3-gi \ @@ -106,7 +108,16 @@ RUN --mount=type=cache,target=/var/cache/apt \ python-is-python3 \ ttf-wqy-zenhei \ vlc \ - sqlite3 + sudo \ + sqlite3 \ + ffmpeg \ + libavcodec-dev \ + libavdevice-dev \ + libavfilter-dev \ + libavformat-dev \ + libavutil-dev \ + libswresample-dev \ + libswscale-dev # We need this to ensure that the wheels can be built. # Otherwise we get "invalid command 'bdist_wheel'" @@ -125,14 +136,14 @@ RUN --mount=type=cache,target=/root/.cache/pip \ RUN c_rehash # QT Base from packages does not support eglfs -RUN curl "$WEBVIEW_BASE_URL/qt5-${QT_VERSION}-${DEBIAN_VERSION}-${BOARD}.tar.gz" \ - -sL -o "/tmp/qt5-${QT_VERSION}-${DEBIAN_VERSION}-${BOARD}.tar.gz" && \ - curl "$WEBVIEW_BASE_URL/qt5-${QT_VERSION}-${DEBIAN_VERSION}-${BOARD}.tar.gz.sha256" \ - -sL -o "/tmp/qt5-${QT_VERSION}-${DEBIAN_VERSION}-${BOARD}.tar.gz.sha256" && \ +RUN curl "$WEBVIEW_BASE_URL/qt${QT_MAJOR_VERSION}-${QT_VERSION}-${DEBIAN_VERSION}-${BOARD}.tar.gz" \ + -sL -o "/tmp/qt${QT_MAJOR_VERSION}-${QT_VERSION}-${DEBIAN_VERSION}-${BOARD}.tar.gz" && \ + curl "$WEBVIEW_BASE_URL/qt${QT_MAJOR_VERSION}-${QT_VERSION}-${DEBIAN_VERSION}-${BOARD}.tar.gz.sha256" \ + -sL -o "/tmp/qt${QT_MAJOR_VERSION}-${QT_VERSION}-${DEBIAN_VERSION}-${BOARD}.tar.gz.sha256" && \ cd /tmp && \ - sha256sum -c "qt5-${QT_VERSION}-${DEBIAN_VERSION}-${BOARD}.tar.gz.sha256" && \ - tar -xzf "/tmp/qt5-${QT_VERSION}-${DEBIAN_VERSION}-${BOARD}.tar.gz" -C /usr/local && \ - rm "qt5-${QT_VERSION}-${DEBIAN_VERSION}-${BOARD}.tar.gz" + sha256sum -c "qt${QT_MAJOR_VERSION}-${QT_VERSION}-${DEBIAN_VERSION}-${BOARD}.tar.gz.sha256" && \ + tar -xzf "/tmp/qt${QT_MAJOR_VERSION}-${QT_VERSION}-${DEBIAN_VERSION}-${BOARD}.tar.gz" -C /usr/local && \ + rm "qt${QT_MAJOR_VERSION}-${QT_VERSION}-${DEBIAN_VERSION}-${BOARD}.tar.gz" RUN curl "$WEBVIEW_BASE_URL/webview-${QT_VERSION}-${DEBIAN_VERSION}-${BOARD}-${WEBVIEW_GIT_HASH}.tar.gz" \ -sL -o "/tmp/webview-${QT_VERSION}-${DEBIAN_VERSION}-${BOARD}-${WEBVIEW_GIT_HASH}.tar.gz" && \ diff --git a/host_agent.py b/host_agent.py index 0ee1bb7b1..5e104ed48 100755 --- a/host_agent.py +++ b/host_agent.py @@ -44,6 +44,7 @@ def get_ip_addresses(): def set_ip_addresses(): rdb = redis.Redis(**REDIS_ARGS) ip_addresses = get_ip_addresses() + rdb.set('ip_addresses', json.dumps(ip_addresses)) diff --git a/lib/media_player.py b/lib/media_player.py index 426e303de..7a4180450 100644 --- a/lib/media_player.py +++ b/lib/media_player.py @@ -1,15 +1,15 @@ from __future__ import unicode_literals -from builtins import object +import sh import vlc -from lib.raspberry_pi_helper import lookup_raspberry_pi_version +from lib.raspberry_pi_helper import get_device_type from settings import settings VIDEO_TIMEOUT = 20 # secs -class MediaPlayer(object): +class MediaPlayer(): def __init__(self): pass @@ -26,9 +26,36 @@ def is_playing(self): raise NotImplementedError -class VLCMediaPlayer(MediaPlayer): - INSTANCE = None +class FFMPEGMediaPlayer(MediaPlayer): + def __init__(self): + MediaPlayer.__init__(self) + self.run = None + self.player_args = list() + self.player_kwargs = dict() + + def set_asset(self, uri, duration): + self.player_args = ['ffplay', uri, '-autoexit'] + self.player_kwargs = { + '_bg': True, + '_ok_code': [0, 124], + } + + def play(self): + self.run = sh.Command(self.player_args[0])( + *self.player_args[1:], **self.player_kwargs + ) + def stop(self): + try: + self.run.kill() + except OSError: + pass + + def is_playing(self): + return bool(self.run.process.alive) + + +class VLCMediaPlayer(MediaPlayer): def __init__(self): MediaPlayer.__init__(self) @@ -38,20 +65,16 @@ def __init__(self): self.player.audio_output_set('alsa') - @classmethod - def get_instance(cls): - if cls.INSTANCE is None: - cls.INSTANCE = VLCMediaPlayer() - return cls.INSTANCE - def get_alsa_audio_device(self): if settings['audio_output'] == 'local': return 'plughw:CARD=Headphones' else: - if lookup_raspberry_pi_version() == 'pi4': + if get_device_type() == 'pi4': return 'default:CARD=vc4hdmi0' - else: + elif get_device_type() in ['pi1', 'pi2', 'pi3']: return 'default:CARD=vc4hdmi' + else: + return 'default:CARD=HID' def __get_options(self): return [ @@ -73,3 +96,17 @@ def stop(self): def is_playing(self): return self.player.get_state() in [ vlc.State.Playing, vlc.State.Buffering, vlc.State.Opening] + + +class MediaPlayerProxy(): + INSTANCE = None + + @classmethod + def get_instance(cls): + if cls.INSTANCE is None: + if get_device_type() in ['pi1', 'pi2', 'pi3', 'pi4']: + cls.INSTANCE = VLCMediaPlayer() + else: + cls.INSTANCE = FFMPEGMediaPlayer() + + return cls.INSTANCE diff --git a/lib/raspberry_pi_helper.py b/lib/raspberry_pi_helper.py index 0c3c0a58e..fc094df2e 100644 --- a/lib/raspberry_pi_helper.py +++ b/lib/raspberry_pi_helper.py @@ -26,15 +26,18 @@ def parse_cpu_info(): return cpu_info -def lookup_raspberry_pi_version(): - with open('/proc/device-tree/model') as file: - content = file.read() - - if 'Raspberry Pi 4' in content: - return 'pi4' - elif 'Raspberry Pi 3' in content: - return 'pi3' - elif 'Raspberry Pi 2' in content: - return 'pi2' - else: - return 'pi1' +def get_device_type(): + try: + with open('/proc/device-tree/model') as file: + content = file.read() + + if 'Raspberry Pi 4' in content: + return 'pi4' + elif 'Raspberry Pi 3' in content: + return 'pi3' + elif 'Raspberry Pi 2' in content: + return 'pi2' + else: + return 'pi1' + except FileNotFoundError: + return 'x86' diff --git a/tests/test_viewer.py b/tests/test_viewer.py index e5dd21a40..7224fc496 100644 --- a/tests/test_viewer.py +++ b/tests/test_viewer.py @@ -91,7 +91,7 @@ def test_load_browser(self): class TestSignalHandlers(ViewerTestCase): @mock.patch('vlc.Instance', mock.MagicMock()) @mock.patch( - 'lib.media_player.lookup_raspberry_pi_version', + 'lib.media_player.get_device_type', return_value='pi4' ) def test_usr1(self, lookup_mock): diff --git a/viewer.py b/viewer.py index 2c40b75cc..04a1ab4f7 100755 --- a/viewer.py +++ b/viewer.py @@ -27,7 +27,7 @@ from lib import assets_helper from lib import db from lib.errors import SigalrmException -from lib.media_player import VLCMediaPlayer +from lib.media_player import MediaPlayerProxy from lib.utils import ( url_fails, is_balena_app, @@ -86,7 +86,7 @@ def sigusr1(signum, frame): playing web or image asset is skipped. """ logging.info('USR1 received, skipping.') - VLCMediaPlayer.get_instance().stop() + MediaPlayerProxy.get_instance().stop() def skip_asset(back=False): @@ -381,7 +381,7 @@ def view_image(uri): def view_video(uri, duration): logging.debug('Displaying video %s for %s ', uri, duration) - media_player = VLCMediaPlayer.get_instance() + media_player = MediaPlayerProxy.get_instance() media_player.set_asset(uri, duration) media_player.play()