release candidate

This commit is contained in:
2025-10-12 16:50:32 -07:00
parent 3432e8ac9f
commit 699d7f54c8
20 changed files with 415 additions and 150 deletions

View File

@ -8,7 +8,8 @@ There is a hardware requirement of USB RCA Capture device, and this device must
There is an option to install a basic GUI and then display the feed in a browser kiosk There is an option to install a basic GUI and then display the feed in a browser kiosk
This platform requires too much processing power to be installed on weak systems. The RK3528A 4-core SBC loads up to 6 when this runs on it. Perhaps a future version of this will have a detector so that when it's installed on a wimpy ARM SBC it will do nothing except save the file; no MediaMTX or Jellyfin. Not important right now. This platform requires too much processing power to be installed on even half-decent systems. An 8-core Ryzen 5 Mini-PC is incapable of running the entire stack. I have separated the implementation of this to a client and server model. I set up a new VM on my home server that can use nearly all the CPU cores when needed, and set that with a static IP. I created two new roles that call this role with a few different variables, and then the cosmos-server pipeline can be ran with a special server selected to build the default cosmos VHS capture stack. This can probably run on an Intel i9 Mini PC or a similarly beefy Ryzen. It turns out that just the A/V to RSTP job takes a non-trivial amount of compute. Trying to do that and the video encoding at the same time generally takes more than a small CPU can offer. Now there is a VCR server VM that does the capturing of both the preview stream and the capture stream. This also saves it to the network in a spot that the website can automatically see. The client just streams to the server and displays the feed that is running from the server.
This process uses stages of different software to accomplish the goal of making capturing a VHS tape as automatic as possible This process uses stages of different software to accomplish the goal of making capturing a VHS tape as automatic as possible

27
defaults/kiosk.yaml Normal file
View File

@ -0,0 +1,27 @@
---
# kiosk vars
kiosk_service_templates:
- chrome_website: "http://{{ stream_control_host }}:8081"
service_name: user_stream_control
service_description: "VCR Capture User Stream Control"
extra_service_configs: ""
user_data_dir: "/opt/chrome/one"
extra_chrome_configs: |
--window-size="560,1080" \
--user-data-dir=/opt/chrome/one \
--device-scale-factor="200" \
- chrome_website: "http://{{ stream_preview_host }}:8888/stream"
service_name: stream_preview
service_description: "VCR Capture Preview Stream"
extra_service_configs: ""
user_data_dir: "/opt/chrome/two"
extra_chrome_configs: |
--window-size="1350,1080" \
--user-data-dir=/opt/chrome/two \
--window-position="570,0" \
--device-scale-factor="200" \
...

View File

