commit c34b325cf0689935098c43729a950ef9733e7827 Author: phermeys Date: Tue Aug 19 11:40:26 2025 -0700 initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..25d0fba --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +I need one more component here, the service manager service site +I'll use what I built for the carputer, and add to it +Things to add: +- Duration selection grid +- Duration Submission Selection API +- Cron job + helper script + +I will update the API to maintain the selection state and return it from the API +I will write a selection grid that updates color depending on selction +It will also be a styled radio button states 0-3 for 6ht, 2hr, 1hr, and 30min +I will write a helper cron script that runs every second that will stop the service when the API says so +The API will have a function that will return the expected state of the service +When the helper script sees it not match, it will stop the service diff --git a/archive/docker-compose-owncast.yaml.j2 b/archive/docker-compose-owncast.yaml.j2 new file mode 100644 index 0000000..f43b513 --- /dev/null +++ b/archive/docker-compose-owncast.yaml.j2 @@ -0,0 +1,14 @@ +services: + + owncast: + container_name: owncast + image: owncast/owncast:latest + volumes: + - {{ owncast_working_folder }}/data:/app/data + - {{ owncast_capture_folder }}:/app/capture + ports: + - 8080:8080 + - 1935:1935 + restart: always + network_mode: bridge + diff --git a/archive/owncast.yaml b/archive/owncast.yaml new file mode 100644 index 0000000..341c004 --- /dev/null +++ b/archive/owncast.yaml @@ -0,0 +1,29 @@ +############################################### +# Start owncast +############################################### +- name: start owncast + block: + + # Create service Folder + - name: video_capture - owncast - create owncast_working_folder folder + file: + path: "{{ owncast_working_folder }}" + state: directory + mode: '0755' + owner: root + group: root + +# restore data later once i have a working setup + + - name: video_capture - owncast - template config + template: + src: docker-compose-owncast.yaml.j2 + dest: "{{ owncast_working_folder }}/docker-compose.yaml" + mode: 0644 + + - name: "video_capture - owncast - Start owncast container" + shell: "docker-compose -f {{ owncast_working_folder }}/docker-compose.yaml up -d" + register: docker_output + - debug: | + msg="{{ docker_output.stdout_lines }}" + msg="{{ docker_output.stderr_lines }}" \ No newline at end of file diff --git a/defaults/main.yaml b/defaults/main.yaml new file mode 100644 index 0000000..6d1c595 --- /dev/null +++ b/defaults/main.yaml @@ -0,0 +1,63 @@ +--- + +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" + +service_control_folder: "/opt/cosmos/streamer/service_control" + +video_device: "/dev/video0" + +audio_device: "hw:0,0" + +mediamtx_version: "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" + +mount_sd: true + +format_sd: false + +sd_unmounted: true + +arm_arch: false + +streamer_packages: + - ffmpeg + - alsa-utils + - alsa-oss + - alsa-firmware-loaders + - apulse + - libasound2-plugins + - v4l-utils + - usbutils + - python3-venv + +# media_mtx variables +mediamtx_configs: + - search_string: 'record:' + line: ' record: yes' + - search_string: 'recordPath' + line: ' recordPath: {{ recording_capture_folder }}/%path/%Y-%m-%d_%H-%M/%H-%M-%S-%f' + - search_string: 'recordSegmentDuration' + line: ' recordSegmentDuration: 3600s' + - search_string: 'recordDeleteAfter' + line: ' recordDeleteAfter: 0s' + - search_string: 'recordMaxPartSize' + line: ' recordMaxPartSize: 500M' + +... \ No newline at end of file diff --git a/files/service_control_api/website/index.php b/files/service_control_api/website/index.php new file mode 100644 index 0000000..06546aa --- /dev/null +++ b/files/service_control_api/website/index.php @@ -0,0 +1,233 @@ +"; + $button_recent = true; + $button_result = runAPI($_GET['action']); + $debug_string = $debug_string."Button Result: ".$button_result."
"; + sleep(1); +} + +function runAPI($submitted_status) { + if(!isset($debug_string)){ + $debug_string = ""; + } + $debug_string = $debug_string."runAPI called, ".$submitted_status."
"; + switch ($submitted_status) { + case "stop": + $apiUrl = "http://172.17.0.1:5000/stop"; + break; + case "start": + $apiUrl = "http://172.17.0.1:5000/start"; + break; + default: + $apiUrl = "http://172.17.0.1:5000/status"; + break; + } + // API URL + $debug_string = $debug_string."After switch, apiUrl is ".$apiUrl."
"; + // Use file_get_contents or cURL to fetch the API data + try { + $response = file_get_contents($apiUrl); + } catch (Exception $e) { + $response = '{ "Message": "unknowable", "Status": "unknowable" }'; + } + if ($response === FALSE) { + $debug_string = $debug_string."Response ERROR!!
"; + return "error"; // API error + } + + // Decode the JSON response (assuming the API returns JSON) + $data = json_decode($response, true); + $debug_string = $debug_string."Data from API call: ".$data['Message']."
"; + // Assuming the API returns a single word result (like "success", "failure", etc.) + return isset($data['Status']) ? $data['Status'] : 'unknown'; +} + +// | awk '{printf(\"%.5f\n\", $1)}' +// | awk '{printf(\"%.5f\n\", $1)}' + + +function getGPS(){ + // check the API + $gps_data = file_get_contents("http://172.17.0.1:5000/return_gps"); + try { + $gps_data = json_decode($gps_data, true); + + } catch (Exception $e){ + $gps_data = '{c}'; + } + + //set the vars + $LAT = $gps_data['lat']; + $LON = $gps_data['lon']; + $SPEED = $gps_data['speed']; + + if (is_null($LAT) || $LAT == 0) { + return "No GPS data available - null LAT"; + } + if (is_null($LON) || $LON == 0) { + return "No GPS data available - null LON"; + } + + return $LAT.", ".$LON.", ".$SPEED."mph"; +} + +// URL of the external API +$apiUrl = "http://172.17.0.1:5000/status"; + +// Use file_get_contents to fetch data from the API +try { + $response = file_get_contents($apiUrl); +} catch (Exception $e) { + $response = '{ "Message": "unknowable", "Status": "unknowable" }'; +} + +// Check if the request was successful +if ($response === FALSE) { + $data = "Failed to fetch data."; +} +else { + // Decode the JSON response (assuming the API returns JSON) + $data = json_decode($response, true); + // If you want to display specific data, adjust this part + // For example, if the API returns a 'message' key, display it + if (isset($data['Message'])) { + $message = $data['Message']; + } else { + $message = "No message found in the response."; + } + if (isset($data['Status'])) { + $status = $data['Status']; + } else { + $status = "None"; + } +} + +switch ($status) { + case "active": + $button_text = "Stop Service"; + $button_active = true; + break; + case "inactive": + $button_text = "Start Service"; + $button_active = true; + break; + case "failed": + $button_text = "Start Service - Warning"; + $button_active = true; + break; + case "deactivating": + $button_text = "Service Stopping..."; + $button_active = false; + break; + case "unknowable": + $button_text = "Service Manager Down..."; + $button_active = false; + break; + default: + $button_text = "Error, No Status"; + $button_active = false; + $status="unknown"; + break; +} + +if($button_active && !$button_recent){ + $action = ""; + if($status == "active"){ + $action = "stop"; + } + else if($status == "inactive"){ + $action = "start"; + } + else if($status == "failed"){ + $action = "start"; + } + $button_action = ' onclick="location.href=\'/?action='.$action.'\'" '; +} + +if($button_recent){ + /* + $delay = 5; // Number of seconds before redirection + echo "You will be redirected in $delay seconds."; + header("Refresh: $delay; url=https://example.com"); + exit(); + */ + header("Location: http://".$http_host); + exit(); // Always include exit() after headers are sent +} + +?> + + + + + + + + API Data Display + + + + + +
+ +
+ +

