diff --git a/README.md b/README.md index 745b25d..047dbe7 100644 --- a/README.md +++ b/README.md @@ -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 -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 diff --git a/defaults/kiosk.yaml b/defaults/kiosk.yaml new file mode 100644 index 0000000..927dc11 --- /dev/null +++ b/defaults/kiosk.yaml @@ -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" \ + + +... \ No newline at end of file diff --git a/defaults/main.yaml b/defaults/main.yaml index d2f504b..6ab437c 100644 --- a/defaults/main.yaml +++ b/defaults/main.yaml @@ -1,69 +1,9 @@ --- - -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" - +# package list streamer_packages: - gdisk - ffmpeg - alsa-utils -# - alsa-oss - alsa-firmware-loaders - apulse - libasound2-plugins @@ -72,7 +12,25 @@ streamer_packages: - python3-venv - 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: - search_string: 'record:' line: ' record: no' @@ -84,5 +42,74 @@ mediamtx_configs: line: ' recordDeleteAfter: 0s' - search_string: 'recordMaxPartSize' 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 ... \ No newline at end of file diff --git a/files/jellyfin-old.tar.gz b/files/jellyfin-old.tar.gz new file mode 100644 index 0000000..b518abd Binary files /dev/null and b/files/jellyfin-old.tar.gz differ diff --git a/files/jellyfin.tar.gz b/files/jellyfin.tar.gz index b518abd..74bf822 100644 Binary files a/files/jellyfin.tar.gz and b/files/jellyfin.tar.gz differ diff --git a/scratch b/scratch new file mode 100644 index 0000000..a6bb2c6 --- /dev/null +++ b/scratch @@ -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 + diff --git a/tasks/arm_gui_check.yaml b/tasks/arm_gui_check.yaml new file mode 100644 index 0000000..b782299 --- /dev/null +++ b/tasks/arm_gui_check.yaml @@ -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 + + + +... \ No newline at end of file diff --git a/tasks/jellyfin.yaml b/tasks/jellyfin.yaml index 9a94998..0ada43f 100644 --- a/tasks/jellyfin.yaml +++ b/tasks/jellyfin.yaml @@ -4,6 +4,12 @@ # Install Jellyfin ############################################### +- name: jellyfin - stop and delete container if running + ignore_errors: true + shell: | + docker stop jellyfin + docker rm jellyfin + - name: jellyfin - create folder file: path: "{{ jellyfin_working_folder }}" @@ -12,6 +18,7 @@ # Extract jellyfin configs - name: jellyfin - Extract jellyfin.tar.gz + when: not refresh_special | bool unarchive: src: jellyfin.tar.gz dest: "/opt/cosmos" @@ -28,10 +35,11 @@ - name: "jellyfin - Start container at {{ jellyfin_port }}" shell: "docker-compose -f {{ jellyfin_working_folder }}/docker-compose.yaml up -d" register: jellyfin_output -- debug: - msg: - - "Docker compose output:" - - "{{ jellyfin_output.stdout_lines }}" - - "{{ jellyfin_output.stderr_lines }}" + +#- debug: +# msg: +# - "Docker compose output:" +# - "{{ jellyfin_output.stdout_lines }}" +# - "{{ jellyfin_output.stderr_lines }}" ... \ No newline at end of file diff --git a/tasks/kiosk.yaml b/tasks/kiosk.yaml deleted file mode 100644 index 6024587..0000000 --- a/tasks/kiosk.yaml +++ /dev/null @@ -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 - -... \ No newline at end of file diff --git a/tasks/main.yaml b/tasks/main.yaml index e3215a1..41eda26 100644 --- a/tasks/main.yaml +++ b/tasks/main.yaml @@ -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 when: refresh_special | bool @@ -12,6 +57,41 @@ set_fact: 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 when: not refresh_special | bool block: @@ -26,20 +106,28 @@ loop_var: streamer_packages_items - name: Video Capture - Working Folder Handler + when: not remote_deploy | bool include_tasks: working_folder.yaml - name: Video Capture - Configure MediaMTX + when: not remote_deploy | bool 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 + when: not remote_deploy | bool 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 ... \ No newline at end of file diff --git a/tasks/mediamtx.yaml b/tasks/mediamtx.yaml index babe793..067fa8d 100644 --- a/tasks/mediamtx.yaml +++ b/tasks/mediamtx.yaml @@ -18,7 +18,7 @@ group: root - name: MediaMTX - check for arm - when: '"arm" in cpu_architecture' + when: armcpu_check | bool set_fact: mediamtx_architecture: "arm64" diff --git a/tasks/preview.yaml b/tasks/preview.yaml new file mode 100644 index 0000000..035ec5b --- /dev/null +++ b/tasks/preview.yaml @@ -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 + + +... \ No newline at end of file diff --git a/tasks/service_control.yaml b/tasks/service_control.yaml index 33a6e7b..aa415d8 100644 --- a/tasks/service_control.yaml +++ b/tasks/service_control.yaml @@ -151,4 +151,28 @@ msg="{{ docker_output.stdout_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 + ... diff --git a/tasks/streamer.yaml b/tasks/streamer.yaml index 2a23020..cd5a498 100644 --- a/tasks/streamer.yaml +++ b/tasks/streamer.yaml @@ -10,6 +10,21 @@ mode: '0755' owner: 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 - name: video_capture - streamer - stop stream_service if running @@ -35,37 +50,59 @@ # same with video, the lsusb ID is 534d:0021 # 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 - shell: "v4l2-ctl --list-devices 2> /dev/null | grep -A3 '{{ v4l2_id_string }}' | grep video | head -n 1 | awk '{print $1}'" - register: video_ID_0 - -- name: video_capture - set video_device - set_fact: - video_device: "{{ video_ID_0.stdout_lines[0] }}" - + when: video_check_output.stdout | length > 0 + set_fact: + video_device: "{{ video_check_output.stdout_lines[0] }}" + no_log: true + loop: "{{ v4l2_id_string_check.results }}" + loop_control: + loop_var: video_check_output + - name: video_capture - show results debug: msg: - "Audio Device: {{ audio_device }}" - "Video Device: {{ video_device }}" -- name: video_capture - streamer - copy service script - template: - src: stream_service.sh - dest: "{{ streaming_working_folder }}/stream_service.sh" - mode: 0755 +# adding the preview service here +- name: video_capture - streamer - preview - ffmpeg service + #when: false + block: -- 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 - preview - create folder + file: + path: "{{ stream_preview_folder }}" + state: directory + mode: '0755' -- name: video_capture - streamer - daemon reload - systemd: - daemon_reload: yes + - 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 - ... \ No newline at end of file diff --git a/tasks/working_folder.yaml b/tasks/working_folder.yaml index 153a9f2..f1129c3 100644 --- a/tasks/working_folder.yaml +++ b/tasks/working_folder.yaml @@ -6,7 +6,7 @@ file: path: "{{ working_storage }}" state: directory - mode: '0755' + mode: '0777' owner: root group: root @@ -20,14 +20,14 @@ storage_unmounted: false - name: Storage Handler Block - when: storage_unmounted and mount_storge | bool + when: storage_unmounted and mount_storge and extra_storage | bool block: # find the device, architecture dependent # the arm64 is intended for the friendlyelec devices # which only have one sd slot, but still be careful - name: Storage Handler - Get Storage Device arm64 - when: '"arm64" in cpu_architecture' + when: armcpu_check | bool block: - 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 # be careful and don't run this on an inappropriate host - name: Storage Handler - Get Storage Device amd64 - when: '"amd64" in cpu_architecture' + when: not armcpu_check | bool block: - name: Storage Handler - get boot UUID @@ -188,7 +188,7 @@ path: "/etc/samba/smb.conf" search_string: "/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 service: diff --git a/templates/docker-compose-local-index.yaml b/templates/docker-compose-local-index.yaml index a748692..d8109ab 100755 --- a/templates/docker-compose-local-index.yaml +++ b/templates/docker-compose-local-index.yaml @@ -1,5 +1,5 @@ # docker-compose.yaml -version: '3' + services: local-index: image: apache-index diff --git a/templates/docker-compose-preview.yaml b/templates/docker-compose-preview.yaml new file mode 100644 index 0000000..71dce38 --- /dev/null +++ b/templates/docker-compose-preview.yaml @@ -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 + +... \ No newline at end of file diff --git a/templates/preview_service.service b/templates/preview_service.service new file mode 100644 index 0000000..0af778d --- /dev/null +++ b/templates/preview_service.service @@ -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 + diff --git a/templates/preview_service.sh b/templates/preview_service.sh new file mode 100644 index 0000000..d4e71b4 --- /dev/null +++ b/templates/preview_service.sh @@ -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 }} \ + diff --git a/templates/stream_service.sh b/templates/stream_service.sh index aedbd8a..1f96613 100644 --- a/templates/stream_service.sh +++ b/templates/stream_service.sh @@ -3,9 +3,7 @@ DATE=$(date +'%Y%m%d-%H%M%S') ffmpeg \ - -f alsa -i {{ audio_device }} -thread_queue_size 64 \ - -f v4l2 -framerate 30 -video_size 640x480 -input_format yuyv422 -i {{ video_device }} \ + -i rtsp://localhost:8554/stream \ -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 \