@ -1,69 +1,9 @@
--- ---
# package list
working_storage: "/media/sd_card"
owncast_capture_folder: "{{ working_storage }}/owncast"
recording_capture_folder: "{{ working_storage }}/recordings"
streaming_working_folder: "/opt/cosmos/streamer"
mediamtx_working_folder: "/opt/cosmos/mediamtx"
jellyfin_working_folder: "/opt/cosmos/jellyfin"
jellyfin_port: "8096"
service_control_folder: "/opt/cosmos/streamer/service_control"
video_device: "/dev/video0"
v4l2_id_string: "AV TO USB2.0"
audio_device: "hw:0,0"
mediamtx_architecture: "amd64"
lsusb_device_ID: "534d:0021"
capture_device_ID_string: "MS210"
service_control_name: "stream_service.service"
service_name: "VCR Streamer"
container_name: "service_control"
mediamtx_local_version: "deadbeef_test"
storage_unmounted: true
mount_storge: true
format_storage: false
sd_unmounted: true
arm_arch: false
refresh_special: true
kiosk_models:
- "Surface Pro 2"
install_kodi: false
local_username: "cosmos"
chrome_website: "http://localhost:8888/stream"
fixed_size: "--start-fullscreen"
streamer_packages: streamer_packages:
- gdisk - gdisk
- ffmpeg - ffmpeg
- alsa-utils - alsa-utils
# - alsa-oss
- alsa-firmware-loaders - alsa-firmware-loaders
- apulse - apulse
- libasound2-plugins - libasound2-plugins
@ -72,7 +12,25 @@ streamer_packages:
- python3-venv - python3-venv
- python3-setuptools - python3-setuptools
# media_mtx variables # jenkinsfile vars
refresh_special: false
GUI_deploy: false
extra_storage: false
kiosk_refresh: false
remote_deploy: false
server_deploy: false
luna_offload: false
# jellfin vars
jellyfin_working_folder: "/opt/cosmos/jellyfin"
jellyfin_port: "8096"
jellyfin_deploy: false
# mediamtx vars
mediamtx_working_folder: "/opt/cosmos/mediamtx"
mediamtx_architecture: "amd64"
mediamtx_local_version: "deadbeef_test"
# config.yaml variables
mediamtx_configs: mediamtx_configs:
- search_string: 'record:' - search_string: 'record:'
line: ' record: no' line: ' record: no'
@ -84,5 +42,74 @@ mediamtx_configs:
line: ' recordDeleteAfter: 0s' line: ' recordDeleteAfter: 0s'
- search_string: 'recordMaxPartSize' - search_string: 'recordMaxPartSize'
line: ' recordMaxPartSize: 500M' line: ' recordMaxPartSize: 500M'
# luna offload vars
mediamtx_remote_ffmpeg: ""
mediamtx_local_ffmpeg: ""
# kiosk vars
stream_preview_host: "localhost"
stream_control_host: "localhost"
#kiosk_service_templates:
# - chrome_website: "http://localhost:8081"
# service_name: user_stream_control
# service_description: "VCR Capture User Stream Control"
# extra_service_configs: ""
# user_data_dir: "/opt/chrome/one"
# extra_chrome_configs: |
# --window-size="560,1080" \
# --user-data-dir=/opt/chrome/one \
# --device-scale-factor="200" \
# - chrome_website: "http://{{ stream_preview_host }}:8888/stream"
# service_name: stream_preview
# service_description: "VCR Capture Preview Stream"
# extra_service_configs: ""
# user_data_dir: "/opt/chrome/two"
# extra_chrome_configs: |
# --window-size="1350,1080" \
# --user-data-dir=/opt/chrome/two \
# --window-position="570,0" \
# --device-scale-factor="200" \
# rtsp site vars
rtsp_site_working_folder: "/opt/cosmos/rtsp_site"
# service control vars
service_control_folder: "/opt/cosmos/streamer/service_control"
service_control_name: "stream_service.service"
service_name: "VCR Streamer"
container_name: "service_control"
# streamer service vars
streaming_working_folder: "/opt/cosmos/streamer"
video_device: "/dev/video0"
v4l2_id_string:
- "AV TO USB2.0"
- "USB Video"
audio_device: "hw:0,0"
lsusb_device_ID: "534d:0021"
capture_device_ID_string: "MS210"
# preview service vars
stream_preview_folder: "/opt/cosmos/preview"
# working folder vars
working_storage: "/media/sd_card"
recording_capture_folder: "{{ working_storage }}/recordings"
storage_unmounted: true
mount_storge: true
format_storage: false
sd_unmounted: true
# other vars
arm_arch: false
kiosk_models:
- "Surface Pro 2"
- "11G5S0C900"
arm_gui_list:
- "RK3588"
local_username: "cosmos"
armcpu_check: false
MediaMTX_only: false
arm_device_found: false
... ...

BIN
files/jellyfin-old.tar.gz Normal file

Binary file not shown.

Binary file not shown.

7
scratch Normal file
View File

@ -0,0 +1,7 @@
ffmpeg \
-f alsa -i {{ audio_device }} -thread_queue_size 64 \
-f v4l2 -framerate 24 -video_size 640x480 -input_format yuyv422 -i {{ video_device }} \
-c:v libx264 -preset fast -crf 23 -c:a aac -b:a 192k -tune zerolatency \
-f mp4 {{ recording_capture_folder }}/$DATE.mp4

22
tasks/arm_gui_check.yaml Normal file
View File

@ -0,0 +1,22 @@
---
# need to put this little guy in a file to do a loop
- name: Video Capture - run product command
shell: "lspci | grep '{{ arm_gui_list_item }}' | head -n 1 | awk {'print $8'}"
register: prod_name_output
- name: Video Capture - check for output
ignore_errors: yes
debug:
msg: "System Info: {{prod_name_output.stdout_lines[0] }}"
- name: Video Capture - Check for GUI-approved platform
ignore_errors: yes
set_fact:
GUI_deploy: "{{ prod_name_output.stdout_lines[0] in kiosk_models }}"
arm_device_found: true
...