+

+

+

+ + ".htmlspecialchars($message)."

"; + echo "Current Date:
".date("F j, Y, g:i:s a")."

"; + echo "Current GPS Data:
".getGPS(); + ?> +

+ +
+ + + + + + + diff --git a/files/service_control_api/website/styles.css b/files/service_control_api/website/styles.css new file mode 100644 index 0000000..066c1b6 --- /dev/null +++ b/files/service_control_api/website/styles.css @@ -0,0 +1,86 @@ + +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + background-color: #2c3e50; /* Dark background color */ + color: #bdc3c7; /* Dimmer text color */ + zoom: 1.5; + overflow: hidden; /* Hide scrollbars */ +} +.container { + max-width: 500px; + margin: 0 auto; + margin-top: 20px; + padding: 20px; + background-color: #34495e; /* Darker background for container */ + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); /* Slightly darker shadow */ +} +.api-data { + font-size: 1.4rem; + color: #bdc3c7; /* Dimmer text color */ +} +.loading { + font-size: 1.2rem; + background-color: #34495e; /* Darker background for container-wide */ +} +.status-button { + padding: 10px 20px; + font-size: 1.5rem; + border: none; + cursor: pointer; + margin-top: 20px; + border-radius: 5px; +} +.button-container { + display: flex; + justify-content: center; /* This centers the button horizontally */ +} +button { + background: radial-gradient(circle, #ff7e5f 0%, #feb47b 100%); + border: none; + color: white; + font-size: 30px; + padding: 25px 50px; + text-align: center; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); + border-radius: 30px; + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); + transition: all 0.3s cubic-bezier(.25,.8,.25,1); +} + +button:hover { + transform: scale(1.05); + box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); +} +.inactive { + background-color: #0b660e; + background: radial-gradient(circle, #0b660e 0%, #2b922f 100%); + color: #bdc3c7; /* Dimmer text color */ +} +.active { + background-color: #a5150b; + background: radial-gradient(circle, #a5150b 0%, #d6382d 100%); + color: #bdc3c7; /* Dimmer text color */ +} +.failed { + background-color: #671281; + background: radial-gradient(circle, #671281 0%, #8423a1 100%); + color: #bdc3c7; /* Dimmer text color */ +} +.deactivating { + background-color: #003699; + background: radial-gradient(circle, #003699 0%, #337aff 100%); + color: #bdc3c7; /* Dimmer text color */ +} +.deactivating { + background-color: #0d2b2c; + background: radial-gradient(circle, #0d2b2c 0%, #123738 100%); + color: #bdc3c7; /* Dimmer text color */ +} +.unknown { + background-color: #ec701e; + background: radial-gradient(circle, #c9580d 0%, #ec701e 100%); + color: #bdc3c7; /* Dimmer text color */ +} \ No newline at end of file diff --git a/tasks/main.yaml b/tasks/main.yaml new file mode 100644 index 0000000..f0d8e6a --- /dev/null +++ b/tasks/main.yaml @@ -0,0 +1,42 @@ +--- + +# - name: Video Capture - Configure Owncast +# include_tasks: owncast.yaml + +- name: Video Capture - Check arch if needed + when: refresh_special | bool + block: + + - name: Video Capture - Check CPU Arch + shell: "dpkg --print-architecture" + register: cpu_architecture_output + + - name: Video Capture - Set cpu_architecture variable + set_fact: + cpu_architecture: "{{ cpu_architecture_output.stdout_lines[0] }}" + +- name: Video Capture - Install Packages + when: not refresh_special | bool + apt: + name: + - "{{ streamer_packages_items }}" + state: present + loop: "{{ streamer_packages }}" + loop_control: + loop_var: streamer_packages_items + +- name: Video Capture - SD Card Handler + when: mount_sd | bool + include_tasks: sd_handler.yaml + +- name: Video Capture - Configure MediaMTX + include_tasks: mediamtx.yaml + +- name: Video Capture - Configure Streaming + include_tasks: streamer.yaml + +#- name: Video Capture - Configure service control +# include_tasks: service_control.yaml + + +... \ No newline at end of file diff --git a/tasks/mediamtx.yaml b/tasks/mediamtx.yaml new file mode 100644 index 0000000..eec0b43 --- /dev/null +++ b/tasks/mediamtx.yaml @@ -0,0 +1,82 @@ +--- + +# mediamtx might automatically make video files + +# newest release URL: +# curl -sL https://api.github.com/repos/bluenviron/mediamtx/releases/latest | \ +# grep browser_download_url | grep linux_amd64 | cut -d\" -f 4 + +- name: MediaMTX - stop mediamtx_service if running + systemd: + name: mediamtx_service.service + state: stopped + ignore_errors: yes + +# Create service Folders +- name: MediaMTX - create mediamtx_working_folder folder + file: + path: "{{ mediamtx_working_folder }}" + state: directory + mode: '0755' + owner: root + group: root + +- name: MediaMTX - check for arm + when: '"arm" in cpu_architecture' + set_fact: + mediamtx_version: "arm64" + +- name: MediaMTX - get current release URL + shell: | + curl -sL https://api.github.com/repos/bluenviron/mediamtx/releases/latest | \ + grep browser_download_url | grep linux_{{ mediamtx_version }} | cut -d\" -f 4 + register: mediamtx_latest_url +- debug: + msg: "URL To Download: {{ mediamtx_latest_url.stdout_lines[0] }}" + +- name: MediaMTX - get current release archive + shell: "curl -s -o {{ mediamtx_working_folder }}/mediamtx.tar.gz -L {{ mediamtx_latest_url.stdout_lines[0] }}" + +- name: MediaMTX - extract archive + unarchive: + # src: "/var/jenkins_home/ansible-files/programs/mediamtx_v1.14.0_linux_amd64.tar.gz" + src: "{{ mediamtx_working_folder }}/mediamtx.tar.gz" + dest: "{{ mediamtx_working_folder }}" + mode: '0755' + remote_src: yes + +- name: MediaMTX - update configs + lineinfile: + path: "{{ mediamtx_working_folder }}/mediamtx.yml" + search_string: "{{ mediamtx_configs_item.search_string }}" + line: "{{ mediamtx_configs_item.line }}" + loop: "{{ mediamtx_configs }}" + loop_control: + loop_var: mediamtx_configs_item + +# - name: MediaMTX - config - enable recording +# lineinfile: +# path: "{{ mediamtx_working_folder }}/mediamtx.yml" +# search_string: 'record' +# line: ' record: yes' +# +- name: MediaMTX - create service file + template: + src: mediamtx_service.service.j2 + dest: "/etc/systemd/system/mediamtx_service.service" + mode: 0644 + +# daemon reload +- name: MediaMTX - daemon reload + systemd: + daemon_reload: yes + +# Enable and start +- name: MediaMTX - enable and start mediamtx_service + systemd: + name: mediamtx_service.service + state: started + enabled: yes + + +... \ No newline at end of file diff --git a/tasks/sd_handler.yaml b/tasks/sd_handler.yaml new file mode 100644 index 0000000..b8cbe75 --- /dev/null +++ b/tasks/sd_handler.yaml @@ -0,0 +1,102 @@ +--- + +# when ran, this will look for an SD card +# optionally format it +# map it to working_storage +# can only run on arm64 +# '"arm64" in cpu_architecture' + + +- name: Video Capture - Check for SD + shell: "df" + register: df_check_output + +- name: Video Capture - Set sd_unmounted + when: working_storage in df_check_output.stdout + set_fact: + sd_unmounted: false + +- name: Video Capture - Set arm_arch + when: '"arm64" in cpu_architecture' + set_fact: + arm_arch: true + +- name: SD Handler - Checks have passed + when: sd_unmounted and arm_arch | bool + block: + + - name: SD Handler - format SD card + when: format_sd | bool + block: + + - name: dummy task + debug: + msg: "nothing to see here, move along" + + - name: SD Handler - find SD card + block: + + - name: SD Handler - get boot device + shell: "blkid | grep '\"boot\"' | cut -d: -f 1 | cut -d/ -f 3 | cut -dp -f 1" + register: boot_device + + - name: SD Handler - get sd card device + shell: "lsblk -o NAME,SIZE --nodeps | grep -v -e loop -e {{ boot_device.stdout_lines[0] }} -e NAME | cut -d ' ' -f 1" + register: sd_card_device + + - name: SD Handler - get sd card uuid + shell: "blkid | grep {{ sd_card_device.stdout_lines[0] }} | awk '{for (i=1; i<=NF; i++) print $i}' | grep UUID | grep -v PART | cut -d '\"' -f 2" + register: sd_card_uuid_output + + - name: SD Handler - set uuid variable + set_fact: + sd_card_uuid: "{{ sd_card_uuid_output.stdout_lines[0] }}" + + - debug: + msg: "UUID Found: {{ sd_card_uuid }}" + + - name: SD Handler - mount sd card + block: + + - name: SD Handler - check folder + file: + path: "{{ working_storage }}" + state: directory + mode: '0755' + owner: root + group: root + + - name: SD Handler - Generate fstab entry + set_fact: + fstab_line_sd: "UUID={{ sd_card_uuid }} {{ working_storage }} ext4 errors=remount-ro 0 1" + + - name: SD Handler - add fstab entry + lineinfile: + path: "/etc/fstab" + search_string: "{{ sd_card_uuid }}" + line: "{{ fstab_line_sd }}" + + - debug: + msg: | + fstab entry: + {{ fstab_line_sd }} + + - name: SD Handler - daemon reload + systemd: + daemon_reload: yes + + - name: SD Handler - Mount it + shell: mount -a + + - name: SD Handler - validate this + block: + + - name: SD Handler - check for new mount point + shell: "df -h | grep -e Size -e {{ working_storage }}" + register: sd_test_output + + - debug: + msg: "{{ sd_test_output.stdout_lines }}" + + +... \ No newline at end of file diff --git a/tasks/service_control.yaml b/tasks/service_control.yaml new file mode 100644 index 0000000..6810175 --- /dev/null +++ b/tasks/service_control.yaml @@ -0,0 +1,139 @@ +--- + +############################################### +# lifted directly from my carputer playbook +############################################### + +############################################### +# This part sets up python serice control api +############################################### + +- name: service control api + block: + + # Stop service + - name: video_capture - service_control api - stop service api + systemd: + name: service_control.service + state: stopped + ignore_errors: yes + + # Create API Folder + - name: video_capture - service_control api - create api folder + file: + path: "{{ service_control_folder }}" + state: directory + mode: '0755' + + # Copy API Code + - name: video_capture - service_control api - copy api code + template: + src: app-service.py.j2 + dest: "{{ service_control_folder }}/app.py" + mode: 0644 + + # Create service_control api service + - name: video_capture - service_control api - create requirement file + copy: + dest: "{{ service_control_folder }}/requirements.txt" + content: | + Flask==2.1.0 + pytz + requests + lxml + Werkzeug==2.0 + mode: 0644 + + # build venv + - name: video_capture - service_control api - build venv + pip: + virtualenv: "{{ service_control_folder }}/venv" + requirements: "{{ service_control_folder }}/requirements.txt" + virtualenv_command: python3 -m venv + state: present + + # Create service_control api service + - name: video_capture - service_control api - create service file + # vars: + template: + src: service_control.service.j2 + dest: /etc/systemd/system/service_control.service + mode: 0644 + + # daemon reload + - name: video_capture - service_control api - daemon reload + systemd: + daemon_reload: yes + + # Enable and start + - name: video_capture - service_control api - enable and start service api + systemd: + name: service_control.service + state: started + enabled: yes + + + +############################################### +# This part sets up serice control website +############################################### + +- name: service control web interface + block: + + - name: set docker folder variable + set_fact: + service_control_web_folder: "{{ service_control_folder }}/web" + + # Create docker Folder + - name: service_control_website - create service_control_web_folder folder + file: + path: "{{ service_control_web_folder }}" + state: directory + mode: '0755' + owner: root + group: root + + - name: service_control_website - copy files for docker container + copy: + src: "service_control_api/website/" + dest: "{{ service_control_web_folder }}/html" + mode: 0755 + owner: root + group: root + +# - name: service_control_website - template index.php +# template: +# src: index-service_control.php.j2 +# dest: "{{ service_control_web_folder }}/html/index.php" +# mode: 0644 + + ############################################### + # Start service_control_website + ############################################### +# https://unix.stackexchange.com/questions/265704/start-stop-a-systemd-service-at-specific-times +# i can create several conflicting services with various timeouts + + - name: start service_control_website + block: + - name: set container variables + set_fact: + container_name: "service_control_website" + container_http_port: "8081" + + - name: service_control_website - template config + template: + src: docker-compose-php.yaml.j2 + dest: "{{ service_control_web_folder }}/docker-compose.yaml" + mode: 0644 + + - name: "service_control_website - Start container at 0.0.0.0:{{ container_http_port }}" + shell: "docker-compose -f {{ service_control_web_folder }}/docker-compose.yaml up -d" + register: docker_output + - debug: | + msg="{{ docker_output.stdout_lines }}" + msg="{{ docker_output.stderr_lines }}" + + + +... diff --git a/tasks/streamer.yaml b/tasks/streamer.yaml new file mode 100644 index 0000000..0b29937 --- /dev/null +++ b/tasks/streamer.yaml @@ -0,0 +1,72 @@ +--- +# video stream with ustreamer +# audio stream probably just a device +# looks like ffmpeg can do it + +# Create service Folder +- name: video_capture - streamer - create streaming_working_folder folder + file: + path: "{{ streaming_working_folder }}" + state: directory + mode: '0755' + owner: root + group: root + +# this service shouldn't stay running +- name: video_capture - streamer - stop stream_service if running + systemd: + name: stream_service.service + state: stopped + ignore_errors: yes + +# gonna try to automatically find the audio info +# the card is MS201 + +- name: video_capture - get card ID + shell: "arecord -l | grep {{ capture_device_ID_string }} | cut -d: -f1 | rev | cut -b 1" + register: sound_ID_0 + +- name: video_capture - get device ID + shell: "arecord -l | grep {{ capture_device_ID_string }} | cut -d: -f2 | rev | cut -b 1" + register: sound_ID_1 + +- name: video_capture - set audio_device + set_fact: + audio_device: "hw:{{ sound_ID_0.stdout_lines[0] }},{{ sound_ID_1.stdout_lines[0] }}" + +# same with video, the lsusb ID is 534d:0021 + +- name: video_capture - get video device + shell: "v4l2-ctl --list-devices -z {{ lsusb_device_ID }}| grep video | head -n 1" + register: video_ID_0 + +- name: video_capture - set video_device + set_fact: + video_device: "{{ video_ID_0.stdout_lines[0] }}" + +- 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.j2 + dest: "{{ streaming_working_folder }}/stream_service.sh" + mode: 0755 + +- name: video_capture - streamer - create service file + template: + src: stream_service.service.j2 + dest: /etc/systemd/system/stream_service.service + mode: 0644 + +# daemon reload +- name: video_capture - streamer - daemon reload + systemd: + daemon_reload: yes + + + +... \ No newline at end of file diff --git a/templates/app-service.py.j2 b/templates/app-service.py.j2 new file mode 100644 index 0000000..416adb7 --- /dev/null +++ b/templates/app-service.py.j2 @@ -0,0 +1,86 @@ +import subprocess +import requests +from lxml import html +from flask import Flask, request, jsonify +import json +import os + +app = Flask(__name__) + +def start_service(): + command = "systemctl start {{ service_control_name }}" + try: + # Run the command using subprocess.run() + process = subprocess.Popen(command, shell=True) + + except subprocess.CalledProcessError as e: + return {"Error": e.strip('\n')} + + command = "systemctl status {{ service_control_name }} | grep Active | cut -d ':' -f 2- | cut -b 2-" + try: + # Run the command using subprocess.run() + result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True) + return {"Message": result.stdout.strip('\n')} + + except subprocess.CalledProcessError as e: + return {"Error": e.strip('\n')} + +def stop_service(): + command = "systemctl stop {{ service_control_name }}" + try: + # Run the command using subprocess.run() + process = subprocess.Popen(command, shell=True) + + except subprocess.CalledProcessError as e: + return {"Error": e.strip('\n')} + + command = "systemctl status {{ service_control_name }} | grep Active | cut -d ':' -f 2- | cut -b 2-" + try: + # Run the command using subprocess.run() + result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True) + return {"Message": result.stdout.strip('\n')} + except subprocess.CalledProcessError as e: + return {"Error": e.strip('\n')} + +def service_status(): + command = "systemctl status {{ service_control_name }} | grep Active | cut -d ':' -f 2- | cut -b 2-" + try: + # Run the command using subprocess.run() + result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True) + command = "systemctl status {{ service_control_name }} | grep Active | cut -d ':' -f 2 | cut -d ' ' -f 2" + status = subprocess.run(command, shell=True, check=True, capture_output=True, text=True) + return {"Message": result.stdout.strip('\n'), "Status": status.stdout.strip('\n')} + except subprocess.CalledProcessError as e: + return {"Error": e.strip('\n')} + +@app.route('/start', methods=['GET']) +def start(): + try: + return jsonify(start_service()) + except ValueError as e: + print(e) + return jsonify({'error': e}), 400 + +@app.route('/stop', methods=['GET']) +def stop(): + try: + return jsonify(stop_service()) + except ValueError as e: + print(e) + return jsonify({'error': e}), 400 + +@app.route('/status', methods=['GET']) +def status(): + try: + return jsonify(service_status()) + except ValueError as e: + print(e) + return jsonify({'error': e}), 400 + +@app.route('/test', methods=['GET']) +def test(): + return jsonify({'message': 'Hello there'}) + +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0', port=5000) + \ No newline at end of file diff --git a/templates/docker-compose-php.yaml.j2 b/templates/docker-compose-php.yaml.j2 new file mode 100644 index 0000000..3833e80 --- /dev/null +++ b/templates/docker-compose-php.yaml.j2 @@ -0,0 +1,12 @@ +services: + + {{ container_name }}: + container_name: {{ container_name }} + image: php:8.0-apache + ports: + - {{ container_http_port }}:80 + volumes: + - ./html:/var/www/html/ + {{ extra_volumes }} + network_mode: bridge + restart: always diff --git a/templates/mediamtx.yml.j2 b/templates/mediamtx.yml.j2 new file mode 100644 index 0000000..2ea2117 --- /dev/null +++ b/templates/mediamtx.yml.j2 @@ -0,0 +1,775 @@ +############################################### +# Global settings + +# Settings in this section are applied anywhere. + +############################################### +# Global settings -> General + +# Verbosity of the program; available values are "error", "warn", "info", "debug". +logLevel: info +# Destinations of log messages; available values are "stdout", "file" and "syslog". +logDestinations: [stdout] +# If "file" is in logDestinations, this is the file which will receive the logs. +logFile: mediamtx.log +# If "syslog" is in logDestinations, use prefix for logs. +sysLogPrefix: mediamtx + +# Timeout of read operations. +readTimeout: 10s +# Timeout of write operations. +writeTimeout: 10s +# Size of the queue of outgoing packets. +# A higher value allows to increase throughput, a lower value allows to save RAM. +writeQueueSize: 512 +# Maximum size of outgoing UDP packets. +# This can be decreased to avoid fragmentation on networks with a low UDP MTU. +udpMaxPayloadSize: 1472 + +# Command to run when a client connects to the server. +# This is terminated with SIGINT when a client disconnects from the server. +# The following environment variables are available: +# * MTX_CONN_TYPE: connection type +# * MTX_CONN_ID: connection ID +# * RTSP_PORT: RTSP server port +runOnConnect: +# Restart the command if it exits. +runOnConnectRestart: no +# Command to run when a client disconnects from the server. +# Environment variables are the same of runOnConnect. +runOnDisconnect: + +############################################### +# Global settings -> Authentication + +# Authentication method. Available values are: +# * internal: users are stored in the configuration file +# * http: an external HTTP URL is contacted to perform authentication +# * jwt: an external identity server provides authentication through JWTs +authMethod: internal + +# Internal authentication. +# list of users. +authInternalUsers: + # Default unprivileged user. + # Username. 'any' means any user, including anonymous ones. +- user: any + # Password. Not used in case of 'any' user. + pass: + # IPs or networks allowed to use this user. An empty list means any IP. + ips: [] + # List of permissions. + permissions: + # Available actions are: publish, read, playback, api, metrics, pprof. + - action: publish + # Paths can be set to further restrict access to a specific path. + # An empty path means any path. + # Regular expressions can be used by using a tilde as prefix. + path: + - action: read + path: + - action: playback + path: + + # Default administrator. + # This allows to use API, metrics and PPROF without authentication, + # if the IP is localhost. +- user: any + pass: + ips: ['127.0.0.1', '::1'] + permissions: + - action: api + - action: metrics + - action: pprof + +# HTTP-based authentication. +# URL called to perform authentication. Every time a user wants +# to authenticate, the server calls this URL with the POST method +# and a body containing: +# { +# "user": "user", +# "password": "password", +# "token": "token", +# "ip": "ip", +# "action": "publish|read|playback|api|metrics|pprof", +# "path": "path", +# "protocol": "rtsp|rtmp|hls|webrtc|srt", +# "id": "id", +# "query": "query" +# } +# If the response code is 20x, authentication is accepted, otherwise +# it is discarded. +authHTTPAddress: +# Actions to exclude from HTTP-based authentication. +# Format is the same as the one of user permissions. +authHTTPExclude: +- action: api +- action: metrics +- action: pprof + +# JWT-based authentication. +# Users have to login through an external identity server and obtain a JWT. +# This JWT must contain the claim "mediamtx_permissions" with permissions, +# for instance: +# { +# "mediamtx_permissions": [ +# { +# "action": "publish", +# "path": "somepath" +# } +# ] +# } +# Users are expected to pass the JWT in the Authorization header, password or query parameter. +# This is the JWKS URL that will be used to pull (once) the public key that allows +# to validate JWTs. +authJWTJWKS: +# If the JWKS URL has a self-signed or invalid certificate, +# you can provide the fingerprint of the certificate in order to +# validate it anyway. It can be obtained by running: +# openssl s_client -connect jwt_jwks_domain:443 /dev/null | sed -n '/BEGIN/,/END/p' > server.crt +# openssl x509 -in server.crt -noout -fingerprint -sha256 | cut -d "=" -f2 | tr -d ':' +authJWTJWKSFingerprint: +# name of the claim that contains permissions. +authJWTClaimKey: mediamtx_permissions +# Actions to exclude from JWT-based authentication. +# Format is the same as the one of user permissions. +authJWTExclude: [] +# allow passing the JWT through query parameters of HTTP requests (i.e. ?jwt=JWT). +# This is a security risk. +authJWTInHTTPQuery: true + +############################################### +# Global settings -> Control API + +# Enable controlling the server through the Control API. +api: no +# Address of the Control API listener. +apiAddress: :9997 +# Enable TLS/HTTPS on the Control API server. +apiEncryption: no +# Path to the server key. This is needed only when encryption is yes. +# This can be generated with: +# openssl genrsa -out server.key 2048 +# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 +apiServerKey: server.key +# Path to the server certificate. +apiServerCert: server.crt +# Value of the Access-Control-Allow-Origin header provided in every HTTP response. +apiAllowOrigin: '*' +# List of IPs or CIDRs of proxies placed before the HTTP server. +# If the server receives a request from one of these entries, IP in logs +# will be taken from the X-Forwarded-For header. +apiTrustedProxies: [] + +############################################### +# Global settings -> Metrics + +# Enable Prometheus-compatible metrics. +metrics: no +# Address of the metrics HTTP listener. +metricsAddress: :9998 +# Enable TLS/HTTPS on the Metrics server. +metricsEncryption: no +# Path to the server key. This is needed only when encryption is yes. +# This can be generated with: +# openssl genrsa -out server.key 2048 +# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 +metricsServerKey: server.key +# Path to the server certificate. +metricsServerCert: server.crt +# Value of the Access-Control-Allow-Origin header provided in every HTTP response. +metricsAllowOrigin: '*' +# List of IPs or CIDRs of proxies placed before the HTTP server. +# If the server receives a request from one of these entries, IP in logs +# will be taken from the X-Forwarded-For header. +metricsTrustedProxies: [] + +############################################### +# Global settings -> PPROF + +# Enable pprof-compatible endpoint to monitor performances. +pprof: no +# Address of the pprof listener. +pprofAddress: :9999 +# Enable TLS/HTTPS on the pprof server. +pprofEncryption: no +# Path to the server key. This is needed only when encryption is yes. +# This can be generated with: +# openssl genrsa -out server.key 2048 +# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 +pprofServerKey: server.key +# Path to the server certificate. +pprofServerCert: server.crt +# Value of the Access-Control-Allow-Origin header provided in every HTTP response. +pprofAllowOrigin: '*' +# List of IPs or CIDRs of proxies placed before the HTTP server. +# If the server receives a request from one of these entries, IP in logs +# will be taken from the X-Forwarded-For header. +pprofTrustedProxies: [] + +############################################### +# Global settings -> Playback server + +# Enable downloading recordings from the playback server. +playback: no +# Address of the playback server listener. +playbackAddress: :9996 +# Enable TLS/HTTPS on the playback server. +playbackEncryption: no +# Path to the server key. This is needed only when encryption is yes. +# This can be generated with: +# openssl genrsa -out server.key 2048 +# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 +playbackServerKey: server.key +# Path to the server certificate. +playbackServerCert: server.crt +# Value of the Access-Control-Allow-Origin header provided in every HTTP response. +playbackAllowOrigin: '*' +# List of IPs or CIDRs of proxies placed before the HTTP server. +# If the server receives a request from one of these entries, IP in logs +# will be taken from the X-Forwarded-For header. +playbackTrustedProxies: [] + +############################################### +# Global settings -> RTSP server + +# Enable publishing and reading streams with the RTSP protocol. +rtsp: yes +# List of enabled RTSP transport protocols. +# UDP is the most performant, but doesn't work when there's a NAT/firewall between +# server and clients. +# UDP-multicast allows to save bandwidth when clients are all in the same LAN. +# TCP is the most versatile. +# The handshake is always performed with TCP. +rtspTransports: [udp, multicast, tcp] +# Use secure protocol variants (RTSPS, TLS, SRTP). +# Available values are "no", "strict", "optional". +rtspEncryption: "no" +# Address of the TCP/RTSP listener. This is needed only when encryption is "no" or "optional". +rtspAddress: :8554 +# Address of the TCP/TLS/RTSPS listener. This is needed only when encryption is "strict" or "optional". +rtspsAddress: :8322 +# Address of the UDP/RTP listener. This is needed only when "udp" is in rtspTransports. +rtpAddress: :8000 +# Address of the UDP/RTCP listener. This is needed only when "udp" is in rtspTransports. +rtcpAddress: :8001 +# IP range of all UDP-multicast listeners. This is needed only when "multicast" is in rtspTransports. +multicastIPRange: 224.1.0.0/16 +# Port of all UDP-multicast/RTP listeners. This is needed only when "multicast" is in rtspTransports. +multicastRTPPort: 8002 +# Port of all UDP-multicast/RTCP listeners. This is needed only when "multicast" is in rtspTransports. +multicastRTCPPort: 8003 +# Address of the UDP/SRTP listener. This is needed only when "udp" is in rtspTransports and encryption is enabled. +srtpAddress: :8004 +# Address of the UDP/SRTCP listener. This is needed only when "udp" is in rtspTransports and encryption is enabled. +srtcpAddress: :8005 +# Port of all UDP-multicast/SRTP listeners. This is needed only when "multicast" is in rtspTransports and encryption is enabled. +multicastSRTPPort: 8006 +# Port of all UDP-multicast/SRTCP listeners. This is needed only when "multicast" is in rtspTransports and encryption is enabled. +multicastSRTCPPort: 8007 +# Path to the server key. This is needed only when encryption is "strict" or "optional". +# This can be generated with: +# openssl genrsa -out server.key 2048 +# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 +rtspServerKey: server.key +# Path to the server certificate. This is needed only when encryption is "strict" or "optional". +rtspServerCert: server.crt +# Authentication methods. Available are "basic" and "digest". +# "digest" doesn't provide any additional security and is available for compatibility only. +rtspAuthMethods: [basic] +# Size of the UDP buffer of the RTSP server. +# This can be increased to mitigate packet losses. +# It defaults to the default value of the operating system. +rtspUDPReadBufferSize: 0 + +############################################### +# Global settings -> RTMP server + +# Enable publishing and reading streams with the RTMP protocol. +rtmp: yes +# Address of the RTMP listener. This is needed only when encryption is "no" or "optional". +rtmpAddress: :1935 +# Encrypt connections with TLS (RTMPS). +# Available values are "no", "strict", "optional". +rtmpEncryption: "no" +# Address of the RTMPS listener. This is needed only when encryption is "strict" or "optional". +rtmpsAddress: :1936 +# Path to the server key. This is needed only when encryption is "strict" or "optional". +# This can be generated with: +# openssl genrsa -out server.key 2048 +# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 +rtmpServerKey: server.key +# Path to the server certificate. This is needed only when encryption is "strict" or "optional". +rtmpServerCert: server.crt + +############################################### +# Global settings -> HLS server + +# Enable reading streams with the HLS protocol. +hls: yes +# Address of the HLS listener. +hlsAddress: :8888 +# Enable TLS/HTTPS on the HLS server. +# This is required for Low-Latency HLS. +hlsEncryption: no +# Path to the server key. This is needed only when encryption is yes. +# This can be generated with: +# openssl genrsa -out server.key 2048 +# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 +hlsServerKey: server.key +# Path to the server certificate. +hlsServerCert: server.crt +# Value of the Access-Control-Allow-Origin header provided in every HTTP response. +# This allows to play the HLS stream from an external website. +hlsAllowOrigin: '*' +# List of IPs or CIDRs of proxies placed before the HLS server. +# If the server receives a request from one of these entries, IP in logs +# will be taken from the X-Forwarded-For header. +hlsTrustedProxies: [] +# By default, HLS is generated only when requested by a user. +# This option allows to generate it always, avoiding the delay between request and generation. +hlsAlwaysRemux: no +# Variant of the HLS protocol to use. Available options are: +# * mpegts - uses MPEG-TS segments, for maximum compatibility. +# * fmp4 - uses fragmented MP4 segments, more efficient. +# * lowLatency - uses Low-Latency HLS. +hlsVariant: lowLatency +# Number of HLS segments to keep on the server. +# Segments allow to seek through the stream. +# Their number doesn't influence latency. +hlsSegmentCount: 7 +# Minimum duration of each segment. +# A player usually puts 3 segments in a buffer before reproducing the stream. +# The final segment duration is also influenced by the interval between IDR frames, +# since the server changes the duration in order to include at least one IDR frame +# in each segment. +hlsSegmentDuration: 1s +# Minimum duration of each part. +# A player usually puts 3 parts in a buffer before reproducing the stream. +# Parts are used in Low-Latency HLS in place of segments. +# Part duration is influenced by the distance between video/audio samples +# and is adjusted in order to produce segments with a similar duration. +hlsPartDuration: 200ms +# Maximum size of each segment. +# This prevents RAM exhaustion. +hlsSegmentMaxSize: 50M +# Directory in which to save segments, instead of keeping them in the RAM. +# This decreases performance, since reading from disk is less performant than +# reading from RAM, but allows to save RAM. +hlsDirectory: '' +# The muxer will be closed when there are no +# reader requests and this amount of time has passed. +hlsMuxerCloseAfter: 60s + +############################################### +# Global settings -> WebRTC server + +# Enable publishing and reading streams with the WebRTC protocol. +webrtc: yes +# Address of the WebRTC HTTP listener. +webrtcAddress: :8889 +# Enable TLS/HTTPS on the WebRTC server. +webrtcEncryption: no +# Path to the server key. +# This can be generated with: +# openssl genrsa -out server.key 2048 +# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 +webrtcServerKey: server.key +# Path to the server certificate. +webrtcServerCert: server.crt +# Value of the Access-Control-Allow-Origin header provided in every HTTP response. +# This allows to play the WebRTC stream from an external website. +webrtcAllowOrigin: '*' +# List of IPs or CIDRs of proxies placed before the WebRTC server. +# If the server receives a request from one of these entries, IP in logs +# will be taken from the X-Forwarded-For header. +webrtcTrustedProxies: [] +# Address of a local UDP listener that will receive connections. +# Use a blank string to disable. +webrtcLocalUDPAddress: :8189 +# Address of a local TCP listener that will receive connections. +# This is disabled by default since TCP is less efficient than UDP and +# introduces a progressive delay when network is congested. +webrtcLocalTCPAddress: '' +# WebRTC clients need to know the IP of the server. +# Gather IPs from interfaces and send them to clients. +webrtcIPsFromInterfaces: yes +# List of interfaces whose IPs will be sent to clients. +# An empty value means to use all available interfaces. +webrtcIPsFromInterfacesList: [] +# List of additional hosts or IPs to send to clients. +webrtcAdditionalHosts: [] +# ICE servers. Needed only when local listeners can't be reached by clients. +# STUN servers allows to obtain and share the public IP of the server. +# TURN/TURNS servers forces all traffic through them. +webrtcICEServers2: [] + # - url: stun:stun.l.google.com:19302 + # if user is "AUTH_SECRET", then authentication is secret based. + # the secret must be inserted into the password field. + # username: '' + # password: '' + # clientOnly: false +# Time to wait for the WebRTC handshake to complete. +webrtcHandshakeTimeout: 10s +# Maximum time to gather video tracks. +webrtcTrackGatherTimeout: 2s +# The maximum time to gather STUN candidates. +webrtcSTUNGatherTimeout: 5s + +############################################### +# Global settings -> SRT server + +# Enable publishing and reading streams with the SRT protocol. +srt: yes +# Address of the SRT listener. +srtAddress: :8890 + +############################################### +# Default path settings + +# Settings in "pathDefaults" are applied anywhere, +# unless they are overridden in "paths". +pathDefaults: + + ############################################### + # Default path settings -> General + + # Source of the stream. This can be: + # * publisher -> the stream is provided by a RTSP, RTMP, WebRTC or SRT client + # * rtsp://existing-url -> the stream is pulled from another RTSP server / camera + # * rtsps://existing-url -> the stream is pulled from another RTSP server / camera with RTSPS + # * rtmp://existing-url -> the stream is pulled from another RTMP server / camera + # * rtmps://existing-url -> the stream is pulled from another RTMP server / camera with RTMPS + # * http://existing-url/stream.m3u8 -> the stream is pulled from another HLS server / camera + # * https://existing-url/stream.m3u8 -> the stream is pulled from another HLS server / camera with HTTPS + # * udp+mpegts://ip:port -> the stream is pulled from MPEG-TS over UDP, by listening on the specified address + # * unix+mpegts://socket -> the stream is pulled from MPEG-TS over Unix socket, by using the socket + # * udp+rtp://ip:port -> the stream is pulled from RTP over UDP, by listening on the specified address + # * unix+rtp://socket -> the stream is pulled from RTP over Unix socket, by using the socket + # * srt://existing-url -> the stream is pulled from another SRT server / camera + # * whep://existing-url -> the stream is pulled from another WebRTC server / camera + # * wheps://existing-url -> the stream is pulled from another WebRTC server / camera with HTTPS + # * redirect -> the stream is provided by another path or server + # * rpiCamera -> the stream is provided by a Raspberry Pi Camera + # The following variables can be used in the source string: + # * $MTX_QUERY: query parameters (passed by first reader) + # * $G1, $G2, ...: regular expression groups, if path name is + # a regular expression. + source: publisher + # If the source is a URL, and the source certificate is self-signed + # or invalid, you can provide the fingerprint of the certificate in order to + # validate it anyway. It can be obtained by running: + # openssl s_client -connect source_ip:source_port /dev/null | sed -n '/BEGIN/,/END/p' > server.crt + # openssl x509 -in server.crt -noout -fingerprint -sha256 | cut -d "=" -f2 | tr -d ':' + sourceFingerprint: + # If the source is a URL, it will be pulled only when at least + # one reader is connected, saving bandwidth. + sourceOnDemand: no + # If sourceOnDemand is "yes", readers will be put on hold until the source is + # ready or until this amount of time has passed. + sourceOnDemandStartTimeout: 10s + # If sourceOnDemand is "yes", the source will be closed when there are no + # readers connected and this amount of time has passed. + sourceOnDemandCloseAfter: 10s + # Maximum number of readers. Zero means no limit. + maxReaders: 0 + # SRT encryption passphrase required to read from this path. + srtReadPassphrase: + # If the stream is not available, redirect readers to this path. + # It can be can be a relative path (i.e. /otherstream) or an absolute RTSP URL. + fallback: + # Route original absolute timestamps of RTSP and WebRTC frames, instead of replacing them. + useAbsoluteTimestamp: false + + ############################################### + # Default path settings -> Record + + # Record streams to disk. + record: yes + + # Path of recording segments. + # Extension is added automatically. + # Available variables are %path (path name), %Y %m %d (year, month, day), + # %H %M %S (hours, minutes, seconds), %f (microseconds), %z (time zone), %s (unix epoch). + recordPath: {{ recording_capture_folder }}/%path/%Y-%m-%d_%H-%M-%S-%f + + # Available formats are "fmp4" (fragmented MP4) and "mpegts" (MPEG-TS). + recordFormat: fmp4 + + # fMP4 segments are concatenation of small MP4 files (parts), each with this duration. + # MPEG-TS segments are concatenation of 188-bytes packets, flushed to disk with this period. + # When a system failure occurs, the last part gets lost. + # Therefore, the part duration is equal to the RPO (recovery point objective). + recordPartDuration: 1s + + # This prevents RAM exhaustion. + recordMaxPartSize: 5000M + + # Minimum duration of each segment. + recordSegmentDuration: 3600s + + # Delete segments after this timespan. + # Set to 0s to disable automatic deletion. + recordDeleteAfter: 0s + + ############################################### + # Default path settings -> Publisher source (when source is "publisher") + + # Allow another client to disconnect the current publisher and publish in its place. + overridePublisher: yes + # SRT encryption passphrase required to publish to this path. + srtPublishPassphrase: + + ############################################### + # Default path settings -> RTSP source (when source is a RTSP or a RTSPS URL) + + # Transport protocol used to pull the stream. available values are "automatic", "udp", "multicast", "tcp". + rtspTransport: automatic + # Support sources that don't provide server ports or use random server ports. This is a security issue + # and must be used only when interacting with sources that require it. + rtspAnyPort: no + # Range header to send to the source, in order to start streaming from the specified offset. + # available values: + # * clock: Absolute time + # * npt: Normal Play Time + # * smpte: SMPTE timestamps relative to the start of the recording + rtspRangeType: + # Available values: + # * clock: UTC ISO 8601 combined date and time string, e.g. 20230812T120000Z + # * npt: duration such as "300ms", "1.5m" or "2h45m", valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h" + # * smpte: duration such as "300ms", "1.5m" or "2h45m", valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h" + rtspRangeStart: + # Size of the UDP buffer of the RTSP client. + # This can be increased to mitigate packet losses. + # It defaults to the default value of the operating system. + rtspUDPReadBufferSize: 0 + + ############################################### + # Default path settings -> MPEG-TS source (when source is MPEG-TS) + + # Size of the UDP buffer of the MPEG-TS client. + # This can be increased to mitigate packet losses. + # It defaults to the default value of the operating system. + mpegtsUDPReadBufferSize: 0 + + ############################################### + # Default path settings -> RTP source (when source is RTP) + + # session description protocol (SDP) of the RTP stream. + rtpSDP: + # Size of the UDP buffer of the RTP client. + # This can be increased to mitigate packet losses. + # It defaults to the default value of the operating system. + rtpUDPReadBufferSize: 0 + + ############################################### + # Default path settings -> Redirect source (when source is "redirect") + + # path which clients will be redirected to. + # It can be can be a relative path (i.e. /otherstream) or an absolute RTSP URL. + sourceRedirect: + + ############################################### + # Default path settings -> Raspberry Pi Camera source (when source is "rpiCamera") + + # ID of the camera. + rpiCameraCamID: 0 + # Whether this is a secondary stream. + rpiCameraSecondary: false + # Width of frames. + rpiCameraWidth: 1920 + # Height of frames. + rpiCameraHeight: 1080 + # Flip horizontally. + rpiCameraHFlip: false + # Flip vertically. + rpiCameraVFlip: false + # Brightness [-1, 1]. + rpiCameraBrightness: 0 + # Contrast [0, 16]. + rpiCameraContrast: 1 + # Saturation [0, 16]. + rpiCameraSaturation: 1 + # Sharpness [0, 16]. + rpiCameraSharpness: 1 + # Exposure mode. + # values: normal, short, long, custom. + rpiCameraExposure: normal + # Auto-white-balance mode. + # (auto, incandescent, tungsten, fluorescent, indoor, daylight, cloudy or custom). + rpiCameraAWB: auto + # Auto-white-balance fixed gains. This can be used in place of rpiCameraAWB. + # format: [red,blue]. + rpiCameraAWBGains: [0, 0] + # Denoise operating mode (off, cdn_off, cdn_fast, cdn_hq). + rpiCameraDenoise: "off" + # Fixed shutter speed, in microseconds. + rpiCameraShutter: 0 + # Metering mode of the AEC/AGC algorithm (centre, spot, matrix or custom). + rpiCameraMetering: centre + # Fixed gain. + rpiCameraGain: 0 + # EV compensation of the image in range [-10, 10]. + rpiCameraEV: 0 + # Region of interest, in format x,y,width,height (all normalized between 0 and 1). + rpiCameraROI: + # Whether to enable HDR on Raspberry Camera 3. + rpiCameraHDR: false + # Tuning file. + rpiCameraTuningFile: + # Sensor mode, in format [width]:[height]:[bit-depth]:[packing] + # bit-depth and packing are optional. + rpiCameraMode: + # frames per second. + rpiCameraFPS: 30 + # Autofocus mode (auto, manual or continuous). + rpiCameraAfMode: continuous + # Autofocus range (normal, macro or full). + rpiCameraAfRange: normal + # Autofocus speed (normal or fast). + rpiCameraAfSpeed: normal + # Lens position (for manual autofocus only), will be set to focus to a specific distance + # calculated by the following formula: d = 1 / value + # Examples: 0 moves the lens to infinity. + # 0.5 moves the lens to focus on objects 2m away. + # 2 moves the lens to focus on objects 50cm away. + rpiCameraLensPosition: 0.0 + # Autofocus window, in the form x,y,width,height where the coordinates + # are given as a proportion of the entire image. + rpiCameraAfWindow: + # Manual flicker correction period, in microseconds. + rpiCameraFlickerPeriod: 0 + # Enables printing text on each frame. + rpiCameraTextOverlayEnable: false + # Text that is printed on each frame. + # format is the one of the strftime() function. + rpiCameraTextOverlay: '%Y-%m-%d %H:%M:%S - MediaMTX' + # Codec (auto, hardwareH264, softwareH264 or mjpeg). + # When is "auto" and stream is primary, it defaults to hardwareH264 (if available) or softwareH264. + # When is "auto" and stream is secondary, it defaults to mjpeg. + rpiCameraCodec: auto + # Period between IDR frames (when codec is hardwareH264 or softwareH264). + rpiCameraIDRPeriod: 60 + # Bitrate (when codec is hardwareH264 or softwareH264). + rpiCameraBitrate: 5000000 + # Hardware H264 profile (baseline, main or high) (when codec is hardwareH264). + rpiCameraHardwareH264Profile: main + # Hardware H264 level (4.0, 4.1 or 4.2) (when codec is hardwareH264). + rpiCameraHardwareH264Level: '4.1' + # Software H264 profile (baseline, main or high) (when codec is softwareH264). + rpiCameraSoftwareH264Profile: baseline + # Software H264 level (4.0, 4.1 or 4.2) (when codec is softwareH264). + rpiCameraSoftwareH264Level: '4.1' + # M-JPEG JPEG quality (when codec is mjpeg). + rpiCameraMJPEGQuality: 60 + + ############################################### + # Default path settings -> Hooks + + # Command to run when this path is initialized. + # This can be used to publish a stream when the server is launched. + # This is terminated with SIGINT when the program closes. + # The following environment variables are available: + # * MTX_PATH: path name + # * RTSP_PORT: RTSP server port + # * G1, G2, ...: regular expression groups, if path name is + # a regular expression. + runOnInit: + # Restart the command if it exits. + runOnInitRestart: no + + # Command to run when this path is requested by a reader + # and no one is publishing to this path yet. + # This can be used to publish a stream on demand. + # This is terminated with SIGINT when there are no readers anymore. + # The following environment variables are available: + # * MTX_PATH: path name + # * MTX_QUERY: query parameters (passed by first reader) + # * RTSP_PORT: RTSP server port + # * G1, G2, ...: regular expression groups, if path name is + # a regular expression. + runOnDemand: + # Restart the command if it exits. + runOnDemandRestart: no + # Readers will be put on hold until the runOnDemand command starts publishing + # or until this amount of time has passed. + runOnDemandStartTimeout: 10s + # The command will be closed when there are no + # readers connected and this amount of time has passed. + runOnDemandCloseAfter: 10s + # Command to run when there are no readers anymore. + # Environment variables are the same of runOnDemand. + runOnUnDemand: + + # Command to run when the stream is ready to be read, whenever it is + # published by a client or pulled from a server / camera. + # This is terminated with SIGINT when the stream is not ready anymore. + # The following environment variables are available: + # * MTX_PATH: path name + # * MTX_QUERY: query parameters (passed by publisher) + # * MTX_SOURCE_TYPE: source type + # * MTX_SOURCE_ID: source ID + # * RTSP_PORT: RTSP server port + # * G1, G2, ...: regular expression groups, if path name is + # a regular expression. + runOnReady: + # Restart the command if it exits. + runOnReadyRestart: no + # Command to run when the stream is not available anymore. + # Environment variables are the same of runOnReady. + runOnNotReady: + + # Command to run when a client starts reading. + # This is terminated with SIGINT when a client stops reading. + # The following environment variables are available: + # * MTX_PATH: path name + # * MTX_QUERY: query parameters (passed by reader) + # * MTX_READER_TYPE: reader type + # * MTX_READER_ID: reader ID + # * RTSP_PORT: RTSP server port + # * G1, G2, ...: regular expression groups, if path name is + # a regular expression. + runOnRead: + # Restart the command if it exits. + runOnReadRestart: no + # Command to run when a client stops reading. + # Environment variables are the same of runOnRead. + runOnUnread: + + # Command to run when a recording segment is created. + # The following environment variables are available: + # * MTX_PATH: path name + # * MTX_SEGMENT_PATH: segment file path + # * RTSP_PORT: RTSP server port + # * G1, G2, ...: regular expression groups, if path name is + # a regular expression. + runOnRecordSegmentCreate: + + # Command to run when a recording segment is complete. + # The following environment variables are available: + # * MTX_PATH: path name + # * MTX_SEGMENT_PATH: segment file path + # * MTX_SEGMENT_DURATION: segment duration + # * RTSP_PORT: RTSP server port + # * G1, G2, ...: regular expression groups, if path name is + # a regular expression. + runOnRecordSegmentComplete: + +############################################### +# Path settings + +# Settings in "paths" are applied to specific paths, and the map key +# is the name of the path. +# Any setting in "pathDefaults" can be overridden here. +# It's possible to use regular expressions by using a tilde as prefix, +# for example "~^(test1|test2)$" will match both "test1" and "test2", +# for example "~^prefix" will match all paths that start with "prefix". +paths: + # example: + # my_camera: + # source: rtsp://my_camera + + # Settings under path "all_others" are applied to all paths that + # do not match another entry. + all_others: + diff --git a/templates/mediamtx_service.service.j2 b/templates/mediamtx_service.service.j2 new file mode 100644 index 0000000..2cf63d4 --- /dev/null +++ b/templates/mediamtx_service.service.j2 @@ -0,0 +1,14 @@ +[Unit] +Description=MediaMTX Service +After=network.target + +[Service] +Type=simple +WorkingDirectory={{ mediamtx_working_folder }} +ExecStart={{ mediamtx_working_folder }}/mediamtx +Restart=always +User=root +Group=root + +[Install] +WantedBy=multi-user.target diff --git a/templates/service_control.service.j2 b/templates/service_control.service.j2 new file mode 100644 index 0000000..237e7a4 --- /dev/null +++ b/templates/service_control.service.j2 @@ -0,0 +1,15 @@ + +[Unit] +Description={{ service_name }} Service Control API +After=network.target + +[Service] +User=root +Group=root +WorkingDirectory={{ service_control_folder }} +ExecStartPre=/bin/sleep 5 +ExecStart={{ service_control_folder }}/venv/bin/python {{ service_control_folder }}/app.py +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/templates/stream_service.service.j2 b/templates/stream_service.service.j2 new file mode 100644 index 0000000..53b3aab --- /dev/null +++ b/templates/stream_service.service.j2 @@ -0,0 +1,14 @@ +[Unit] +Description=VCR Streaming Service +After=network.target + +[Service] +Type=simple +WorkingDirectory={{ streaming_working_folder }} +ExecStart={{ streaming_working_folder }}/stream_service.sh +Restart=always +User=root +Group=root + +[Install] +WantedBy=multi-user.target diff --git a/templates/stream_service.sh.j2 b/templates/stream_service.sh.j2 new file mode 100644 index 0000000..dd75aa9 --- /dev/null +++ b/templates/stream_service.sh.j2 @@ -0,0 +1,12 @@ +#!/bin/bash + +ffmpeg -hwaccel cuda -hwaccel_output_format cuda \ + -f alsa -ac 2 -i {{ audio_device }} -thread_queue_size 64 \ + -f v4l2 -framerate 30 -video_size 640x480 -input_format yuyv422 -i {{ video_device }} \ + -c:v libx264 -preset ultrafast -tune zerolatency \ + -vf "format=yuv420p" -g 60 -c:a aac -b:a 128k -ar 44100 \ + -f flv rtmp://0.0.0.0:1935/stream + + + +