View File

@ -4,6 +4,12 @@
# Install Jellyfin # Install Jellyfin
############################################### ###############################################
- name: jellyfin - stop and delete container if running
ignore_errors: true
shell: |
docker stop jellyfin
docker rm jellyfin
- name: jellyfin - create folder - name: jellyfin - create folder
file: file:
path: "{{ jellyfin_working_folder }}" path: "{{ jellyfin_working_folder }}"
@ -12,6 +18,7 @@
# Extract jellyfin configs # Extract jellyfin configs
- name: jellyfin - Extract jellyfin.tar.gz - name: jellyfin - Extract jellyfin.tar.gz
when: not refresh_special | bool
unarchive: unarchive:
src: jellyfin.tar.gz src: jellyfin.tar.gz
dest: "/opt/cosmos" dest: "/opt/cosmos"
@ -28,10 +35,11 @@
- name: "jellyfin - Start container at {{ jellyfin_port }}" - name: "jellyfin - Start container at {{ jellyfin_port }}"
shell: "docker-compose -f {{ jellyfin_working_folder }}/docker-compose.yaml up -d" shell: "docker-compose -f {{ jellyfin_working_folder }}/docker-compose.yaml up -d"
register: jellyfin_output register: jellyfin_output
- debug:
msg: #- debug:
- "Docker compose output:" # msg:
- "{{ jellyfin_output.stdout_lines }}" # - "Docker compose output:"
- "{{ jellyfin_output.stderr_lines }}" # - "{{ jellyfin_output.stdout_lines }}"
# - "{{ jellyfin_output.stderr_lines }}"
... ...

View File

@ -1,43 +0,0 @@
---
- name: Kiosk - get Product Name
shell: "dmidecode | grep -A3 'System Info' | grep Product | cut -d: -f2 | cut -b2-"
register: prod_name_output
- name: Kiosk - Check for approved platform
set_fact:
install_sddm: "{{ prod_name_output.stdout_lines[0] in kiosk_models }}"
- name: Kiosk - Proceed with local kiosk install
when: install_sddm | bool
block:
# install SDDM + Plasma using Kodi role
- name: Kiosk - Install SDDM + KDE
include_role:
- kodi
# Install Chrome
- name: Install chrome
block:
- name: prereqs - Chrome - Check if installed
command: dpkg -l google-chrome-stable
register: chrome_installed
ignore_errors: true
- name: prereqs - Chrome - Set chrome_present variable
set_fact:
chrome_present: "{{ chrome_installed.rc == 0 }}"
- name: prereqs - Install Chrome
include_tasks: /var/jenkins_home/ansible/roles/install_apps/tasks/chrome.yaml
when: not chrome_present | bool
# set up chrome kiosk using lldp_scan role
- name: Kiosk - Install Chrome Kiosk Service
include_role:
- lldp_scan
...

View File

@ -1,4 +1,49 @@
--- ---
- name: Video Capture - Luna Offload Vars
when: luna_offload | bool
set_fact:
mediamtx_remote_ffmpeg: "-f rtsp rtsp://172.20.20.60:8554/stream"
stream_preview_host: "172.20.20.60"
- name: Video Capture - server only Vars
when: server_deploy | bool
set_fact:
stream_preview_host: "172.20.20.60"
stream_control_host: "172.20.20.60"
- name: Video Capture - Local stream vars
when: remote_deploy | bool
set_fact:
mediamtx_local_ffmpeg: "-f rtsp rtsp://172.20.20.60:8554/stream"
- name: Video Capture - Remote stream vars
when: not remote_deploy | bool
set_fact:
mediamtx_local_ffmpeg: "-f rtsp rtsp://localhost:8554/stream"
- name: Video Capture - MediaMTX Only
when: MediaMTX_only | bool
block:
- name: include mediamtx task
include_tasks: mediamtx.yaml
- meta: end_play
- name: Video Capture - Kiosk Only
when: kiosk_refresh | bool
block:
- name: display initial kiosk_service_templates
debug:
msg: "{{ kiosk_service_templates }}"
- name: Run the chrome kiosk role when it's GUI time
include_role:
name: chrome_kiosk
when: GUI_deploy | bool
- meta: end_play
- name: Video Capture - Check arch if needed - name: Video Capture - Check arch if needed
when: refresh_special | bool when: refresh_special | bool
@ -12,6 +57,41 @@
set_fact: set_fact:
cpu_architecture: "{{ cpu_architecture_output.stdout_lines[0] }}" cpu_architecture: "{{ cpu_architecture_output.stdout_lines[0] }}"
# override GUI_deploy if unauthorized
# if non-1080p resolutions are needed
# chrome configs can be set here on a
# case-by-case basis
- name: Video Capture - get Product Name for x86
when: GUI_deploy | bool and not armcpu_check | bool
block:
- name: Video Capture - run product command
shell: "dmidecode | grep -A3 'System Info' | grep Product | cut -d: -f2 | cut -b2-"
register: prod_name_output
- name: Video Capture - show output
debug:
msg: "System Info: {{prod_name_output.stdout_lines[0] }}"
- name: Video Capture - Check for GUI-approved platform
ignore_errors: yes
set_fact:
GUI_deploy: "{{ prod_name_output.stdout_lines[0] in kiosk_models }}"
- name: Video Capture - get Product Name for arm
when: GUI_deploy | bool and armcpu_check | bool and not arm_device_found | bool
block:
- name: Video capture - arm check loop
loop: "{{ arm_gui_list }}"
loop_control:
loop_var: arm_gui_list_item
include_tasks: arm_gui_check.yaml
- name: Video Capture - Will we GUI?
debug:
msg: "GUI Deploy: {{ GUI_deploy }}"
- name: Video Capture - Run Once Block - name: Video Capture - Run Once Block
when: not refresh_special | bool when: not refresh_special | bool
block: block:
@ -26,20 +106,28 @@
loop_var: streamer_packages_items loop_var: streamer_packages_items
- name: Video Capture - Working Folder Handler - name: Video Capture - Working Folder Handler
when: not remote_deploy | bool
include_tasks: working_folder.yaml include_tasks: working_folder.yaml
- name: Video Capture - Configure MediaMTX - name: Video Capture - Configure MediaMTX
when: not remote_deploy | bool
include_tasks: mediamtx.yaml include_tasks: mediamtx.yaml
- name: Video Capture - Configure Jellyfin
when: '"amd64" in cpu_architecture'
include_tasks: jellyfin.yaml
- name: Video Capture - Configure Streaming
include_tasks: streamer.yaml
- name: Video Capture - Configure service control - name: Video Capture - Configure service control
when: not remote_deploy | bool
include_tasks: service_control.yaml include_tasks: service_control.yaml
- name: Video Capture - Configure Streaming
when: not server_deploy | bool
include_tasks: streamer.yaml
- name: Video Capture - Configure Jellyfin
when: '"amd64" in cpu_architecture and jellyfin_deploy | bool and not remote_deploy | bool'
include_tasks: jellyfin.yaml
- name: Run the chrome kiosk role when it's GUI time
include_role:
name: chrome_kiosk
when: GUI_deploy | bool and not server_deploy | bool
... ...

View File

@ -18,7 +18,7 @@
group: root group: root
- name: MediaMTX - check for arm - name: MediaMTX - check for arm
when: '"arm" in cpu_architecture' when: armcpu_check | bool
set_fact: set_fact:
mediamtx_architecture: "arm64" mediamtx_architecture: "arm64"

31
tasks/preview.yaml Normal file
View File

@ -0,0 +1,31 @@
---
# this is not just for preview but for capture too
- name: video_capture - streamer - preview - create folder
file:
path: "{{ stream_preview_folder }}"
state: directory
mode: '0755'
- name: video_capture - streamer - preview - copy service script
template:
src: preview_service.sh
dest: "{{ stream_preview_folder }}/preview_service.sh"
mode: 0755
- name: video_capture - streamer - preview - create service file
template:
src: preview_service.service
dest: /etc/systemd/system/preview_service.service
mode: 0644
- name: video_capture - streamer - preview - daemon reload
systemd:
name: preview_service.service
state: started
enabled: yes
daemon_reload: yes
...

View File

@ -151,4 +151,28 @@
msg="{{ docker_output.stdout_lines }}" msg="{{ docker_output.stdout_lines }}"
msg="{{ docker_output.stderr_lines }}" msg="{{ docker_output.stderr_lines }}"
# create streaming service
# this used to live with the streamer but i moved it here because most of the time this will be split up
# the names don't make as much sense anymore either...
# oh well...
- name: video_capture - fil - ffmpeg service
#when: false
block:
- name: video_capture - streamer - copy service script
template:
src: stream_service.sh
dest: "{{ streaming_working_folder }}/stream_service.sh"
mode: 0755
- name: video_capture - streamer - create service file
template:
src: stream_service.service
dest: /etc/systemd/system/stream_service.service
mode: 0644
- name: video_capture - streamer - daemon reload
systemd:
daemon_reload: yes
... ...

View File

@ -10,6 +10,21 @@
mode: '0755' mode: '0755'
owner: root owner: root
group: root group: root
register: folder_output
- debug:
msg: "{{ folder_output }}"
# Create capture Folder
- name: "video_capture - streamer - create {{ recording_capture_folder }} folder"
file:
path: "{{ recording_capture_folder }}"
state: directory
mode: '0755'
owner: root
group: root
register: folder_output_2
- debug:
msg: "{{ folder_output_2 }}"
# this service shouldn't stay running # this service shouldn't stay running
- name: video_capture - streamer - stop stream_service if running - name: video_capture - streamer - stop stream_service if running
@ -35,37 +50,59 @@
# same with video, the lsusb ID is 534d:0021 # same with video, the lsusb ID is 534d:0021
# v4l2-ctl shows it as "AV TO USB2.0" # v4l2-ctl shows it as "AV TO USB2.0"
# this checks a list and saves the only correct output
- name: video_capture - check for each video device
shell: "v4l2-ctl --list-devices 2> /dev/null | grep -A3 '{{ v4l2_strings }}' | grep video | head -n 1 | awk '{print $1}' || true"
register: v4l2_id_string_check
loop: "{{ v4l2_id_string }}"
loop_control:
loop_var: v4l2_strings
- name: video_capture - get video device - name: video_capture - get video device
shell: "v4l2-ctl --list-devices 2> /dev/null | grep -A3 '{{ v4l2_id_string }}' | grep video | head -n 1 | awk '{print $1}'" when: video_check_output.stdout | length > 0
register: video_ID_0 set_fact:
video_device: "{{ video_check_output.stdout_lines[0] }}"
- name: video_capture - set video_device no_log: true
set_fact: loop: "{{ v4l2_id_string_check.results }}"
video_device: "{{ video_ID_0.stdout_lines[0] }}" loop_control:
loop_var: video_check_output
- name: video_capture - show results - name: video_capture - show results
debug: debug:
msg: msg:
- "Audio Device: {{ audio_device }}" - "Audio Device: {{ audio_device }}"
- "Video Device: {{ video_device }}" - "Video Device: {{ video_device }}"
- name: video_capture - streamer - copy service script # adding the preview service here
template: - name: video_capture - streamer - preview - ffmpeg service
src: stream_service.sh #when: false
dest: "{{ streaming_working_folder }}/stream_service.sh" block:
mode: 0755
- name: video_capture - streamer - create service file - name: video_capture - streamer - preview - create folder
template: file:
src: stream_service.service path: "{{ stream_preview_folder }}"
dest: /etc/systemd/system/stream_service.service state: directory
mode: 0644 mode: '0755'
- name: video_capture - streamer - daemon reload - name: video_capture - streamer - preview - copy service script
systemd: template:
daemon_reload: yes src: preview_service.sh
dest: "{{ stream_preview_folder }}/preview_service.sh"
mode: 0755
- name: video_capture - streamer - preview - create service file
template:
src: preview_service.service
dest: /etc/systemd/system/preview_service.service
mode: 0644
- name: video_capture - streamer - preview - daemon reload
systemd:
name: preview_service.service
state: started
enabled: yes
daemon_reload: yes
... ...

View File

@ -6,7 +6,7 @@
file: file:
path: "{{ working_storage }}" path: "{{ working_storage }}"
state: directory state: directory
mode: '0755' mode: '0777'
owner: root owner: root
group: root group: root
@ -20,14 +20,14 @@
storage_unmounted: false storage_unmounted: false
- name: Storage Handler Block - name: Storage Handler Block
when: storage_unmounted and mount_storge | bool when: storage_unmounted and mount_storge and extra_storage | bool
block: block:
# find the device, architecture dependent # find the device, architecture dependent
# the arm64 is intended for the friendlyelec devices # the arm64 is intended for the friendlyelec devices
# which only have one sd slot, but still be careful # which only have one sd slot, but still be careful
- name: Storage Handler - Get Storage Device arm64 - name: Storage Handler - Get Storage Device arm64
when: '"arm64" in cpu_architecture' when: armcpu_check | bool
block: block:
- name: Storage Handler - get boot device - name: Storage Handler - get boot device
@ -39,7 +39,7 @@
# this will choose the first storage device that it finds that isn't the boot device # this will choose the first storage device that it finds that isn't the boot device
# be careful and don't run this on an inappropriate host # be careful and don't run this on an inappropriate host
- name: Storage Handler - Get Storage Device amd64 - name: Storage Handler - Get Storage Device amd64
when: '"amd64" in cpu_architecture' when: not armcpu_check | bool
block: block:
- name: Storage Handler - get boot UUID - name: Storage Handler - get boot UUID
@ -188,7 +188,7 @@
path: "/etc/samba/smb.conf" path: "/etc/samba/smb.conf"
search_string: "/etc/samba/smb.conf.d/vcr_rip.conf" search_string: "/etc/samba/smb.conf.d/vcr_rip.conf"
line: "include = /etc/samba/smb.conf.d/vcr_rip.conf" line: "include = /etc/samba/smb.conf.d/vcr_rip.conf"
insertbefore: '[global]' insertafter: '### smb.conf.d configs here'
- name: Working Folder - restart smbd - name: Working Folder - restart smbd
service: service:

View File

@ -1,5 +1,5 @@
# docker-compose.yaml # docker-compose.yaml
version: '3'
services: services:
local-index: local-index:
image: apache-index image: apache-index

View File

@ -0,0 +1,14 @@
---
services:
web:
image: nginx:latest
ports:
- "8888:80"
volumes:
- {{ stream_preview_folder }}/nginx.conf:/etc/nginx/nginx.conf
- {{ stream_preview_folder }}/html:/usr/share/nginx/html
network_mode: bridge
restart: always
...

View File

@ -0,0 +1,15 @@
[Unit]
Description=VCR Stream Preview
After=network.target
[Service]
Type=simple
KillSignal=SIGINT
WorkingDirectory={{ stream_preview_folder }}
ExecStart={{ stream_preview_folder }}/preview_service.sh
User=root
Group=root
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,9 @@
#!/bin/bash
ffmpeg \
-f alsa -i {{ audio_device }} -thread_queue_size 64 \
-f v4l2 -framerate 24 -video_size 640x480 -input_format yuyv422 -i {{ video_device }} \
-c:v libx264 -preset fast -crf 23 -c:a aac -b:a 192k -tune zerolatency \
{{ mediamtx_local_ffmpeg }} \
{{ mediamtx_remote_ffmpeg }} \

View File

@ -3,9 +3,7 @@
DATE=$(date +'%Y%m%d-%H%M%S') DATE=$(date +'%Y%m%d-%H%M%S')
ffmpeg \ ffmpeg \
-f alsa -i {{ audio_device }} -thread_queue_size 64 \ -i rtsp://localhost:8554/stream \
-f v4l2 -framerate 30 -video_size 640x480 -input_format yuyv422 -i {{ video_device }} \
-c:v libx264 -preset fast -crf 23 -c:a aac -b:a 192k -tune zerolatency \ -c:v libx264 -preset fast -crf 23 -c:a aac -b:a 192k -tune zerolatency \
-f rtsp rtsp://localhost:8554/stream \ -f mp4 {{ recording_capture_folder }}/$DATE.mp4 \
-f mp4 {{ recording_capture_folder }}/$DATE.mp4