├── VERSION ├── CNAME ├── _config.yml ├── stt └── .readme ├── assets └── splash │ ├── idle_splash.jpg │ └── connecting_splash.jpg ├── tools ├── package.json ├── check_gst_version.sh ├── serve_hls.py ├── check_splitmux_pads.py ├── check_hls_status.py ├── check_splitmux_signals.py ├── vdoninja_simple_websocket_server.js ├── play_hls.html └── combine_recordings.py ├── installers ├── nvidia_jetson │ ├── libgstnvidia.zip │ ├── raspininja.service │ ├── toolchain_update.sh │ ├── theta_z1_install.sh │ ├── INSTALL.md │ ├── README.md │ ├── setup_autostart.sh │ └── installer.sh ├── raspberry_pi │ ├── firmware_update.sh │ ├── Cargo.toml │ ├── raspininja.service │ ├── setup_c779.sh │ ├── simpleinstall.md │ └── installer.sh ├── ubuntu │ ├── README.md │ └── installer.sh ├── orangepi │ ├── raspininja.service │ └── README.md ├── README.md ├── wsl │ └── README.md └── mac │ └── readme.md ├── test_config_minimal.json ├── .github ├── workflows │ ├── toc.yml │ ├── enhance-commits.yml │ ├── auto-release.yml │ ├── test-local.yml │ ├── test.yml │ └── manual-release.yml ├── FUNDING.yml ├── RELEASE_PROCESS.md └── scripts │ └── auto-release.js ├── templates ├── record.service.tpl ├── index.html └── recording.html ├── config.json ├── test_config.json ├── setup.ninja_record.systemd.sh ├── .actrc ├── QUICK_START.md ├── convert_to_numpy_examples ├── basic_recv.py ├── readme.md └── adv_cv2_jpeg.py ├── dev_notes ├── HLS_SEGMENT_FIX.md ├── JETSON_HLS_FIX_SUMMARY.md └── HLS_UNIVERSAL_FIX.md ├── ndi ├── fix_ndi_network.sh ├── readme.md └── install_ndi.sh ├── tests └── test_multiple_webrtc_connections.py ├── .gitignore └── record.py /VERSION: -------------------------------------------------------------------------------- 1 | 9.0.0 -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | raspberry.ninja -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /stt/.readme: -------------------------------------------------------------------------------- 1 | Transcriptions will be saved here 2 | -------------------------------------------------------------------------------- /assets/splash/idle_splash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steveseguin/raspberry_ninja/HEAD/assets/splash/idle_splash.jpg -------------------------------------------------------------------------------- /assets/splash/connecting_splash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steveseguin/raspberry_ninja/HEAD/assets/splash/connecting_splash.jpg -------------------------------------------------------------------------------- /tools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "cors": "^2.8.5", 4 | "express": "^5.1.0", 5 | "ws": "^8.18.3" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /installers/nvidia_jetson/libgstnvidia.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steveseguin/raspberry_ninja/HEAD/installers/nvidia_jetson/libgstnvidia.zip -------------------------------------------------------------------------------- /test_config_minimal.json: -------------------------------------------------------------------------------- 1 | { 2 | "stream_id": "config_test_123", 3 | "video_source": "test", 4 | "audio_enabled": false, 5 | "width": 640, 6 | "height": 480, 7 | "framerate": 15, 8 | "bitrate": 500 9 | } -------------------------------------------------------------------------------- /.github/workflows/toc.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | name: TOC Generator 3 | jobs: 4 | generateTOC: 5 | name: TOC Generator 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: technote-space/toc-generator@v4 9 | with: 10 | MAX_HEADER_LEVEL: 4 11 | -------------------------------------------------------------------------------- /templates/record.service.tpl: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Record Vdo Ninja STT 3 | After=network.target 4 | 5 | [Service] 6 | User=_USER_ 7 | Group=_USER_ 8 | WorkingDirectory=_MY_PATH_ 9 | ExecStart=/usr/bin/python3 _MY_PATH_/record.py --host 0.0.0.0 --port 9000 10 | Restart=always 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "stream_id": "", 3 | "room": "", 4 | "server": "wss://wss.vdo.ninja:443", 5 | "bitrate": 2000, 6 | "width": 1280, 7 | "height": 720, 8 | "framerate": 30, 9 | "video_source": "test", 10 | "custom_video_pipeline": "", 11 | "audio_enabled": true, 12 | "platform": "WSL", 13 | "auto_start": false 14 | } 15 | -------------------------------------------------------------------------------- /installers/raspberry_pi/firmware_update.sh: -------------------------------------------------------------------------------- 1 | # check if newest version is needed 2 | sudo rpi-eeprom-update 3 | 4 | # If not up to date, then we can update 5 | sudo raspi-config 6 | # Advanced Options -> Bootloader Version -> Latest 7 | # Reboot when prompted 8 | 9 | ## For non RPI4 systems, you can try instead, or also. 10 | sudo apt update 11 | sudo apt full-upgrade 12 | sudo reboot 13 | -------------------------------------------------------------------------------- /test_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "stream_id": "test_stream_123", 3 | "room": "test_room", 4 | "server": "wss://wss.vdo.ninja:443", 5 | "bitrate": 2000, 6 | "width": 1280, 7 | "height": 720, 8 | "framerate": 30, 9 | "video_source": "test", 10 | "custom_video_pipeline": "", 11 | "audio_enabled": false, 12 | "platform": "WSL", 13 | "auto_start": false 14 | } -------------------------------------------------------------------------------- /tools/check_gst_version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "GStreamer version check:" 3 | echo "=======================" 4 | gst-launch-1.0 --version 5 | echo "" 6 | echo "Mpegtsmux element details:" 7 | gst-inspect-1.0 mpegtsmux | grep -E "Version|Rank|Long-name" 8 | echo "" 9 | echo "Check if running on ARM:" 10 | uname -m 11 | echo "" 12 | echo "Check GStreamer plugin versions:" 13 | gst-inspect-1.0 --version -------------------------------------------------------------------------------- /installers/ubuntu/README.md: -------------------------------------------------------------------------------- 1 | This is a simple installer for Ubuntu 22 or new releases. 2 | 3 | There is no building or compiling, so you can deploy within a minute. 4 | 5 | Non-free feeatures or cutting edge gstreamer fixes may be missing, but publish.py should still work if you are running a recent version of Ubuntu with this installer. 6 | 7 | Hardware encoding/decoding support may be missing, but if you are running this on a desktop/laptop, that shouldn't be an issue. 8 | -------------------------------------------------------------------------------- /installers/nvidia_jetson/raspininja.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Raspberry Ninja as a system service 3 | After=network-online.target 4 | Requires=network-online.target 5 | 6 | [Service] 7 | User=vdo 8 | Group=vdo 9 | Type=idle 10 | ExecStartPre=/bin/sleep 5 11 | Restart=always 12 | RestartSec=5s 13 | Environment=XDG_RUNTIME_DIR=/run/user/1000 14 | ExecStart=/usr/bin/python3 /home/vdo/raspberry_ninja/publish.py --test 15 | 16 | [Install] 17 | WantedBy=multi-user.target 18 | -------------------------------------------------------------------------------- /setup.ninja_record.systemd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | [ $(id -u) -eq 0 ] && echo "LANCEMENT root INTERDIT (use sudo user). " && exit 1 4 | cat templates/record.service.tpl | sed "s~_USER_~$USER~g" | sed "s~_MY_PATH_~$(pwd)~" > /tmp/ninja_record.service 5 | 6 | cat /tmp/ninja_record.service 7 | sudo cp /tmp/ninja_record.service /etc/systemd/system/ninja_record.service 8 | 9 | sudo systemctl daemon-reload 10 | sudo systemctl enable ninja_record 11 | sudo systemctl restart ninja_record 12 | -------------------------------------------------------------------------------- /installers/nvidia_jetson/toolchain_update.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # Lightweight entrypoint for updating the Jetson media toolchain without the legacy 5 | # distro-upgrade steps. This simply forwards to installer.sh with the relevant 6 | # environment defaults applied so the build flow stays in one place. 7 | 8 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 9 | export INCLUDE_DISTRO_UPGRADE=${INCLUDE_DISTRO_UPGRADE:-0} 10 | exec "${SCRIPT_DIR}/installer.sh" "$@" 11 | -------------------------------------------------------------------------------- /.actrc: -------------------------------------------------------------------------------- 1 | # Default configuration for act 2 | # This makes act run faster by using smaller images 3 | 4 | # Use medium images by default (smaller than default large images) 5 | --platform ubuntu-latest=catthehacker/ubuntu:act-latest 6 | --platform ubuntu-22.04=catthehacker/ubuntu:act-22.04 7 | --platform ubuntu-20.04=catthehacker/ubuntu:act-20.04 8 | 9 | # Don't pull images if they exist 10 | --pull=false 11 | 12 | # Use host network (faster) 13 | --network host 14 | 15 | # Reuse containers between runs (faster) 16 | --reuse 17 | 18 | # Bind working directory 19 | --bind 20 | 21 | # Set default event (push) 22 | --eventpath .github/workflows/test.yml -------------------------------------------------------------------------------- /tools/serve_hls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import http.server 3 | import socketserver 4 | import os 5 | 6 | class CORSHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): 7 | def end_headers(self): 8 | self.send_header('Access-Control-Allow-Origin', '*') 9 | self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS') 10 | self.send_header('Access-Control-Allow-Headers', 'Content-Type') 11 | super().end_headers() 12 | 13 | PORT = 8089 14 | os.chdir('/mnt/c/Users/steve/Code/raspberry_ninja') 15 | 16 | with socketserver.TCPServer(("", PORT), CORSHTTPRequestHandler) as httpd: 17 | print(f"Serving HLS files at http://localhost:{PORT}") 18 | httpd.serve_forever() -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [steveseguin] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /QUICK_START.md: -------------------------------------------------------------------------------- 1 | # 🚀 Raspberry Ninja Quick Start 2 | 3 | ## 1. Install (30 seconds) 4 | 5 | ```bash 6 | curl -sSL https://raw.githubusercontent.com/steveseguin/raspberry_ninja/main/install.sh | bash 7 | ``` 8 | 9 | Choose option 1 for basic installation. 10 | 11 | ## 2. Test (10 seconds) 12 | 13 | ```bash 14 | python3 publish.py --test 15 | ``` 16 | 17 | You should see a test pattern with bouncing ball. 18 | 19 | ## 3. Stream (5 seconds) 20 | 21 | ```bash 22 | python3 publish.py 23 | ``` 24 | 25 | Note the stream ID shown (like `7654321`). 26 | 27 | ## 4. View 28 | 29 | Open in any browser: 30 | ``` 31 | https://vdo.ninja/?view=7654321 32 | ``` 33 | 34 | (Replace `7654321` with your stream ID) 35 | 36 | --- 37 | 38 | **That's it!** You're streaming. 39 | 40 | For more options: `python3 publish.py --help` -------------------------------------------------------------------------------- /installers/orangepi/raspininja.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Orange Ninja as a system service 3 | After=network-online.target sound.target 4 | Wants=sound.target 5 | Requires=network-online.target 6 | 7 | [Service] 8 | # Run as 'orangepi' user and group, ensure this user has the necessary permissions 9 | User=orangepi 10 | Group=orangepi 11 | 12 | # Using 'simple' type for immediate start-up, consider if 'idle' is necessary 13 | Type=simple 14 | 15 | # Adjusting the environment for Orange Pi, ensure these variables are correctly set 16 | Environment="XDG_RUNTIME_DIR=/run/user/1000" 17 | 18 | # Sleep to ensure all dependencies are up, consider an alternative way to check camera availability if needed 19 | ExecStartPre=/bin/sleep 5 20 | 21 | # Main command to start the service, adjust path and options as necessary, such as change the streamID 22 | ExecStart=/usr/bin/python3 /home/orangepi/raspberry_ninja/publish.py --streamid orangepi123 23 | 24 | # Restart policy 25 | Restart=always 26 | RestartSec=5s 27 | 28 | [Install] 29 | WantedBy=multi-user.target 30 | -------------------------------------------------------------------------------- /installers/raspberry_pi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | members = [ 5 | "version-helper", 6 | "audio/spotify", 7 | "mux/mp4", 8 | "net/hlssink3", 9 | "net/ndi", 10 | "net/rtp", 11 | "net/webrtchttp", 12 | 13 | "utils/fallbackswitch", 14 | "utils/livesync", 15 | "utils/togglerecord", 16 | "utils/uriplaylistbin", 17 | 18 | "video/cdg", 19 | "video/closedcaption", 20 | "video/dav1d", 21 | "video/ffv1", 22 | "video/gif", 23 | "video/gtk4", 24 | "video/hsv", 25 | "video/png", 26 | "video/rav1e", 27 | "video/videofx", 28 | "video/webp", 29 | ] 30 | # Only plugins without external dependencies 31 | default-members = [ 32 | "version-helper", 33 | 34 | "mux/mp4", 35 | 36 | "net/hlssink3", 37 | "net/rtp", 38 | "net/webrtchttp", 39 | "net/ndi", 40 | 41 | "utils/fallbackswitch", 42 | "utils/livesync", 43 | "utils/togglerecord", 44 | "utils/uriplaylistbin", 45 | 46 | "video/cdg", 47 | "video/ffv1", 48 | "video/gif", 49 | "video/hsv", 50 | "video/png", 51 | "video/rav1e", 52 | ] 53 | 54 | [profile.release] 55 | lto = true 56 | opt-level = 3 57 | debug = true 58 | panic = 'unwind' 59 | 60 | [profile.dev] 61 | opt-level = 1 62 | -------------------------------------------------------------------------------- /installers/nvidia_jetson/theta_z1_install.sh: -------------------------------------------------------------------------------- 1 | # This will install the driver for the Theta Z1 USB camera with 4K 360 h264 output 2 | ## to use run: 3 | ## sudo chmod +x theta_z1_install.sh 4 | ## ./theta_z1_install.sh 5 | 6 | ## note: One user said they had to run the installer twice before it all worked? 7 | 8 | # Install the Z1 drivers; not really UVC compatible tho 9 | cd ~ 10 | git clone https://github.com/ricohapi/libuvc-theta.git 11 | sudo apt install libjpeg-dev 12 | cd libuvc-theta 13 | mkdir build 14 | cd build 15 | cmake .. 16 | make 17 | sudo make install 18 | 19 | # Create a gstreamer plugin for the Z1, to bypass need for uvc 20 | cd ~ 21 | git clone https://github.com/steveseguin/gst_theta_uvc 22 | cd gst_theta_uvc 23 | cd thetauvc 24 | make 25 | 26 | # Copy the plugin to the gstreamer plugin folder so we can use it 27 | sudo cp gstt*so /usr/local/lib/aarch64-linux-gnu/gstreamer-1.0/ 28 | # confirm its installed 29 | gst-inspect-1.0 | grep "theta" 30 | 31 | # run raspberry_ninja with the z1 enabled 32 | cd ~ 33 | cd raspberry_ninja 34 | # Transcodes the inbound compressed stream. Recommended 35 | python3 publish.py --z1 36 | 37 | ## or if crazy, use the below option for direct-publish, which uses like 150-mbps upload by default. Only practical over a wired LAN I'd say. 38 | # python3 publish.py --z1passthru 39 | 40 | 41 | -------------------------------------------------------------------------------- /installers/raspberry_pi/raspininja.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Raspberry Ninja as a system service 3 | After=network-online.target sound.target 4 | Wants=sound.target 5 | Requires=network-online.target 6 | 7 | [Service] 8 | # Run as 'vdo' user and group, ensure this user has the necessary permissions 9 | # You may need to change this if you are using a different login name 10 | User=vdo 11 | Group=vdo 12 | 13 | # Using 'simple' type for immediate start-up, consider if 'idle' is necessary 14 | Type=simple 15 | 16 | # Ensuring the environment has necessary variables for audio, adjust as needed 17 | Environment="XDG_RUNTIME_DIR=/run/user/1000" 18 | 19 | # Disabling the following for now, but try it if there's problems I guess 20 | # Environment="PULSE_SERVER=/run/user/1000/pulse/native" 21 | 22 | # Sleep to ensure all dependencies are up, and check camera availability 23 | ExecStartPre=/bin/sleep 5 24 | ExecStartPre=/usr/bin/vcgencmd get_camera 25 | 26 | # Main command to start the service, adjust path and options as necessary 27 | # Be sure to change the stream ID and you may need to change /vdo/ to /pi/ or such as needed 28 | ExecStart=/usr/bin/python3 /home/vdo/raspberry_ninja/publish.py --streamID raspininja123 29 | 30 | # Restart policy 31 | Restart=always 32 | RestartSec=5s 33 | 34 | [Install] 35 | WantedBy=multi-user.target 36 | -------------------------------------------------------------------------------- /installers/ubuntu/installer.sh: -------------------------------------------------------------------------------- 1 | ## Ubuntu (newest LTS) simple installer without building requirements 2 | ## Non-free components may not be included in this 3 | 4 | sudo apt-get update 5 | 6 | # Use a virtual environment or delete the following file if having issues 7 | # Remove EXTERNALLY-MANAGED file for any Python version (Debian 12+ systems) 8 | PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') 9 | sudo rm /usr/lib/python${PYTHON_VERSION}/EXTERNALLY-MANAGED 2>/dev/null || true 10 | 11 | # Install GStreamer and development packages 12 | sudo apt-get install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev \ 13 | gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly \ 14 | gstreamer1.0-libav gstreamer1.0-tools gstreamer1.0-x gstreamer1.0-alsa gstreamer1.0-gl gstreamer1.0-gtk3 \ 15 | gstreamer1.0-qt5 gstreamer1.0-pulseaudio gstreamer1.0-nice 16 | 17 | # Try to install rust plugins for WHIP/WHEP support 18 | sudo apt-get install -y gstreamer1.0-plugins-rs 2>/dev/null || echo "Note: WHIP/WHEP plugins not available in repos" 19 | 20 | # Install Python and required modules 21 | sudo apt install python3-pip -y 22 | pip3 install websockets cryptography 23 | 24 | # Install additional recommended packages 25 | sudo apt-get install -y python3-rtmidi git 26 | -------------------------------------------------------------------------------- /tools/check_splitmux_pads.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Check splitmuxsink pad templates""" 3 | import gi 4 | gi.require_version('Gst', '1.0') 5 | from gi.repository import Gst 6 | 7 | Gst.init(None) 8 | 9 | # Create splitmuxsink 10 | splitmuxsink = Gst.ElementFactory.make("splitmuxsink", None) 11 | 12 | # List all pad templates 13 | factory = splitmuxsink.get_factory() 14 | print("Pad templates for splitmuxsink:") 15 | for i in range(factory.get_num_pad_templates()): 16 | template = factory.get_static_pad_templates()[i] 17 | print(f" {template.name_template} ({template.direction.value_nick}) - {template.presence.value_nick}") 18 | 19 | # Try different approaches 20 | print("\nTrying to request pads:") 21 | 22 | # Try video and audio names 23 | for name in ['video', 'audio', 'sink', 'sink_%u', 'video_%u', 'audio_%u']: 24 | pad = splitmuxsink.request_pad_simple(name) 25 | if pad: 26 | print(f" ✓ Successfully got pad with '{name}': {pad.get_name()}") 27 | else: 28 | print(f" ✗ Failed with '{name}'") 29 | 30 | # Check if splitmuxsink needs a muxer set first 31 | print("\nSetting muxer property and trying again:") 32 | muxer = Gst.ElementFactory.make("mpegtsmux", None) 33 | splitmuxsink.set_property("muxer", muxer) 34 | 35 | for name in ['video', 'audio', 'sink_%u']: 36 | pad = splitmuxsink.request_pad_simple(name) 37 | if pad: 38 | print(f" ✓ Successfully got pad with '{name}': {pad.get_name()}") 39 | else: 40 | print(f" ✗ Failed with '{name}'") -------------------------------------------------------------------------------- /.github/workflows/enhance-commits.yml: -------------------------------------------------------------------------------- 1 | name: Enhance Commit Messages 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | types: [opened, synchronize] 8 | 9 | jobs: 10 | enhance-commits: 11 | if: "!contains(github.event.head_commit.message, '[auto-enhanced]') && !contains(github.event.head_commit.message, '[skip pages]') && !contains(github.event.head_commit.message, '[release]') && github.actor != 'github-pages[bot]' && github.actor != 'github-actions[bot]'" 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: write 15 | pull-requests: write 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 2 22 | token: ${{ secrets.COMMIT_ENHANCER_PAT }} 23 | 24 | - name: Setup Node.js 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: '18' 28 | 29 | - name: Install dependencies 30 | run: npm install @google/generative-ai axios 31 | 32 | - name: Configure Git 33 | run: | 34 | git config --global user.name "GitHub Actions Commit Enhancer" 35 | git config --global user.email "actions@github.com" 36 | 37 | - name: Enhance commit messages 38 | id: enhance 39 | env: 40 | GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} 41 | GITHUB_TOKEN: ${{ secrets.COMMIT_ENHANCER_PAT }} # This is used by the script for PR updates 42 | run: node .github/scripts/enhance-commits.js -------------------------------------------------------------------------------- /tools/check_hls_status.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import time 4 | 5 | # Check latest HLS files 6 | m3u8_file = "asdfasfsdfgasdf_YAPCDUE808d64_1750667561.m3u8" 7 | if os.path.exists(m3u8_file): 8 | print(f"✓ M3U8 file exists: {m3u8_file}") 9 | print(f" Size: {os.path.getsize(m3u8_file)} bytes") 10 | print(f" Modified: {time.ctime(os.path.getmtime(m3u8_file))}") 11 | 12 | # Read content 13 | with open(m3u8_file, 'r') as f: 14 | content = f.read() 15 | 16 | # Count segments 17 | segments = [line for line in content.split('\n') if line.endswith('.ts')] 18 | print(f" Segments: {len(segments)}") 19 | 20 | # Check each segment 21 | print("\nSegment status:") 22 | for seg in segments[:5]: # Check first 5 23 | if os.path.exists(seg): 24 | size = os.path.getsize(seg) 25 | print(f" ✓ {seg}: {size:,} bytes") 26 | else: 27 | print(f" ✗ {seg}: NOT FOUND") 28 | 29 | # Check for ENDLIST 30 | if '#EXT-X-ENDLIST' in content: 31 | print("\n✓ Stream is complete (has ENDLIST)") 32 | else: 33 | print("\n⚠ Stream is live (no ENDLIST)") 34 | else: 35 | print(f"✗ M3U8 file not found: {m3u8_file}") 36 | 37 | # Test with ffprobe 38 | print("\nTesting with ffprobe:") 39 | import subprocess 40 | try: 41 | result = subprocess.run(['ffprobe', '-v', 'error', '-show_format', m3u8_file], 42 | capture_output=True, text=True) 43 | if result.returncode == 0: 44 | print("✓ ffprobe can read the playlist") 45 | else: 46 | print(f"✗ ffprobe error: {result.stderr}") 47 | except Exception as e: 48 | print(f"✗ ffprobe not available: {e}") -------------------------------------------------------------------------------- /convert_to_numpy_examples/basic_recv.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | import numpy as np 3 | import time 4 | from multiprocessing import shared_memory, Lock 5 | from multiprocessing.resource_tracker import unregister 6 | 7 | def receiver_process(shm_name): 8 | shm = shared_memory.SharedMemory(name=shm_name) 9 | frame_buffer = np.ndarray(shm.size, dtype=np.uint8, buffer=shm.buf) 10 | unregister(shm._name, 'shared_memory') # https://forums.raspberrypi.com/viewtopic.php?t=340441#p2039792 11 | last_frame = -1 12 | #lock = Lock() 13 | try: 14 | while True: 15 | #lock.acquire() 16 | frame_data = frame_buffer.copy() # Make a copy to avoid modifying the shared memory 17 | #lock.release() 18 | frame_array = np.frombuffer(frame_data, dtype=np.uint8) 19 | meta_header = frame_array[0:5] 20 | 21 | frame = meta_header[4] 22 | if frame == last_frame: 23 | continue 24 | 25 | width = meta_header[0]*255+meta_header[1] 26 | if width==0: 27 | print("image size is 0. will retry") 28 | time.sleep(1) 29 | continue 30 | height = meta_header[2]*255+meta_header[3] 31 | last_frame = frame 32 | 33 | frame_array = frame_array[5:5+width*height*3].reshape((height,width,3)) 34 | print(np.shape(frame_array),frame, frame_array[0, 0, :]) 35 | 36 | time.sleep(1/60) 37 | finally: 38 | shm.close() 39 | 40 | if __name__ == "__main__": 41 | shm_name = "psm_raspininja_streamid" 42 | 43 | receiver = multiprocessing.Process(target=receiver_process, args=(shm_name,)) 44 | receiver.start() 45 | receiver.join() 46 | -------------------------------------------------------------------------------- /dev_notes/HLS_SEGMENT_FIX.md: -------------------------------------------------------------------------------- 1 | # HLS Segment Event Fix 2 | 3 | ## Problem 4 | When running HLS recording mode on Nvidia Jetson, GStreamer was showing warnings: 5 | ``` 6 | mpegtsmux gstmpegtsmux.c:1236:mpegtsmux_collected: Got data flow before segment event 7 | ``` 8 | 9 | This occurred because the mpegtsmux element was receiving data without proper segment events being propagated through the pipeline when elements are connected dynamically. 10 | 11 | ## Solution 12 | The fix involves several improvements to ensure proper segment event propagation: 13 | 14 | 1. **Segment Event Injection**: Added pad probes that inject segment events before the first buffer for both video and audio streams 15 | 2. **Identity Elements**: Added identity elements with `single-segment=true` property to help with segment handling 16 | 3. **Mpegtsmux Configuration**: Set the `alignment` property on mpegtsmux for proper segmentation 17 | 4. **State Management**: Ensure HLS sink transitions to PLAYING state after all connections are made 18 | 5. **Monitoring**: Added file creation checks to verify HLS segments are being written 19 | 20 | ## Code Changes 21 | 22 | ### Video Pipeline (H264) 23 | Before: `queue -> depay -> h264parse -> video_queue -> hlssink` 24 | After: `queue -> depay -> h264parse -> identity -> video_queue -> hlssink` 25 | 26 | ### Audio Pipeline (OPUS to AAC) 27 | Before: `queue -> depay -> decoder -> convert -> aacenc -> aacparse -> audio_queue -> hlssink` 28 | After: `queue -> depay -> decoder -> convert -> aacenc -> aacparse -> identity -> audio_queue -> hlssink` 29 | 30 | The identity elements help ensure proper segment boundaries, and the pad probes inject segment events when needed. 31 | 32 | ## Testing 33 | To test the fix: 34 | ```bash 35 | python3 publish.py --record-room --hls --room TestRoom --webserver 8080 36 | ``` 37 | 38 | The warnings should no longer appear, and HLS segments should be created properly. -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Vdo.Ninja Audio to AI 5 | 48 | 49 | 50 |
51 |

VDO.ninja audio to text

52 |

Enter vdo.ninja room and push channel

53 |
54 | 55 | 56 | 57 |
58 |

code : https://github.com/papiche/raspberry_ninja/

59 |
60 | 61 | 62 | -------------------------------------------------------------------------------- /installers/README.md: -------------------------------------------------------------------------------- 1 | # Platform-Specific Installers 2 | 3 | This directory contains platform-specific installation guides and scripts for advanced users who need hardware-specific optimizations. 4 | 5 | ## 🚀 Most Users Should Use the Universal Installer 6 | 7 | ```bash 8 | # From the repository root: 9 | ./install.sh 10 | ``` 11 | 12 | Or use the one-liner: 13 | ```bash 14 | curl -sSL https://raw.githubusercontent.com/steveseguin/raspberry_ninja/main/install.sh | bash 15 | ``` 16 | 17 | The universal installer automatically detects your platform and handles everything for you. 18 | 19 | ## 📁 Platform-Specific Guides 20 | 21 | These guides are for users who need: 22 | - Hardware-specific optimizations 23 | - Custom configurations 24 | - Manual control over the installation process 25 | 26 | ### Available Platforms: 27 | 28 | - **[raspberry_pi/](./raspberry_pi/)** - Raspberry Pi specific optimizations 29 | - CSI camera support 30 | - GPIO features 31 | - Hardware encoding on Pi 4 32 | 33 | - **[nvidia_jetson/](./nvidia_jetson/)** - Nvidia Jetson optimizations 34 | - NVENC hardware encoding 35 | - DeepStream integration 36 | - CSI camera support 37 | 38 | - **[ubuntu/](./ubuntu/)** - Desktop Ubuntu/Debian 39 | - Full desktop integration 40 | - Multiple camera support 41 | 42 | - **[orangepi/](./orangepi/)** - Orange Pi boards 43 | - Board-specific fixes 44 | - Hardware encoding where available 45 | 46 | - **[mac/](./mac/)** - macOS installation 47 | - Homebrew-based setup 48 | - macOS-specific adaptations 49 | 50 | - **[wsl/](./wsl/)** - Windows Subsystem for Linux 51 | - WSL2 configuration 52 | - Windows integration tips 53 | 54 | ## 📋 Manual Installation Steps 55 | 56 | If you prefer complete manual control: 57 | 58 | 1. **Install GStreamer** (1.16 or newer) 59 | 2. **Install Python 3** and pip 60 | 3. **Install Python packages**: `websockets`, `cryptography`, `aiohttp` 61 | 4. **Clone this repository** 62 | 5. **Run**: `python3 publish.py --test` 63 | 64 | See each platform's README for specific package names and commands. -------------------------------------------------------------------------------- /dev_notes/JETSON_HLS_FIX_SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Jetson Nano HLS Recording Fix Summary 2 | 3 | ## Problem 4 | On Jetson Nano 2GB running GStreamer 1.23.0 (development version), HLS recording was showing "Got data flow before segment event" warnings from mpegtsmux. This didn't occur on x86/WSL systems. 5 | 6 | ## Root Cause 7 | 1. **GStreamer 1.23.0** has stricter segment event checking 8 | 2. **Jetson Nano 2GB** slower processing exposes race conditions 9 | 3. Dynamic pad linking from webrtcbin causes segment events to arrive after data 10 | 11 | ## Solution Implemented 12 | 13 | ### 1. Explicit mpegtsmux Control 14 | - Create our own mpegtsmux element (except for splitmuxsink which manages its own) 15 | - Configure with proper HLS alignment settings 16 | - Direct control over element state transitions 17 | 18 | ### 2. Segment Event Injection 19 | - Add pad probes on video/audio queues before mux 20 | - Inject segment events before first buffer reaches mux 21 | - Ensures proper event ordering 22 | 23 | ### 3. Delayed Element Start 24 | - 100ms delay after pad connections before starting mux/sink 25 | - Allows segment events to propagate on slower systems 26 | - Prevents race condition 27 | 28 | ### 4. Dual Mode Support 29 | - **splitmuxsink**: Uses internal mux, connects directly 30 | - **manual mode**: Uses explicit mux for future m3u8 support 31 | 32 | ## Testing 33 | Run on Jetson Nano: 34 | ```bash 35 | python3 publish.py --record-room --hls --room TestRoom --webserver 8087 36 | ``` 37 | 38 | Expected results: 39 | - No segment event warnings 40 | - HLS files created successfully 41 | - Clean state transitions logged 42 | 43 | ## Key Code Changes 44 | 1. `setup_hls_muxer()`: Creates explicit mpegtsmux or uses internal 45 | 2. Video/audio pad connections: Route through mux with segment injection 46 | 3. `check_hls_streams_ready()`: Delayed start with proper state management 47 | 48 | ## Architecture Notes 49 | - GStreamer dev versions (1.23.x) have stricter checks than stable 50 | - ARM scheduling differences expose latent timing bugs 51 | - Explicit pipeline control prevents race conditions -------------------------------------------------------------------------------- /ndi/fix_ndi_network.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Fix NDI discovery between WSL2 and Windows host 3 | 4 | echo "Fixing NDI network visibility between WSL2 and Windows..." 5 | 6 | # Get WSL2 IP address 7 | WSL_IP=$(ip addr show eth0 | grep 'inet ' | awk '{print $2}' | cut -d/ -f1) 8 | echo "WSL2 IP: $WSL_IP" 9 | 10 | # Get Windows host IP (WSL2 uses it as default gateway) 11 | WIN_IP=$(ip route show | grep default | awk '{print $3}') 12 | echo "Windows host IP: $WIN_IP" 13 | 14 | echo "" 15 | echo "SOLUTION 1: Use NDI Access Manager (Recommended)" 16 | echo "------------------------------------------------" 17 | echo "1. On Windows, open NDI Access Manager" 18 | echo "2. Go to the 'Advanced' tab" 19 | echo "3. Add WSL IP to 'Extra IPs': $WSL_IP" 20 | echo "4. Click 'OK' and restart NDI applications" 21 | 22 | echo "" 23 | echo "SOLUTION 2: Use mDNS relay" 24 | echo "--------------------------" 25 | echo "Install avahi in WSL to relay mDNS between WSL and Windows:" 26 | echo "sudo apt-get install avahi-daemon avahi-utils" 27 | echo "sudo service avahi-daemon start" 28 | 29 | echo "" 30 | echo "SOLUTION 3: Direct IP Connection" 31 | echo "---------------------------------" 32 | echo "Some NDI viewers allow direct IP connection." 33 | echo "Try connecting directly to: $WSL_IP" 34 | 35 | echo "" 36 | echo "SOLUTION 4: Use NDI Proxy" 37 | echo "-------------------------" 38 | echo "Run NDI Proxy on Windows to bridge the networks." 39 | 40 | echo "" 41 | echo "TESTING NDI OUTPUT" 42 | echo "------------------" 43 | echo "To test if NDI is working in WSL:" 44 | echo "gst-launch-1.0 videotestsrc ! ndisink ndi-name='WSL-Test'" 45 | echo "" 46 | echo "Then check if 'WSL-Test' appears in your Windows NDI viewer." 47 | 48 | # Create a test script 49 | cat > test_ndi_output.sh << 'EOF' 50 | #!/bin/bash 51 | echo "Creating test NDI stream 'WSL-NDI-Test'..." 52 | echo "Check your Windows NDI viewer for this stream." 53 | echo "Press Ctrl+C to stop." 54 | gst-launch-1.0 videotestsrc pattern=ball ! video/x-raw,width=640,height=480,framerate=30/1 ! ndisink ndi-name="WSL-NDI-Test" 55 | EOF 56 | 57 | chmod +x test_ndi_output.sh 58 | 59 | echo "" 60 | echo "Created test_ndi_output.sh - run it to test NDI visibility" -------------------------------------------------------------------------------- /.github/workflows/auto-release.yml: -------------------------------------------------------------------------------- 1 | name: Auto Release 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - 'publish.py' 8 | - 'raspberry_pi/installer.sh' 9 | - 'nvidia_jetson/installer.sh' 10 | - 'orangepi/installer.sh' 11 | - 'ubuntu/installer.sh' 12 | - '**/*.service' 13 | - 'raspberry_pi/Cargo.toml' 14 | 15 | jobs: 16 | check-and-release: 17 | # Don't run on release commits or auto-enhanced commits 18 | if: | 19 | !contains(github.event.head_commit.message, '[release]') && 20 | !contains(github.event.head_commit.message, '[skip-release]') && 21 | !contains(github.event.head_commit.message, '[auto-enhanced]') && 22 | github.actor != 'github-actions[bot]' && 23 | github.actor != 'GitHub Actions Release Bot' 24 | 25 | runs-on: ubuntu-latest 26 | permissions: 27 | contents: write 28 | packages: write 29 | 30 | steps: 31 | - name: Checkout code 32 | uses: actions/checkout@v4 33 | with: 34 | fetch-depth: 0 # Need full history for version analysis 35 | token: ${{ secrets.GITHUB_TOKEN }} 36 | 37 | - name: Setup Node.js 38 | uses: actions/setup-node@v4 39 | with: 40 | node-version: '18' 41 | 42 | - name: Configure Git 43 | run: | 44 | git config --global user.name "GitHub Actions Release Bot" 45 | git config --global user.email "actions@github.com" 46 | 47 | - name: Install GitHub CLI 48 | run: | 49 | type gh >/dev/null 2>&1 || ( 50 | curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo gpg --dearmor -o /usr/share/keyrings/githubcli-archive-keyring.gpg 51 | echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null 52 | sudo apt update 53 | sudo apt install gh -y 54 | ) 55 | 56 | - name: Run auto-release script 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # For GitHub CLI 60 | run: node .github/scripts/auto-release.js -------------------------------------------------------------------------------- /tools/check_splitmux_signals.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import gi 4 | gi.require_version('Gst', '1.0') 5 | from gi.repository import Gst, GObject 6 | 7 | Gst.init(None) 8 | 9 | # Create splitmuxsink element 10 | sink = Gst.ElementFactory.make('splitmuxsink', None) 11 | 12 | if sink: 13 | print("Splitmuxsink signals:") 14 | print("-" * 40) 15 | 16 | # Get all signals for this element 17 | signal_ids = GObject.signal_list_ids(sink) 18 | for signal_id in signal_ids: 19 | signal_info = GObject.signal_query(signal_id) 20 | print(f"\nSignal: {signal_info.signal_name}") 21 | 22 | # Test specific signals we're interested in 23 | print("\n\nTesting signal connections:") 24 | print("-" * 40) 25 | 26 | def test_format_location(splitmux, fragment_id): 27 | print(f"format-location called with: splitmux={splitmux}, fragment_id={fragment_id}") 28 | return f"test_{fragment_id:05d}.ts" 29 | 30 | def test_format_location_full_2args(splitmux, filename): 31 | print(f"format-location-full (2 args) called with: splitmux={splitmux}, filename={filename}") 32 | 33 | def test_format_location_full_3args(splitmux, fragment_id, filename): 34 | print(f"format-location-full (3 args) called with: splitmux={splitmux}, fragment_id={fragment_id}, filename={filename}") 35 | 36 | # Try connecting with different signatures 37 | try: 38 | sink.connect('format-location', test_format_location) 39 | print("✓ format-location connected successfully") 40 | except Exception as e: 41 | print(f"✗ format-location failed: {e}") 42 | 43 | try: 44 | sink.connect('format-location-full', test_format_location_full_2args) 45 | print("✓ format-location-full (2 args) connected successfully") 46 | except Exception as e: 47 | print(f"✗ format-location-full (2 args) failed: {e}") 48 | 49 | try: 50 | sink.disconnect_by_func(test_format_location_full_2args) 51 | except: 52 | pass 53 | 54 | try: 55 | sink.connect('format-location-full', test_format_location_full_3args) 56 | print("✓ format-location-full (3 args) connected successfully") 57 | except Exception as e: 58 | print(f"✗ format-location-full (3 args) failed: {e}") -------------------------------------------------------------------------------- /installers/raspberry_pi/setup_c779.sh: -------------------------------------------------------------------------------- 1 | ## This configures the C779 HDMI to CSI adapter for use. 2 | ## You can also find a guide with your board's manufacture, such as https://wiki.geekworm.com/C779-Software 3 | 4 | ## First, Setup /boot/config.txt ; you will need to uncomment or add a line near the bottom of the file and then save then file. 5 | # ie: dtoverlay=tc358743, dtoverlay=tc358743-audio, dtoverlay=tc358743,4lane=1 or whatever your board/requirements support 6 | # You may also need "dtoverlay=vc4-kms-v3d" or "dtoverlay=vc4-fkms-v3d" added also. 7 | ########## For reference, my own RPi4 2023 config.txt for the C779 looks like: 8 | ## arm_64bit=1 9 | ## disable_overscan=1 10 | ## [all] 11 | ## dtoverlay=vc4-fkms-v3d 12 | ## max_framebuffers=2 13 | ## dtoverlay=tc358743 14 | #################### 15 | 16 | ## Second, ensure you have enough CMA memory; this probabably isn't an issue, but still... 17 | dmesg | grep cma 18 | # If you have less than 96M, add some more 19 | ## cma=96M can be added to /boot/cmdline.txt if needed (leave no blank last line) 20 | 21 | ## Third, enable the camera module. More recently, you'll need to enable the legacy camera mode to continue. 22 | sudo raspi-config 23 | # -> `-'Interfacing Options' -> '[Legacy] Camera' -> enable and "Finish" 24 | ## Fourth, unplug any HDMI input from the board; we will plug an HDMI source in later 25 | ## REBOOT 26 | sudo reboot 27 | 28 | ## you can make your own EDID file; 1080P25 / 1080P30 and even in some cases 1080P60 are possible (but not all!) 29 | wget https://raw.githubusercontent.com/steveseguin/CSI2_device_config/master/1080P30EDID.txt 30 | v4l2-ctl --set-edid=file=1080P30EDID.txt # load it 31 | ## If you get an error at this step, check the community forum here: https://forums.raspberrypi.com//viewtopic.php?f=38&t=281972 32 | # https://raw.githubusercontent.com/steveseguin/CSI2_device_config/master/1080P60EDID.txt ## CM4 33 | # v4l2-ctl --set-edid=file=1080P60EDID.txt ## Only if using a RPi Compute Module, since a basic rpi lacks enough data lanes 34 | 35 | ## PLUG IN HDMI NOW - and make sure the source (camera or whatever) can support the resolution and frame rate you specified in the EDID 36 | v4l2-ctl --set-dv-bt-timings query 37 | # v4l2-ctl --log-status 38 | v4l2-ctl --query-dv-timings 39 | ## Should show the correct resolution output of your camera 40 | ## Please note; camera's HDMI output should be in 8-bit color mode, with no more than 1080p50 set, inless using compute module 41 | -------------------------------------------------------------------------------- /installers/wsl/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Windows (WSL) installer - Ubuntu 3 | 4 | ### Install Windows Subsystem for Linux 5 | 6 | https://learn.microsoft.com/en-us/windows/wsl/install 7 | 8 | The basic idea of how to install WSL is to open up the Windows Powershell and enter something like: 9 | 10 | ``` 11 | wsl --install 12 | wsl --install -d Ubuntu 13 | ``` 14 | image 15 | 16 | There might also be graphical installers for Ubuntu for Windows these days, perhaps already installed on your computer, ready to go. 17 | 18 | Anyways, once Ubuntu is installed, you'd open that. You might be able to just search in Windows for and run `wsl` to open it. 19 | 20 | From that Linux command prompt shell, you'd install and use Raspberry Ninja. Pretty nifty! 21 | 22 | ### This install may not have broad hardware support; cameras, encoders, etc, but the basics are there 23 | ``` 24 | sudo apt-get update && sudo apt upgrade -y 25 | sudo apt-get install python3-pip -y 26 | 27 | sudo apt-get install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-tools gstreamer1.0-x python3-pyqt5 python3-opengl gstreamer1.0-alsa gstreamer1.0-gl gstreamer1.0-qt5 gstreamer1.0-gtk3 gstreamer1.0-pulseaudio gstreamer1.0-nice gstreamer1.0-plugins-base-apps 28 | 29 | pip3 install --break-system-packages websockets cryptography 30 | pip3 install aiohttp --break-system-packages 31 | 32 | sudo apt-get install git -y 33 | cd ~ 34 | git clone https://github.com/steveseguin/raspberry_ninja 35 | cd raspberry_ninja 36 | python3 publish.py --test 37 | ``` 38 | Raspberry.Ninja should be running now, and you can open the provided link in your browser to confirm. 39 | 40 | Using a camera on WSL is a bit more tricky, but running `gst-inspect-1.0 | grep "src"` will give you a sense of what media sources you have available to you. It could be possible to pipe encoded video data into Raspberry.Ninja via OBS Studio for example, however specifying the video meta data (caps) and getting the piping setup right is then needed. This process needs to be documented. 41 | 42 | If wishing to record a remote video to disk, without decoding it first, you can use: 43 | 44 | ``` 45 | python3 publish.py --record STREAMIDHERE123 46 | ``` 47 | You can then access the saved recording in File Explorer here at `\\wsl$`, and navigate to the raspberry_ninja folder as needed: 48 | 49 | ie, for me, username `vdo`, it would be here: `\\wsl.localhost\Ubuntu\home\vdo\raspberry_ninja\STREAMIDHERE123.mkv` 50 | 51 | -------------------------------------------------------------------------------- /installers/mac/readme.md: -------------------------------------------------------------------------------- 1 | **Raspberry Ninja runs on a Mac!** Useful if you want low level control over your VDO.Ninja outputs, or perhaps you want to use VDO.Ninaj with OpenCV/SciKit for your next AI project. Anyways, the installer guide is below 2 | 3 | #### Moderate pain tolerance needed to install on Mac 4 | 5 | Installing Raspberry Ninja for Mac OS is made a bit more difficult due to the decision not to include GStreamer-WebRTC support by the Homebrew developers. see: https://github.com/Homebrew/homebrew-core/pull/25680. Be sure to provide them feedback that you think its valuable to have included by default. It could the easiest way to install Raspberry Ninja with a minor change. :) 6 | 7 | It's also possible to install Gstreamer on Mac OS using an install package provided by the Gstreamer developers, but it's unsigned (so Apple makes it annoying to install), the website for it is often offline, and I'm not sure how to get its Python-bindings working. If somneone can provide instructions for it instead though, I'll include them though. 8 | 9 | ## Homebrew install method for Mac OS X 10 | 11 | note: I already had XCode installed on my Mac M1, but you might need to install XCode as well for the following steps to work. 12 | 13 | Just open Terminal on your mac, and enter each of the following commands, one at a time, and hopefully it goes well! 14 | 15 | ``` 16 | # install brew 17 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 18 | 19 | # update if needed 20 | brew update 21 | brew upgrade 22 | 23 | # install dependencies 24 | brew install automake autoconf libtool pkg-config glib gtk-doc srtp wget git 25 | brew install python gobject-introspection 26 | 27 | # need to build gstreamer from source - it took several minutes for me. 28 | wget https://gist.githubusercontent.com/steveseguin/0533d4ab0bd8cc9acf5737bff20d37a8/raw/e495c41b85808d845ed4d21b0b41840a03d44e96/gstreamer.rb 29 | brew reinstall --build-from-source gstreamer.rb 30 | 31 | # get Raspberry Ninja 32 | git clone https://github.com/steveseguin/raspberry_ninja 33 | cd raspberry_ninja 34 | 35 | # run test 36 | python3 publish.py --test 37 | ``` 38 | I think I got most of the hard parts figured out with this brew method, but I don't have a fresh macbook to test the installer on again, so I might be overlooking something. Please report issues with the installer so I can update as needed. 39 | 40 | ## Hardware encoding 41 | 42 | Looks like there is an h264 and h265 video encoder available, but by default I think I have x264 added support at the moment. 43 | ``` 44 | steveseguin@Steves-MacBook-Air raspberry_ninja % gst-inspect-1.0 | grep "264" 45 | applemedia: vtenc_h264: H.264 encoder 46 | applemedia: vtenc_h264_hw: H.264 (HW only) encoder 47 | x264: x264enc: x264 H.264 Encoder 48 | ``` 49 | -------------------------------------------------------------------------------- /dev_notes/HLS_UNIVERSAL_FIX.md: -------------------------------------------------------------------------------- 1 | # Universal HLS Fix for Jetson and x86 2 | 3 | ## Summary 4 | This fix addresses HLS recording issues on both Nvidia Jetson (with GStreamer 1.23.0) and x86 platforms. The solution ensures proper audio/video muxing without transcoding H264. 5 | 6 | ## Changes Made 7 | 8 | ### 1. Platform Detection 9 | - Added `is_jetson()` method to detect Nvidia Jetson platforms 10 | - Adjusts timing delays based on platform (200ms for Jetson, 100ms for x86) 11 | 12 | ### 2. Blocking Pad Probes 13 | - Changed from non-blocking to BLOCKING pad probes for segment event injection 14 | - Ensures segment events are injected before any data flows to mpegtsmux 15 | - Critical for GStreamer 1.23.0's stricter event ordering requirements 16 | 17 | ### 3. Identity Elements 18 | - Added identity elements with `single-segment=true` for both audio and video 19 | - Helps with segment boundary management and timestamp synchronization 20 | - Provides better compatibility across different GStreamer versions 21 | 22 | ### 4. Improved Segment Event Handling 23 | - Segment events now use buffer PTS as base time for better synchronization 24 | - Handles both injected and natural segment events 25 | - Logs segment base times for debugging 26 | 27 | ### 5. Splitmuxsink State Management 28 | - Improved state transition handling for splitmuxsink 29 | - Sets splitmuxsink to NULL before transitioning to PLAYING 30 | - Configures internal muxer properties when accessible 31 | 32 | ### 6. Muxer Configuration 33 | - Added prog-map property to mpegtsmux for better stream identification 34 | - Ensures alignment=7 for HLS compatibility 35 | - Sets latency=0 for live streaming 36 | 37 | ## Key Benefits 38 | 39 | 1. **Jetson Compatibility**: Fixes "Got data flow before segment event" warnings on GStreamer 1.23.0 40 | 2. **Audio/Video Sync**: Proper timestamp synchronization when audio joins existing video stream 41 | 3. **No Transcoding**: H264 video passes through without re-encoding 42 | 4. **Universal Solution**: Single codebase works on both ARM (Jetson) and x86 platforms 43 | 44 | ## Testing 45 | 46 | To test the fix: 47 | ```bash 48 | python3 publish.py --record-room --hls --room TestRoom --webserver 8087 49 | ``` 50 | 51 | Expected results: 52 | - No segment event warnings in logs 53 | - HLS segments created with both audio and video 54 | - Playback works in video.js player 55 | - Works on both Jetson and x86 platforms 56 | 57 | ## Technical Details 58 | 59 | The fix addresses several race conditions: 60 | 1. **Event Ordering**: Blocking probes ensure segment events arrive before data 61 | 2. **State Transitions**: Proper NULL->PLAYING transitions for all elements 62 | 3. **Timestamp Sync**: Using buffer PTS for segment base times 63 | 4. **Platform Timing**: Different delays for ARM vs x86 architectures 64 | 65 | ## Related Files 66 | - `webrtc_subprocess_glib.py`: Main implementation 67 | - `HLS_SEGMENT_FIX.md`: Original Jetson-specific fix 68 | - `JETSON_HLS_FIX_SUMMARY.md`: Jetson debugging notes -------------------------------------------------------------------------------- /tests/test_multiple_webrtc_connections.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Unit tests for multiple WebRTC connections 4 | """ 5 | 6 | import unittest 7 | import sys 8 | import os 9 | 10 | # Add the parent directory to Python path 11 | sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) 12 | 13 | 14 | class TestMultipleWebRTCConnections(unittest.TestCase): 15 | """Test cases for handling multiple WebRTC connections""" 16 | 17 | def setUp(self): 18 | """Set up test fixtures""" 19 | self.test_config = { 20 | 'room': 'test_room', 21 | 'stream_id': 'test_stream', 22 | 'server': 'wss://apibackup.vdo.ninja:443' 23 | } 24 | 25 | def test_connection_isolation(self): 26 | """Test that multiple connections are properly isolated""" 27 | # This is a placeholder test that passes 28 | # TODO: Implement actual connection isolation tests 29 | connections = [] 30 | 31 | # Simulate creating multiple connections 32 | for i in range(3): 33 | conn = { 34 | 'id': f'conn_{i}', 35 | 'stream_id': f'{self.test_config["stream_id"]}_{i}', 36 | 'room': self.test_config['room'] 37 | } 38 | connections.append(conn) 39 | 40 | # Verify each connection has unique ID 41 | ids = [conn['id'] for conn in connections] 42 | self.assertEqual(len(ids), len(set(ids)), "Connection IDs should be unique") 43 | 44 | # Verify connections are independent 45 | for i, conn in enumerate(connections): 46 | self.assertEqual(conn['id'], f'conn_{i}') 47 | self.assertEqual(conn['stream_id'], f'{self.test_config["stream_id"]}_{i}') 48 | 49 | def test_multiple_rooms(self): 50 | """Test handling connections to multiple rooms""" 51 | rooms = ['room1', 'room2', 'room3'] 52 | connections = [] 53 | 54 | for room in rooms: 55 | conn = {'room': room, 'active': True} 56 | connections.append(conn) 57 | 58 | # Verify all rooms are tracked 59 | self.assertEqual(len(connections), len(rooms)) 60 | for conn in connections: 61 | self.assertTrue(conn['active']) 62 | 63 | 64 | if __name__ == '__main__': 65 | # Support running specific test when called from command line 66 | if len(sys.argv) > 1 and '::' in sys.argv[-1]: 67 | # Extract test name from pytest-style argument 68 | test_spec = sys.argv[-1] 69 | if 'test_connection_isolation' in test_spec: 70 | # Run specific test 71 | suite = unittest.TestLoader().loadTestsFromName( 72 | 'test_connection_isolation', 73 | TestMultipleWebRTCConnections 74 | ) 75 | runner = unittest.TextTestRunner() 76 | result = runner.run(suite) 77 | sys.exit(0 if result.wasSuccessful() else 1) 78 | 79 | # Otherwise run all tests 80 | unittest.main() -------------------------------------------------------------------------------- /.github/workflows/test-local.yml: -------------------------------------------------------------------------------- 1 | name: Local Test Suite 2 | 3 | on: 4 | push: 5 | branches: [ main, develop, feature/* ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | quick-test: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: Set up Python 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: '3.10' 21 | 22 | - name: Cache dependencies 23 | uses: actions/cache@v3 24 | with: 25 | path: ~/.cache/pip 26 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} 27 | restore-keys: | 28 | ${{ runner.os }}-pip- 29 | 30 | - name: Install minimal dependencies 31 | run: | 32 | python -m pip install --upgrade pip 33 | pip install pytest pytest-asyncio psutil 34 | 35 | - name: Run core unit tests 36 | run: | 37 | echo "=== Running Core Unit Tests ===" 38 | python -m pytest tests/test_multiple_webrtc_connections.py -v -x --tb=short || true 39 | 40 | - name: Generate test summary 41 | if: always() 42 | run: | 43 | echo "=== Test Summary ===" 44 | cat > test_summary.py << 'EOF' 45 | import json 46 | import os 47 | from datetime import datetime 48 | 49 | results = { 50 | 'timestamp': datetime.now().isoformat(), 51 | 'status': 'completed', 52 | 'python_version': '3.10', 53 | 'tests_run': True 54 | } 55 | 56 | with open('quick_test_results.json', 'w') as f: 57 | json.dump(results, f, indent=2) 58 | print('Quick test run completed') 59 | EOF 60 | python3 test_summary.py 61 | 62 | - name: Upload test results 63 | uses: actions/upload-artifact@v4 64 | if: always() 65 | with: 66 | name: quick-test-results 67 | path: quick_test_results.json 68 | 69 | recording-test: 70 | runs-on: ubuntu-latest 71 | 72 | steps: 73 | - uses: actions/checkout@v3 74 | 75 | - name: Set up Python 76 | uses: actions/setup-python@v4 77 | with: 78 | python-version: '3.10' 79 | 80 | - name: Test recording functionality 81 | run: | 82 | echo "=== Testing Recording Functionality ===" 83 | # Test if recording files can be created 84 | cat > recording_test.py << 'EOF' 85 | import os 86 | import time 87 | 88 | # Simulate recording file creation 89 | test_file = f'test_recording_{int(time.time())}.mkv' 90 | with open(test_file, 'wb') as f: 91 | f.write(b'TEST_DATA' * 100) 92 | 93 | if os.path.exists(test_file): 94 | print(f'✓ Recording test file created: {test_file}') 95 | print(f' Size: {os.path.getsize(test_file)} bytes') 96 | os.remove(test_file) 97 | else: 98 | print('✗ Failed to create recording test file') 99 | EOF 100 | python3 recording_test.py -------------------------------------------------------------------------------- /installers/raspberry_pi/simpleinstall.md: -------------------------------------------------------------------------------- 1 | ## Installation on a Raspberry Pi /w Bullseye 32bit 2 | 3 | This is the `'simple approach'` to installing Raspberry.Ninja onto a vanilla Raspberry Pi image. 4 | 5 | At the moment this may lead to the installation of Gstreamer 1.18, which works for publishing video to VDO.Ninja, but doesn't quite work well with playing back video. A newer version may be needed if intending to restream from VDO.Ninja to RTSP, NDI, file, etc. 6 | 7 | (Please note, Linux in general is designed to torment users, and so this install script may fail to work in the future as the world changes. I will try to keep it updated as best I can, but I can only say it last worked for me on February 21, 2024, using a fresh install of Raspberry Pi OS - Bookworm Lite 64-bit edition.) 8 | 9 | #### Setting up and connecting 10 | 11 | Run command to update the board, be sure that python3 and pip are installed 12 | 13 | ``sudo apt update && sudo apt upgrade -y`` 14 | 15 | If running a Debian 12-based system, including new Raspberry OS systems (eg; bookworm), you'll either want to deploy things as a virtual environment (suggested), or disable the flag that prevents self-managing depedencies (easier). You can skip this step if you don't have issues or if you prefer to manage your environment some other way. Pretty much though, this new install speedbump was added by others because the world doesn't like us having fun. 16 | 17 | ```sudo rm /usr/lib/python3.11/EXTERNALLY-MANAGED # Delete this file to prefer fun over safety``` 18 | 19 | Install some required lib 20 | 21 | `` 22 | sudo apt-get install -y python3-pip libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-tools gstreamer1.0-x python3-pyqt5 python3-opengl gstreamer1.0-alsa gstreamer1.0-gl gstreamer1.0-gtk3 gstreamer1.0-pulseaudio gstreamer1.0-nice gstreamer1.0-plugins-base-apps 23 | `` 24 | 25 | Install websocket and gi module. (Installing PyGObject often breaks due to changing dependencies, and it's near impossible to install on Windows, so consider it your lucky day if it installs without issues.) 26 | 27 | ``` 28 | pip3 install websockets 29 | pip3 install cryptography 30 | pip3 install aiohttp 31 | sudo apt-get install -y libcairo-dev libgirepository1.0-dev # these dependencies may or may not be needed; it rather depends on the mood of the universe. 32 | pip3 install PyGObject 33 | ``` 34 | 35 | Since Linux in general is designed to cause endless grief to casual users, if using Bookworm with your Raspberry Pi, you'll now need to also install libcamerasrc manually. This may fail if using an older or future version of Linux however. 36 | 37 | `` 38 | sudo apt-get install -y gstreamer1.0-libcamera 39 | `` 40 | 41 | #### Downloading Raspberry.Ninja 42 | 43 | ```sudo apt-get install git vim -y``` 44 | 45 | ```git clone https://github.com/steveseguin/raspberry_ninja``` 46 | 47 | ## Running things 48 | 49 | After all, run the command to test 50 | 51 | ``` 52 | cd raspberry_ninja # change into the directory 53 | python3 publish.py --rpi --test 54 | ``` 55 | 56 | `--test` should show a test video with static sounds. You can remove it afterwards and configure Raspberry Ninja for your actual camera. 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | .claude 9 | .actrc 10 | .claude/ 11 | 12 | # Distribution / packaging 13 | WSL2-Linux-Kernel 14 | WSL2-Linux-Kernel/ 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | venv/ 28 | wheels/ 29 | pip-wheel-metadata/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | local_settings.py 67 | db.sqlite3 68 | db.sqlite3-journal 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 101 | __pypackages__/ 102 | 103 | # Celery stuff 104 | celerybeat-schedule 105 | celerybeat.pid 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | 137 | # IDE and Editor files 138 | .vscode/ 139 | .idea/ 140 | *.swp 141 | *.swo 142 | *~ 143 | .DS_Store 144 | 145 | # Test and temporary files 146 | *.tmp 147 | *.bak 148 | *.orig 149 | # test_*.py # Commented out - we want to track test files 150 | # *_test.py # Commented out - we want to track test files 151 | *_old.py 152 | *_backup.py 153 | 154 | # Media files (recordings) 155 | *.webm 156 | *.mp4 157 | *.wav 158 | *.ts 159 | *.m3u8 160 | 161 | # NDI specific 162 | *_ndi_*.py 163 | ndi_wrapper.sh 164 | FIX_*.md 165 | 166 | # Local configuration 167 | .env.local 168 | .env.*.local 169 | config.local.json 170 | 171 | # Log files 172 | *.log 173 | logs/ 174 | 175 | # OS specific 176 | Thumbs.db 177 | .Trash-* 178 | .nfs* 179 | ndi/gst-plugin-ndi/ 180 | 181 | # record.py whisper transcriptions output directory 182 | *_audio.ts 183 | stt/*.txt 184 | stt/*.json 185 | 186 | # GitHub Pages specific files 187 | CNAME 188 | _config.yml 189 | 190 | # GitHub Actions specific 191 | .github/workflows/test-local.yml 192 | 193 | # Package files that users should generate themselves 194 | package-lock.json 195 | 196 | # Development/testing artifacts 197 | .actrc 198 | tests/run_hls_test.sh 199 | test_summary.py 200 | quick_test_results.json 201 | recording_test.py 202 | 203 | # macOS specific 204 | .DS_Store 205 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test Suite 2 | 3 | on: 4 | push: 5 | branches: [ main, develop ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: ['3.8', '3.9', '3.10', '3.11'] 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | 24 | - name: Install system dependencies 25 | run: | 26 | sudo apt-get update 27 | sudo apt-get install -y \ 28 | gstreamer1.0-tools \ 29 | gstreamer1.0-plugins-base \ 30 | gstreamer1.0-plugins-good \ 31 | gstreamer1.0-plugins-bad \ 32 | gstreamer1.0-plugins-ugly \ 33 | gstreamer1.0-libav \ 34 | gstreamer1.0-nice \ 35 | libgstreamer1.0-dev \ 36 | libgstreamer-plugins-base1.0-dev \ 37 | python3-gi \ 38 | python3-gi-cairo \ 39 | gir1.2-gstreamer-1.0 \ 40 | gir1.2-gst-plugins-base-1.0 41 | 42 | - name: Install Python dependencies 43 | run: | 44 | python -m pip install --upgrade pip 45 | pip install pytest pytest-asyncio pytest-cov coverage psutil 46 | 47 | - name: Run unit tests 48 | run: | 49 | # Run tests with coverage 50 | python -m pytest \ 51 | tests/test_multiple_webrtc_connections.py \ 52 | -v --tb=short --cov=. --cov-report=xml 53 | continue-on-error: true 54 | 55 | - name: Upload coverage reports 56 | uses: codecov/codecov-action@v3 57 | with: 58 | file: ./coverage.xml 59 | flags: unittests 60 | name: codecov-umbrella 61 | if: always() 62 | 63 | - name: Upload test results 64 | uses: actions/upload-artifact@v4 65 | with: 66 | name: test-results-${{ matrix.python-version }} 67 | path: | 68 | test_report.json 69 | test_report.txt 70 | coverage.xml 71 | if: always() 72 | 73 | lint: 74 | runs-on: ubuntu-latest 75 | 76 | steps: 77 | - uses: actions/checkout@v3 78 | 79 | - name: Set up Python 80 | uses: actions/setup-python@v4 81 | with: 82 | python-version: '3.10' 83 | 84 | - name: Install dependencies 85 | run: | 86 | python -m pip install --upgrade pip 87 | pip install flake8 black isort mypy 88 | 89 | - name: Lint with flake8 90 | run: | 91 | # Stop the build if there are Python syntax errors or undefined names 92 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 93 | # Exit-zero treats all errors as warnings 94 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 95 | continue-on-error: true 96 | 97 | - name: Check formatting with black 98 | run: | 99 | black --check --diff . 100 | continue-on-error: true 101 | 102 | - name: Check import sorting with isort 103 | run: | 104 | isort --check-only --diff . 105 | continue-on-error: true 106 | 107 | security: 108 | runs-on: ubuntu-latest 109 | 110 | steps: 111 | - uses: actions/checkout@v3 112 | 113 | - name: Set up Python 114 | uses: actions/setup-python@v4 115 | with: 116 | python-version: '3.10' 117 | 118 | - name: Install safety 119 | run: | 120 | python -m pip install --upgrade pip 121 | pip install safety 122 | 123 | - name: Run security checks 124 | run: | 125 | # Run safety check on requirements if they exist 126 | if [ -f requirements.txt ]; then 127 | safety check --json 128 | else 129 | echo "No requirements.txt found, skipping safety check" 130 | fi 131 | continue-on-error: true 132 | -------------------------------------------------------------------------------- /tools/vdoninja_simple_websocket_server.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2021 Steve Seguin. All Rights Reserved. 3 | // Use of this source code is governed by the APGLv3 open-source 4 | // 5 | // This is VDO.Ninja-specific handshake server implementation 6 | // It has better routing isolation and performance than a generic fan-out implementation 7 | // 8 | // >> Use at your own risk, as it still may contain bugs or security vunlerabilities << 9 | // 10 | ///// INSTALLATION 11 | // sudo apt-get update 12 | // sudo apt-get upgrade 13 | // sudo apt-get install nodejs -y 14 | // sudo apt-get install npm -y 15 | // sudo npm install express 16 | // sudo npm install ws 17 | // sudo npm install fs 18 | // sudo npm install cors 19 | // sudo add-apt-repository ppa:certbot/certbot 20 | // sudo apt-get install certbot -y 21 | // sudo certbot certonly // register your domain 22 | // sudo nodejs vdoninja.js // port 443 needs to be open. THIS STARTS THE SERVER (or create a service instead) 23 | // 24 | //// Finally, within VDO.Ninja, update index.html of the ninja installation as needed, such as with: 25 | // session.wss = "wss://wss.contribute.cam:443"; 26 | // session.customWSS = true; # Please refer to the vdo.ninja instructions for exact details on settings; this is just a demo. 27 | ///////////////////////// 28 | 29 | "use strict"; 30 | var fs = require("fs"); 31 | var https = require("https"); 32 | var express = require("express"); 33 | var app = express(); 34 | var WebSocket = require("ws"); 35 | var cors = require('cors'); 36 | 37 | const key = fs.readFileSync("/etc/letsencrypt/live/debug.vdo.ninja/privkey.pem"); /// UPDATE THIS PATH 38 | const cert = fs.readFileSync("/etc/letsencrypt/live/debug.vdo.ninja/fullchain.pem"); /// UPDATE THIS PATH 39 | 40 | var server = https.createServer({ key, cert }, app); 41 | var websocketServer = new WebSocket.Server({ server }); 42 | 43 | app.use(cors({ 44 | origin: '*' 45 | })); 46 | 47 | websocketServer.on('connection', (webSocketClient) => { 48 | var room = false; 49 | webSocketClient.on('message', (message) => { 50 | try { 51 | var msg = JSON.parse(message); 52 | } catch (e) { 53 | return; 54 | } 55 | 56 | if (!msg.from) return; 57 | 58 | if (!webSocketClient.uuid) { 59 | let alreadyExists = Array.from(websocketServer.clients).some(client => client.uuid && client.uuid === msg.from && client != webSocketClient); 60 | 61 | if (alreadyExists) { 62 | webSocketClient.send(JSON.stringify({ error: "uuid already in use" })); 63 | return; 64 | } 65 | webSocketClient.uuid = msg.from; 66 | } 67 | 68 | var streamID = false; 69 | 70 | try { 71 | if (msg.request == "seed" && msg.streamID) { 72 | streamID = msg.streamID; 73 | } else if (msg.request == "joinroom") { 74 | room = msg.roomid + ""; 75 | webSocketClient.room = room; 76 | if (msg.streamID) { 77 | streamID = msg.streamID; 78 | } 79 | } 80 | } catch (e) { 81 | return; 82 | } 83 | 84 | if (streamID) { 85 | if (webSocketClient.sid && streamID != webSocketClient.sid) { 86 | webSocketClient.send(JSON.stringify({ error: "can't change sid" })); 87 | return; 88 | } 89 | 90 | let alreadyExists = Array.from(websocketServer.clients).some(client => client.sid && client.sid === streamID && client != webSocketClient); 91 | 92 | if (alreadyExists) { 93 | webSocketClient.send(JSON.stringify({ error: "sid already in use" })); 94 | return; 95 | } 96 | webSocketClient.sid = streamID; 97 | } 98 | 99 | websocketServer.clients.forEach(client => { 100 | if (webSocketClient == client || (msg.UUID && msg.UUID != client.uuid) || (room && (!client.room || client.room !== room)) || (!room && client.room) || (msg.request == "play" && msg.streamID && (!client.sid || client.sid !== msg.streamID))) return; 101 | 102 | client.send(message.toString()); 103 | }); 104 | }); 105 | 106 | webSocketClient.on('close', function(reasonCode, description) {}); 107 | }); 108 | server.listen(443, () => { console.log(`Server started on port 443`) }); 109 | -------------------------------------------------------------------------------- /installers/nvidia_jetson/INSTALL.md: -------------------------------------------------------------------------------- 1 | ## Jetson Nano Media Toolchain Installers 2 | 3 | These scripts target the NVIDIA Jetson Nano 2 GB / 4 GB (Maxwell) platform running JetPack 4.x (L4T 32.7.x) and keep the NVIDIA‐patched `tegra` kernel intact while refreshing the userland media stack in `/usr/local`. 4 | 5 | ### Script Summary 6 | 7 | | Script | Purpose | Typical Use | 8 | | --- | --- | --- | 9 | | `installer.sh` | End‑to‑end rebuild of Python, codec libraries, FFmpeg, GStreamer, libcamera, and supporting deps. Includes optional `INCLUDE_DISTRO_UPGRADE=1` block for legacy rootfs upgrades. | Freshly flashed Jetson Nano image or heavily drifted systems that need a clean rebuild. | 10 | | `toolchain_update.sh` | Thin wrapper around `installer.sh` that keeps `INCLUDE_DISTRO_UPGRADE=0` (skip distro upgrade). | Updating an existing Ubuntu 20.04‑based Jetson image—e.g., the customized system captured in this repo. | 11 | 12 | Both scripts expect `sudo` access (password `ninja` in the lab environment) and will rebuild components under `/usr/local`, leaving the system packages untouched so the NVIDIA kernel and drivers continue to work. 13 | 14 | ### Components Installed 15 | 16 | - **Python 3.11.9** with shared libs, PGO/LTO, and upgraded `pip/setuptools/wheel`. Installs Meson 1.4.1, Ninja 1.13, scikit-build, Jinja2, PyYAML, Mako, ply, websockets, pycairo. 17 | - **Codec libraries** rebuilt from source with multithreaded support: 18 | - `fdk-aac` (master), `dav1d` 1.3.0, `kvazaar` 2.3.0 (CMake shared libs), `x264` stable, `libvpx` 1.13.1 (`--enable-multithread --runtime-cpu-detect`), `srt` 1.5.3 (CMake `ENABLE_SHARED=ON`), `libsrtp` 2.6.0 (shared). 19 | - **FFmpeg n7.0** linked against the refreshed codecs plus PulseAudio, OMX, OpenSSL, SRT/RTMP, and amrwb/amrnb encoders; built shared-only with pthreads. 20 | - **GLib 2.80.5**, **GObject-Introspection 1.80.1**, **usrsctp**, **libnice 0.1.21**, **libsrtp 2.6.0**, **libcamera v0.3.2** (GStreamer support enabled; Python bindings off). 21 | - **GStreamer 1.26.7** via Meson with introspection enabled so GIR/typelib outputs land under `/usr/local` for Python `gi` usage. We continue to disable `libav`, `qt5/qt6`, `rpicamsrc`, `ptp-helper`, VA/MSE plugins to dodge Jetson linker issues while still picking up the WebRTC jitterbuffer retransmission toggles required for RTX. 22 | - **NVIDIA gstnv* plugins** are backed up to `~/nvgst` before the rebuild and restored into `/usr/local/lib/aarch64-linux-gnu/gstreamer-1.0/` after installation to retain hardware acceleration. 23 | 24 | ### When to Use Which Flow 25 | 26 | - **Fresh Jetson Nano (JetPack 4.x) images**: run `installer.sh`. Leave `INCLUDE_DISTRO_UPGRADE=0` unless you explicitly want to attempt the legacy in-place distro upgrade (not recommended for stable setups). 27 | - **Customized Ubuntu 20.04 Jetson image**: run `toolchain_update.sh`. This skips the distro-upgrade block and mirrors the manual rebuild that now targets Python 3.11.9, FFmpeg n7.0, and GStreamer 1.26.7 on the current system. 28 | - **Maintenance updates**: rerun `toolchain_update.sh` after future script tweaks; it reclones/builds everything cleanly each time, ensuring consistent `/usr/local` outputs. 29 | 30 | ### Prerequisites & Warnings 31 | 32 | - Confirm `uname -a` shows `tegra` (NVIDIA kernel). Do **not** install generic Ubuntu kernels. 33 | - Ensure ≥ 40 GB free disk space and several GB of swap (the Nano’s zram helps but long builds benefit from extra swap if available). 34 | - Stable internet is required for git and tarball downloads. 35 | - `/usr/local` takes precedence in `PATH`, `PKG_CONFIG_PATH`, `LD_LIBRARY_PATH`; be mindful of future package installs that might collide. 36 | - GStreamer validator (`gst-validate`) and VA/MSE plugins are disabled by default. If you restore them, ensure the required dependencies (gst-devtools, newer `libva`) are present or the build will fail. 37 | 38 | ### Post-Install Smoke Tests 39 | 40 | After a successful run: 41 | 42 | ```bash 43 | python3.11 --version 44 | python3.11 -m pip list | head 45 | ffmpeg -codecs | grep -E '264|vp9|av1' 46 | gst-launch-1.0 --version 47 | gst-inspect-1.0 nvarguscamerasrc 48 | ``` 49 | 50 | Run representative GStreamer pipelines (e.g., your `publish.py`) to verify multithreaded decode behavior and GPU plugin loading. Investigate any remaining warnings about `gst-validate`, `libgstmse.so`, or `vaCopy`—they indicate optional plugins missing runtime dependencies (typically gst-devtools or a newer VA stack). 51 | 52 | ### Known Limitations 53 | 54 | - GStreamer still omits optional `gst-validate`/MSE/VA plugins; enable them only after providing the required dependencies (gst-devtools, updated libva). 55 | - libcamera warns about kernels < 5.0; this is expected on JetPack 4.x and can be ignored unless you migrate to a newer L4T release. 56 | - The scripts assume a Jetson Nano; other Jetson models may require additional acceleration plugins or kernel headers. 57 | 58 | Refer to `AGENTS.md` for an up-to-date progress log, pending tasks, and validation notes derived from the current deployment. 59 | -------------------------------------------------------------------------------- /.github/RELEASE_PROCESS.md: -------------------------------------------------------------------------------- 1 | # Raspberry Ninja Release Process 2 | 3 | This document describes the automated release system for Raspberry Ninja. 4 | 5 | ## Overview 6 | 7 | Raspberry Ninja uses an intelligent automated release system that: 8 | - Automatically creates releases when significant files are modified 9 | - Analyzes changes to determine appropriate version bumps (major/minor/patch) 10 | - Generates comprehensive release notes 11 | - Updates the changelog 12 | - Creates GitHub releases with tags 13 | 14 | ## Automatic Releases 15 | 16 | ### Trigger Conditions 17 | 18 | Releases are automatically triggered when changes are pushed to the `main` branch that modify: 19 | - `publish.py` - The core streaming script 20 | - `*/installer.sh` - Platform installation scripts 21 | - `*.service` - System service files 22 | - `raspberry_pi/Cargo.toml` - Rust configuration 23 | 24 | ### Version Bump Logic 25 | 26 | The system analyzes changes to determine the appropriate version bump: 27 | 28 | #### Major Version (X.0.0) 29 | - Breaking changes in `publish.py` 30 | - Protocol or format changes 31 | - Large refactoring (>50 lines removed) 32 | - Commit message contains "breaking" or "!:" 33 | 34 | #### Minor Version (0.X.0) 35 | - New features in `publish.py` 36 | - New functions or classes added 37 | - Installation script updates 38 | - Service file changes 39 | - API or streaming changes 40 | - Commit message starts with "feat" 41 | 42 | #### Patch Version (0.0.X) 43 | - Bug fixes 44 | - Performance improvements 45 | - Small changes (<30 lines added) 46 | - Configuration updates 47 | - Commit message starts with "fix", contains "perf", or "refactor" 48 | 49 | ### Skipping Releases 50 | 51 | To skip automatic release for a commit, include one of these in your commit message: 52 | - `[skip-release]` 53 | - `[release]` (already a release commit) 54 | - `[auto-enhanced]` (from the commit enhancement system) 55 | 56 | ## Manual Releases 57 | 58 | You can also trigger a release manually through GitHub Actions: 59 | 60 | 1. Go to Actions → "Manual Release" workflow 61 | 2. Click "Run workflow" 62 | 3. Select the version bump type (patch/minor/major) 63 | 4. Optionally add custom release notes 64 | 5. Click "Run workflow" 65 | 66 | ## Release Contents 67 | 68 | Each release includes: 69 | 70 | ### Version File 71 | - `VERSION` - Contains the current version number (e.g., "1.2.3") 72 | 73 | ### Changelog Entry 74 | - Automatically added to `CHANGELOG.md` 75 | - Includes release date, version, and changes 76 | - Lists all commits since last release 77 | 78 | ### GitHub Release 79 | - Created with tag `vX.Y.Z` 80 | - Includes full release notes 81 | - Links to platform-specific installation guides 82 | - Shows recent commits 83 | 84 | ### Release Notes Format 85 | ```markdown 86 | # Release X.Y.Z 87 | 88 | **Release Date:** YYYY-MM-DD 89 | 90 | ## 🚀 Major Release / ✨ Minor Release / 🐛 Patch Release 91 | 92 | [Description of changes] 93 | 94 | ### Core Changes (publish.py) 95 | [If publish.py was modified] 96 | 97 | ### Commits 98 | - Commit message (hash) 99 | - Commit message (hash) 100 | 101 | ### Installation 102 | [Links to platform guides] 103 | 104 | ### ⚠️ Upgrade Notes 105 | [For major versions only] 106 | ``` 107 | 108 | ## Best Practices 109 | 110 | 1. **Commit Messages**: Use conventional commits (feat:, fix:, chore:, etc.) for better automation 111 | 2. **Testing**: Ensure changes are tested before pushing to main 112 | 3. **Documentation**: Update relevant docs when adding features 113 | 4. **Breaking Changes**: Clearly mark breaking changes in commit messages 114 | 115 | ## Configuration 116 | 117 | ### Required Secrets 118 | - `GITHUB_TOKEN` - Automatically provided by GitHub Actions 119 | - `COMMIT_ENHANCER_PAT` - Personal Access Token for commit enhancement (optional) 120 | 121 | ### Files 122 | - `.github/scripts/auto-release.js` - Main release automation script 123 | - `.github/workflows/auto-release.yml` - Automatic release workflow 124 | - `.github/workflows/manual-release.yml` - Manual release workflow 125 | - `VERSION` - Current version number 126 | - `CHANGELOG.md` - Version history 127 | 128 | ## Troubleshooting 129 | 130 | ### Release Not Triggering 131 | - Check if the modified files are in the trigger list 132 | - Ensure commit doesn't contain skip keywords 133 | - Verify GitHub Actions are enabled 134 | 135 | ### Version Bump Incorrect 136 | - Review the commit message format 137 | - Check the analysis logic in `auto-release.js` 138 | - Use manual release for specific version control 139 | 140 | ### GitHub Release Failed 141 | - Ensure GitHub CLI is available 142 | - Check permissions for the GitHub token 143 | - Verify tag doesn't already exist 144 | 145 | ## Development 146 | 147 | To test the release system locally: 148 | ```bash 149 | # Set required environment variables 150 | export GITHUB_TOKEN="your-token" 151 | 152 | # Run the release script 153 | node .github/scripts/auto-release.js 154 | ``` 155 | 156 | Note: Some features require GitHub Actions environment and won't work locally. -------------------------------------------------------------------------------- /tools/play_hls.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Direct HLS Player 5 | 6 | 11 | 12 | 13 |

Direct HLS Player

14 | 15 | 16 | 17 |
18 | Status: Initializing...
19 | Stream URL: http://localhost:8088/hls/asdfasfsdfgasdf_YAPCDUE808d64_1750667561.m3u8 20 |
21 | 22 |
23 | 24 | 120 | 121 | -------------------------------------------------------------------------------- /installers/orangepi/README.md: -------------------------------------------------------------------------------- 1 | # Orange Pi Installation Guide for Raspberry Ninja 2 | 3 | 4 | 5 | ## Supported Hardware 6 | It is recommended to use Orange Pi 5 and Orange Pi 5 Plus, as other models have not been thoroughly tested yet. 7 | 8 | ## Installation Options 9 | 10 | ### Option 1: Using Pre-built OS Image 11 | There are no preinstalled images specifically for Raspberry Ninja. However, you can download the prebuilt OS from the manufacturer website [orangepi.org](https://orangepi.org) and follow the setup instructions below. 12 | 13 | > **Note:** This guide is based on Debian, but should work with Ubuntu and other Linux distributions. For the Orange Pi 5+, the HDMI input works when using `--raw` as a `publish.py` parameter. If it doesn't work, check if the HDMI input is listed when running `gst-device-monitor-1.0`. Confirmed working with `Armbian_24.2.3_Orangepi5-plus_bookworm_legacy_5.10.160_minimal.img.xz` as of May 10th, 2024. If multiple video devices are connected, use the `--v4l2` parameter to specify which video device ID to use. 14 | 15 | ### Option 2: Setting Up from Scratch 16 | 17 | ## Quick Install Script 18 | 19 | Copy and paste this entire block to install all dependencies and set up Raspberry Ninja: 20 | 21 | ```bash 22 | # Update system 23 | sudo apt update && sudo apt upgrade -y 24 | 25 | # For Debian bookworm, remove EXTERNALLY-MANAGED file (skip if using venv) 26 | sudo rm /usr/lib/python3.11/EXTERNALLY-MANAGED 27 | 28 | # Install Python PIP 29 | sudo apt install python3-pip -y 30 | 31 | # Install required GStreamer libraries 32 | sudo apt-get install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \ 33 | libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good \ 34 | gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav \ 35 | gstreamer1.0-tools gstreamer1.0-x gstreamer1.0-alsa gstreamer1.0-gl \ 36 | gstreamer1.0-gtk3 gstreamer1.0-qt5 gstreamer1.0-pulseaudio \ 37 | gstreamer1.0-nice gstreamer1.0-plugins-base-apps git ninja 38 | 39 | # Install Python dependencies 40 | pip3 install websockets cryptography 41 | 42 | # Install PyGObject dependencies (may be needed for Ubuntu) 43 | sudo apt-get install -y libcairo-dev 44 | sudo apt-get install -y python3-dev cmake libgirepository1.0-dev 45 | 46 | # Use system-provided PyGObject instead of pip version 47 | sudo apt-get install -y python3-gi python3-gi-cairo 48 | 49 | # Clone Raspberry Ninja repository 50 | cd ~ 51 | git clone https://github.com/steveseguin/raspberry_ninja 52 | cd raspberry_ninja 53 | ``` 54 | 55 | ## Running the Software 56 | 57 | Test the stream with colored bars and static noise: 58 | 59 | ```bash 60 | python3 publish.py --test 61 | ``` 62 | 63 | If successful, configure the command-line as needed, removing `--test`, and customizing for your setup. 64 | 65 | ## Camera Configuration 66 | 67 | ### MIPI RK Camera 68 | If using the MIPI RKCamera, edit `publish.py` to use `/dev/video11`. 69 | 70 | ### USB Camera 71 | If using a USB Camera, edit to `/dev/video0`. 72 | 73 | ### HDMI Input 74 | HDMI Input is typically found at `/dev/video1`, but you must first enable HDMI-RX using `orangepi-config`. Once enabled, it works with any HDMI input, even at high resolutions. 75 | 76 | ![Orange Pi 5 Plus with USB webcam](https://github.com/steveseguin/raspberry_ninja/assets/5319910/25934ec7-da3a-4cff-96ac-5a723840caf4) 77 | 78 | ## Setting Up Auto-boot Service 79 | 80 | There's a service file included that sets up the Orange Pi to auto-boot. You'll need to modify it with your preferred settings: 81 | 82 | ```bash 83 | # Edit the service file 84 | cd ~/raspberry_ninja/orangepi 85 | sudo nano raspininja.service # or sudo vi raspininja.service 86 | ``` 87 | 88 | After editing the file with your stream ID and other settings, install and enable the service: 89 | 90 | ```bash 91 | # Install and enable auto-boot service 92 | sudo cp raspininja.service /etc/systemd/system/ 93 | sudo systemctl daemon-reload 94 | sudo systemctl enable raspininja 95 | sudo systemctl restart raspininja 96 | 97 | # Check the service status 98 | sudo systemctl status raspininja 99 | ``` 100 | 101 | The service should now auto-start on system boot and restart if it crashes. 102 | 103 | ## Hardware Encoder/Decoder Setup 104 | 105 | ### Installing Rockchip Hardware H.265 Encoding Support 106 | 107 | To enable hardware-accelerated H.265 (HEVC) encoding on your Orange Pi 5/5+, follow these steps: 108 | 109 | ```bash 110 | # 1. Install the Rockchip MPP (Media Process Platform) libraries 111 | cd ~ 112 | git clone https://github.com/rockchip-linux/mpp.git 113 | cd mpp 114 | mkdir build 115 | cd build 116 | cmake -DRKPLATFORM=ON -DHAVE_DRM=ON .. 117 | make -j4 118 | sudo make install 119 | 120 | # 2. Install the Rockchip GStreamer plugins 121 | cd ~ 122 | git clone https://github.com/Meonardo/gst-rockchip.git 123 | cd gst-rockchip 124 | meson setup build 125 | cd build 126 | ninja 127 | sudo ninja install 128 | 129 | # 3. Refresh library paths 130 | sudo ldconfig 131 | ``` 132 | 133 | After installation, verify the plugins are available: 134 | 135 | ```bash 136 | gst-inspect-1.0 rockchipmpp 137 | ``` 138 | 139 | You should see `mpph265enc` and `mpph264enc` listed among the available elements. 140 | 141 | if not, maybe try: 142 | 143 | ``` 144 | sudo cp /usr/local/lib/aarch64-linux-gnu/gstreamer-1.0/* /usr/lib/aarch64-linux-gnu/gstreamer-1.0 145 | sudo ldconfig 146 | ``` 147 | -------------------------------------------------------------------------------- /convert_to_numpy_examples/readme.md: -------------------------------------------------------------------------------- 1 | ## WebRTC -> Numpy (ML/CV/AI/MJPEG) 2 | 3 | One of the more powerful aspects of Python is Numpy; it's a powerful matrix library for data and image processing. 4 | 5 | Data science, machine learning, and computer vision software packages are typically built on Numpy, and it's perhaps one of the core reasons for Python's popularity. While hard core developers might prefer C++, data-scientists, researchers, and students will likely be using SciPy, Pandas, OpenCV, PyTorch, or any of the many other highly-performant toolkits available for Python instead. 6 | 7 | Raspberry.Ninja includes in it the ability to pull webRTC streams from VDO.Ninja, and convert them into raw-uncompressed video frames, which can then be used for computer vision, machine learning, or even to simply host as a simple motion jpeg stream. 8 | 9 | YouTube Video demo and walk-thru of this code: https://www.youtube.com/watch?v=LGaruUjb8dg 10 | 11 | ### Initial installation 12 | 13 | Follow the installation requirements for Raspberry Ninja as normal, except you'll also need: 14 | 15 | ``` 16 | pip3 install numpy 17 | ``` 18 | and if using Pillow, to follow along with Youtube video, you can install that pretty easily: 19 | ``` 20 | python3 -m pip install --upgrade pip 21 | python3 -m pip install --upgrade Pillow 22 | ``` 23 | 24 | If wanting to use OpenCV, on some operating systems it's as easy as: 25 | ``` 26 | pip3 install opencv-python 27 | ``` 28 | However, sometimes OpenCV isn't available and you will be required compiling. I'm not really sure how to install OpenCV via WSL though; you might need to build OpenCV yourself in that case. Google is your friend in these cases. 29 | 30 | If using Windows OS (not WSL), you can try the the cv2 binary as a pip wheel via: https://www.lfd.uci.edu/~gohlke/pythonlibs/#opencv instead. 31 | 32 | OpenCV isn't required to follow along with the YouTube video though. 33 | 34 | ### how to setup publish.py in frame buffer mode 35 | 36 | To configure Raspberry.Ninja to pull a stream, you can you use the following command: 37 | ```python3 publish.py --framebuffer STREAMIDHERE123 --h264 --noaudio``` 38 | 39 | `--framebuffer` tells the code that we are viewing a remote stream, with the specified stream ID; the goal is to make the video frames available in a shared memory buffer. Audio can be supported, but at present it's video only, so specifying `--noaudio` can conserve some bandwidth if used. `--h264` is likely to be more compatible, however vp8 might also work. 40 | 41 | While there are ways to modify the `publish.py` code to directly make use of the incoming compressed or decompressed video streams, `--framebuffer` chooses to push as raw video frames to a shared memory buffer, so the frames can be accessible by any other Python script running on the system. If you wanted to share the raw video frames with another local system, I'd probably suggest pushing frames via a UDP socket or pushing to a Redis server; compressing to JPEG/PNG first would help with bandwidth. Since we are using a shared memory buffer in this case, we will just leave the frames uncompressed and raw. There's no need to modify the `publish.py` file this way either, so long as you intend to run your own code from another Python script locally. 42 | 43 | ### simple example; how to access the raw videos frames in your own code now 44 | 45 | In this folder there is a basic reciever example of how to read the frames from the shared memory buffer from a small Python script. The core logic though is as follows: 46 | ``` 47 | # we assume publish.py --framebuffer is running already on the same computer 48 | shm = shared_memory.SharedMemory(name="psm_raspininja_streamid") # open the shared memory 49 | frame_buffer = np.ndarray(shm.size, dtype=np.uint8, buffer=shm.buf) # read from the shared memory 50 | frame_array = np.frombuffer(frame_buffer, dtype=np.uint8) # .. 51 | 52 | meta_header = frame_array[0:5] ## the first 5-words (10-bytes) store width, height, and frame ID 53 | width = meta_header[0]*255+meta_header[1] 54 | height = meta_header[2]*255+meta_header[3] 55 | frame_array = frame_array[5:5+width*height*3].reshape((height,width,3)) # remove meta data, crop to actual image size, and re-shape 1D -> 2.5D 56 | ``` 57 | 58 | So we access the shared memory, specified with a given name set by the running publish.py script, and then we read the entire shared memory buffer. Since our current image frame might not use up the entire buffer, we include meta-header data to help us know what parts of the shared memory we want to keep or ignore. We now have our raw image data in a numpy array, ready to use however we want. 59 | 60 | ### advanced example; host numpy images as mjpeg web stream 61 | 62 | There's a second example file also provided, which just takes the basic recieve concept to the next level. This more advanced script converts the incoming raw frame into a JPEG image, and hosts it as a motion-jpg stream on a local http webserver (`http://x.x.x.x:81`). This allows you to visualize the video frames on a headless remote system via your browser, without needing to deal with added complexities like gstreamer, ssl, webrtc, or other. Very simple, and at 640x360 or lower resolutions, it's also extremely low-latency. In fact, the `--framebuffer` mode, and provided code, is optimized for low-latency. The system will drop video frames if newer frames become available, keeping the latency as low as possible. 63 | 64 | The advanced code example also includes some concepts like Events and Socket messaging also, but you can use Redis or other approach of your chosing as well. 65 | 66 | If looking for another advanced example of what's possible, I have another similar project from several years ago now, hosted at: https://github.com/ooblex/ooblex. The project investigated applying deepfake detection to a webRTC video stream, by actually applying a deepfake TF model to an incoming webRTC stream, and then hosting the resulting deep faked live video. While that project is fairly dusty at this point, it still might offer you some fresh ideas. 67 | -------------------------------------------------------------------------------- /installers/nvidia_jetson/README.md: -------------------------------------------------------------------------------- 1 | # Raspberry Ninja on NVIDIA Jetson 2 | 3 | The Jetson Nano/NX/AGX can handle Raspberry Ninja well (1080p30 is easy compared to a Pi). These notes target JetPack 4.x images and keep the NVIDIA `tegra` kernel intact while updating the userland media stack under `/usr/local`. 4 | 5 | ## Pick the right script 6 | 7 | Most users on the provided pre-built image only need to refresh code and dependencies: 8 | 9 | ``` 10 | cd ~/raspberry_ninja 11 | git pull 12 | cd installers/nvidia_jetson 13 | chmod +x installer.sh toolchain_update.sh setup_autostart.sh 14 | ./toolchain_update.sh 15 | ``` 16 | 17 | Run `installer.sh` when you are starting from a clean official JetPack image or repairing a badly broken system: 18 | 19 | ``` 20 | cd ~/raspberry_ninja/installers/nvidia_jetson 21 | chmod +x installer.sh 22 | sudo ./installer.sh # heavy build; expect several hours 23 | # Optional legacy path if you really want an in-place distro upgrade: 24 | # INCLUDE_DISTRO_UPGRADE=1 sudo ./installer.sh 25 | ``` 26 | 27 | Both flows need sudo, a stable internet connection, and plenty of free space (40 GB+ is safer). Expect to babysit long builds and rerun steps if network mirrors flake out. 28 | 29 | ## Disable screen blanking and set up autostart 30 | 31 | `setup_autostart.sh` can disable DPMS/console blanking, prompt for your `publish.py` command, and create a systemd service: 32 | 33 | ``` 34 | cd ~/raspberry_ninja/installers/nvidia_jetson 35 | sudo ./setup_autostart.sh 36 | ``` 37 | 38 | You can rerun it to toggle screen-blanking later; only that step needs sudo. 39 | 40 | ## Installing from an official NVIDIA image 41 | 42 | The official JetPack images include the encoder/decoder bits we rely on. The installer script builds newer GStreamer and friends on top of that base. Run sections of `installer.sh` manually if you hit errors; newer GStreamer versions sometimes require tweaks. 43 | 44 | Building from scratch takes hours. You can skip optional steps (SRT/FFmpeg extras) if you want a faster but less feature-complete setup. 45 | 46 | ## Using the pre-built image 47 | 48 | Latest pre-setup Jetson images (needs 16 GB uSD or larger and up-to-date firmware): 49 | 50 | - Download (updated October 2025): https://drive.google.com/file/d/1B_ywphXQ49F9we3ytcM-Zn1h7dCYOLBh/view?usp=share_link 51 | - Works on Jetson Nano 2GB A02; should also work on Nano 4GB with current firmware. 52 | - Includes GStreamer 1.26.7 with libcamera, SRT, RTMP, FFmpeg, hardware encode, and AV1 support. 53 | - Image is shrunk to ~15 GB (about 7 GB zipped); 32 GB cards are recommended. 54 | 55 | After flashing, expand the root partition to use the full SD card (example for `/dev/mmcblk0`): 56 | 57 | ``` 58 | sudo growpart /dev/mmcblk0 1 # grows partition 1 to fill the card 59 | sudo resize2fs /dev/mmcblk0p1 # expands the ext4 filesystem 60 | ``` 61 | 62 | If your device enumerates as `/dev/sda`, replace `mmcblk0` with `sda`. You can also do this via GNOME Disks (“Resize…”) if you prefer a GUI. 63 | 64 | Flash with Win32DiskImager (or balenaEtcher). Default credentials: 65 | 66 | ``` 67 | username: ninja 68 | password: vdo 69 | ``` 70 | 71 | Chromium may not be installed on older builds. 72 | 73 | After flashing, pull the latest code and refresh dependencies: 74 | 75 | ``` 76 | sudo rm -r raspberry_ninja 2>/dev/null || true 77 | git clone https://github.com/steveseguin/raspberry_ninja 78 | cd raspberry_ninja/installers/nvidia_jetson 79 | sudo ./installer.sh 80 | ``` 81 | 82 | ## Missing NVIDIA GStreamer plugins? 83 | 84 | If you are not on an official JetPack image (or something wiped the NVIDIA plugins), grab `libgstnvidia.zip` from this repo and restore it after running the installer: 85 | 86 | - https://github.com/steveseguin/raspberry_ninja/raw/refs/heads/main/installers/nvidia_jetson/libgstnvidia.zip 87 | - After the installer finishes, extract/copy the contents into `/usr/local/lib/aarch64-linux-gnu/gstreamer-1.0/`. 88 | - You can also pre-copy them to `/usr/lib/aarch64-linux-gnu/gstreamer-1.0/` before running the script and hope detection picks them up. 89 | 90 | ## Official NVIDIA images 91 | 92 | Official downloads: https://developer.nvidia.com/embedded/downloads (Jetson Nano images ship with Ubuntu 18 and GStreamer 1.14; we recommend upgrading GStreamer to at least 1.16+ with the installer script). 93 | 94 | After flashing the image and logging in: 95 | 96 | ``` 97 | git clone https://github.com/steveseguin/raspberry_ninja/ 98 | cd raspberry_ninja/installers/nvidia_jetson 99 | chmod +x installer.sh 100 | sudo ./installer.sh 101 | ``` 102 | 103 | ## Auto-start service on boot 104 | 105 | You can adapt the Raspberry Pi service file at `installers/raspberry_pi/raspininja.service` if you prefer a manual service setup: https://github.com/steveseguin/raspberry_ninja/tree/main/installers/raspberry_pi#setting-up-auto-boot 106 | 107 | If you copy it, consider changing or removing: 108 | ``` 109 | User=vdo 110 | Group=vdo 111 | Environment=XDG_RUNTIME_DIR=/run/user/1000 112 | ExecStartPre=vcgencmd get_camera 113 | ``` 114 | 115 | `setup_autostart.sh` already handles these tweaks for Jetson users, so start there unless you need a custom service. 116 | 117 | ## Details on NVIDIA's GStreamer implementation 118 | 119 | Docs on the encoder and pipeline options: https://docs.nvidia.com/jetson/l4t/index.html#page/Tegra%20Linux%20Driver%20Package%20Development%20Guide/accelerated_gstreamer.html#wwpID0E0A40HA 120 | 121 | ![image](https://user-images.githubusercontent.com/2575698/127804472-073ce656-babc-450a-a7a5-754493ad1fd8.png) 122 | ![image](https://user-images.githubusercontent.com/2575698/127804558-1560ad4d-6c2a-4791-92ca-ca50d2eacc2d.png) 123 | 124 | ## Problems and troubleshooting 125 | 126 | - **"START PIPE" but nothing happens** – make sure the correct video device is set (`video0`, `video1`, etc.). `gst-device-monitor-1.0` lists available devices. 127 | - **Other capture issues** – confirm the camera supports MJPEG (or adapt the pipeline accordingly). 128 | - **`nvjpegdec` not found** – ensure the NVIDIA GStreamer plugins are present (see `libgstnvidia.zip` above) and that you ran the installer script successfully. 129 | 130 | ### Updating firmware on Jetson Nano 131 | 132 | Nano 2GB/4GB dev kits may need firmware updates for newer images. Updating requires Ubuntu 18 with JetPack 4 installed and can take a couple of hours. Once updated, 2GB and 4GB images are usually compatible. 133 | -------------------------------------------------------------------------------- /convert_to_numpy_examples/adv_cv2_jpeg.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | import ssl 4 | import sys 5 | import os 6 | try: 7 | import cv2 ## PIL or CV2 can be used: $ pip3 install opencv-python 8 | except: 9 | print("OpenCV wasn't found (pip3 install opencv-python). Trying to use Pillow instead") 10 | from PIL import Image 11 | from io import BytesIO 12 | from socketserver import ThreadingMixIn 13 | from http.server import BaseHTTPRequestHandler, HTTPServer 14 | import socket 15 | import multiprocessing 16 | import numpy as np 17 | import time 18 | from multiprocessing import shared_memory, Lock 19 | from multiprocessing.resource_tracker import unregister 20 | from threading import Event 21 | 22 | global last_frame, jpeg,promise 23 | promise = Event() 24 | 25 | def read_shared_memory(): 26 | global last_frame, jpeg, promise 27 | 28 | try: 29 | shm = False 30 | trigger_socket = False 31 | trigger_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 32 | trigger_socket.bind(("127.0.0.1", 12345)) 33 | 34 | shm_name = "psm_raspininja_streamid" 35 | shm = shared_memory.SharedMemory(name=shm_name) 36 | frame_buffer = np.ndarray(shm.size, dtype=np.uint8, buffer=shm.buf) 37 | unregister(shm._name, 'shared_memory') # https://forums.raspberrypi.com/viewtopic.php?t=340441#p2039792 38 | last_frame = -1 39 | frame = 0 40 | failed = 0 41 | 42 | prev_array = False 43 | 44 | while True: 45 | trigger_signal = trigger_socket.recv(1024) # wait for some alert that the shared memory was updated 46 | frame_data = frame_buffer.copy() # Make a copy to avoid modifying the shared memory 47 | frame_array = np.frombuffer(frame_data, dtype=np.uint8) 48 | meta_header = frame_array[0:5] 49 | frame = meta_header[4] 50 | if frame == last_frame: 51 | failed = failed + 1 52 | if failed > 5: 53 | print("this is unexpected; let's reconnect") 54 | break 55 | failed = 0 56 | width = meta_header[0]*255+meta_header[1] 57 | if width==0: 58 | print("image size is 0. will retry") 59 | time.sleep(1) 60 | continue 61 | height = meta_header[2]*255+meta_header[3] 62 | 63 | frame_array = frame_array[5:5+width*height*3].reshape((height,width,3)) 64 | 65 | # if type(prev_array) == type(False): 66 | # prev_array = frame_array 67 | # modded_array = frame_array 68 | # if np.shape(frame_array) == np.shape(prev_array): 69 | # modded_array = frame_array%128+128 - prev_array%128 70 | # prev_array = frame_array 71 | 72 | try: # Try OpenCV 73 | _, jpeg = cv2.imencode('.jpeg', frame_array) 74 | except: # Try Pillow if CV2 isnt' there 75 | with BytesIO() as f: 76 | im = Image.fromarray(frame_array[:, :, ::-1]) # Convert from numpy/cv2 BGR to pillow RGB (OpenCV to PIL format) 77 | im.save(f, format='JPEG') 78 | jpeg = f.getvalue() 79 | 80 | last_frame = frame 81 | if not promise.is_set(): # let any waiting thread know we are done 82 | promise.set() 83 | print(np.shape(frame_array),frame, frame_array[0, 0, :]) 84 | except Exception as E: 85 | print(E) 86 | finally: 87 | if shm: 88 | shm.close() 89 | if trigger_socket: 90 | trigger_socket.close() 91 | return True 92 | 93 | 94 | class myHandler(BaseHTTPRequestHandler): 95 | def do_GET(self): 96 | global jpeg, last_frame, promise 97 | print("Web connect") 98 | 99 | try: 100 | videotype = self.path.split(".")[1].split("?")[0] 101 | except: 102 | videotype = "html" 103 | 104 | if videotype == 'mjpg': 105 | self.send_response(200) 106 | self.send_header('Content-type','multipart/x-mixed-replace; boundary=--jpgboundary') 107 | self.end_headers() 108 | sent_frame = -1 109 | while True: 110 | try: 111 | 112 | while last_frame == sent_frame: # if we have multiple viewers, can see if already done this way 113 | if not promise.is_set(): 114 | promise.wait(25) # under 30s, incase there's a timeout on some CDN? 115 | else: 116 | promise.clear # just in case last-frame == sent-frame is incorrect, we won't get stuck this way. 117 | 118 | sent_frame = last_frame 119 | 120 | self.wfile.write("--jpgboundary".encode('utf-8')) 121 | self.send_header('Content-type','image/jpeg') 122 | self.send_header('Content-length',str(len(jpeg))) 123 | self.end_headers() 124 | self.wfile.write(jpeg) 125 | 126 | # promise.clear() # we 127 | except KeyboardInterrupt: 128 | break 129 | return 130 | else: 131 | self.send_response(200) 132 | self.send_header('Content-type','text/html') 133 | self.end_headers() 134 | self.wfile.write(''.encode('utf-8')) 135 | self.wfile.write(('').encode('utf-8')) 136 | self.wfile.write(''.encode('utf-8')) 137 | return 138 | 139 | class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): 140 | """Handle requests in a separate thread.""" 141 | pass 142 | 143 | def remote_threader(): 144 | remote_Server = ThreadedHTTPServer(("", 81), myHandler) 145 | #remote_Server.socket = ssl.wrap_socket(remote_Server.socket, keyfile=key_pem, certfile=chain_pem, server_side=True) ## if you need SSL, you can use certbot to get free valid keys 146 | remote_Server.serve_forever() 147 | print("webserver ending") 148 | 149 | if __name__ == "__main__": 150 | remote_http_thread = threading.Thread(target=remote_threader) 151 | remote_http_thread.start() 152 | while read_shared_memory(): 153 | print("reloading in a second") 154 | time.sleep(1) 155 | print("processing server ending") 156 | -------------------------------------------------------------------------------- /templates/recording.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Recording 5 | 67 | 68 | 69 |
70 |

Recording in progress...

71 |

Room: {{ room }}

72 |

Record ID: {{ record }}

73 |
74 |

processPid: {{ process_pid }}

75 |
76 | 77 | 83 |
84 | 85 |
86 |
87 |
88 | 89 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /ndi/readme.md: -------------------------------------------------------------------------------- 1 | # Universal NDI SDK Installer for Linux 2 | 3 | A comprehensive installer script that sets up NDI (Network Device Interface) SDK support on Linux systems, including automatic Rust/Cargo installation and GStreamer NDI plugin compilation. 4 | 5 | ## Features 6 | 7 | - 🌍 **Universal Linux Support** - Works on Ubuntu, Debian, CentOS, RHEL, Fedora, openSUSE, Arch, Alpine, and more 8 | - 🦀 **Automatic Rust Installation** - Detects and installs/updates Rust and Cargo as needed 9 | - 🏗️ **Multi-Architecture Support** - Supports x86_64, ARM64 (aarch64), and ARM32 architectures 10 | - 📦 **Smart Package Management** - Uses the appropriate package manager for your distribution 11 | - 🔧 **GStreamer Integration** - Builds and installs the GStreamer NDI plugin 12 | - ✅ **Verification Testing** - Tests the installation to ensure everything works 13 | 14 | ## Supported Distributions 15 | 16 | | Distribution | Package Manager | Tested | 17 | |--------------|----------------|---------| 18 | | Ubuntu 18.04+ | apt | ✅ | 19 | | Debian 9+ | apt | ✅ | 20 | | CentOS 7+ | yum/dnf | ✅ | 21 | | RHEL 7+ | yum/dnf | ✅ | 22 | | Fedora 30+ | dnf | ✅ | 23 | | openSUSE | zypper | ✅ | 24 | | Arch Linux | pacman | ✅ | 25 | | Alpine Linux | apk | ✅ | 26 | | Other distros | manual | ⚠️ | 27 | 28 | ## Prerequisites 29 | 30 | - Linux system with internet connection 31 | - `sudo` privileges 32 | - `curl` and `git` (will be installed if missing) 33 | - Basic development tools (will be installed automatically) 34 | 35 | ## Quick Start 36 | 37 | ### Download and Run 38 | 39 | ```bash 40 | # Download the installer 41 | curl -LO https://raw.githubusercontent.com/steveseguin/ndi-installer/main/install_ndi.sh 42 | 43 | # Make it executable 44 | chmod +x install_ndi.sh 45 | 46 | # Run the installer 47 | ./install_ndi.sh 48 | ``` 49 | 50 | ### One-Line Installation 51 | 52 | ```bash 53 | curl -sSf https://raw.githubusercontent.com/steveseguin/ndi-installer/main/install_ndi.sh | bash 54 | ``` 55 | 56 | ## What Gets Installed 57 | 58 | ### Core Components 59 | - **NDI SDK v6** - The latest NDI Software Development Kit 60 | - **NDI Runtime Libraries** - Required for NDI functionality 61 | - **Development Headers** - For building NDI applications 62 | 63 | ### Development Tools 64 | - **Rust Toolchain** - Latest stable Rust compiler and Cargo package manager 65 | - **Build Dependencies** - GCC, CMake, pkg-config, and other development tools 66 | - **GStreamer Development** - Headers and libraries for GStreamer plugin development 67 | 68 | ### GStreamer Plugin 69 | - **NDI Plugin** - Compiled from [gst-plugin-ndi](https://github.com/steveseguin/gst-plugin-ndi) 70 | - **Plugin Installation** - Automatically installed to system GStreamer plugin directory 71 | - **Compatibility Links** - Creates necessary symlinks for older applications 72 | 73 | ## Architecture Support 74 | 75 | The installer automatically detects your system architecture: 76 | 77 | - **x86_64** - Standard 64-bit Intel/AMD processors 78 | - **aarch64/arm64** - 64-bit ARM processors (Raspberry Pi 4, Apple M1, etc.) 79 | - **armv7l/armhf** - 32-bit ARM processors (older Raspberry Pi models) 80 | 81 | ## Post-Installation 82 | 83 | After successful installation, you may need to: 84 | 85 | 1. **Restart your terminal** or run: 86 | ```bash 87 | source ~/.cargo/env 88 | ``` 89 | 90 | 2. **Verify the installation**: 91 | ```bash 92 | # Check Rust 93 | rustc --version 94 | cargo --version 95 | 96 | # Check GStreamer NDI plugin 97 | gst-inspect-1.0 ndi 98 | 99 | # Check NDI libraries 100 | ldconfig -p | grep ndi 101 | ``` 102 | 103 | 3. **Test NDI functionality** with your applications 104 | 105 | ## Usage Examples 106 | 107 | ### GStreamer Pipeline with NDI 108 | 109 | ```bash 110 | # NDI source 111 | gst-launch-1.0 ndisrc ! videoconvert ! autovideosink 112 | 113 | # NDI sink 114 | gst-launch-1.0 videotestsrc ! ndisink 115 | ``` 116 | 117 | ### Building Rust Applications with NDI 118 | 119 | ```toml 120 | # Cargo.toml 121 | [dependencies] 122 | ndi = "0.2" 123 | ``` 124 | 125 | ## Troubleshooting 126 | 127 | ### Common Issues 128 | 129 | **Q: Installation fails with "permission denied"** 130 | A: Ensure you have `sudo` privileges and the script is executable (`chmod +x install_ndi.sh`) 131 | 132 | **Q: GStreamer plugin not found after installation** 133 | A: Try running `sudo ldconfig` and restart your terminal. Check plugin location with `gst-inspect-1.0 --print-plugin-auto-install-info` 134 | 135 | **Q: NDI libraries not found** 136 | A: Verify installation with `ldconfig -p | grep ndi`. You may need to add `/usr/local/lib` to your library path. 137 | 138 | **Q: Rust installation fails** 139 | A: The script uses rustup for installation. If it fails, try installing rustup manually: `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` 140 | 141 | ### Debug Mode 142 | 143 | Run with debug output: 144 | ```bash 145 | bash -x ./install_ndi.sh 146 | ``` 147 | 148 | ### Manual Verification 149 | 150 | Check installed components: 151 | ```bash 152 | # NDI SDK 153 | ls -la /usr/local/lib/*ndi* 154 | 155 | # GStreamer plugin 156 | find /usr -name "*gstndi*" 2>/dev/null 157 | 158 | # Rust toolchain 159 | which rustc cargo 160 | ``` 161 | 162 | ## Advanced Configuration 163 | 164 | ### Custom Installation Paths 165 | 166 | The script uses standard system paths, but you can modify these variables at the top of the script: 167 | 168 | ```bash 169 | # Custom NDI version 170 | NDI_VERSION="6" 171 | 172 | # Custom library architecture path 173 | LIB_ARCH="x86_64-linux-gnu" 174 | 175 | # Custom GStreamer plugin directory 176 | GST_LIB_DIR="/usr/lib/x86_64-linux-gnu/gstreamer-1.0" 177 | ``` 178 | 179 | ### Offline Installation 180 | 181 | For systems without internet access: 182 | 183 | 1. Download NDI SDK manually from [NDI Downloads](https://ndi.tv/sdk/) 184 | 2. Download Rust installer: `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup-init.sh` 185 | 3. Clone GStreamer plugin: `git clone https://github.com/steveseguin/gst-plugin-ndi.git` 186 | 4. Modify script paths accordingly 187 | 188 | ## System Requirements 189 | 190 | ### Minimum 191 | - 2GB RAM 192 | - 1GB free disk space 193 | - Linux kernel 3.10+ 194 | - glibc 2.17+ 195 | 196 | ### Recommended 197 | - 4GB+ RAM 198 | - 5GB+ free disk space 199 | - Linux kernel 4.15+ 200 | - Recent distribution (last 3 years) 201 | 202 | ## Security Considerations 203 | 204 | This installer: 205 | - Downloads software from official sources (NDI, Rust, GitHub) 206 | - Uses HTTPS for all downloads 207 | - Verifies installations before proceeding 208 | - Only requests necessary sudo permissions 209 | 210 | Always review scripts before running with elevated privileges. 211 | 212 | ## Contributing 213 | 214 | Contributions are welcome! Please: 215 | 216 | 1. Fork the repository 217 | 2. Create a feature branch 218 | 3. Test on multiple distributions 219 | 4. Submit a pull request 220 | 221 | ### Testing 222 | 223 | Test the installer on different distributions using Docker: 224 | 225 | ```bash 226 | # Ubuntu 227 | docker run -it ubuntu:20.04 bash 228 | # ... install and test 229 | 230 | # Fedora 231 | docker run -it fedora:latest bash 232 | # ... install and test 233 | ``` 234 | -------------------------------------------------------------------------------- /.github/workflows/manual-release.yml: -------------------------------------------------------------------------------- 1 | name: Manual Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version_bump: 7 | description: 'Version bump type' 8 | required: true 9 | default: 'minor' 10 | type: choice 11 | options: 12 | - patch 13 | - minor 14 | - major 15 | release_notes: 16 | description: 'Additional release notes (optional)' 17 | required: false 18 | type: string 19 | 20 | jobs: 21 | create-release: 22 | runs-on: ubuntu-latest 23 | permissions: 24 | contents: write 25 | packages: write 26 | 27 | steps: 28 | - name: Checkout code 29 | uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 0 32 | token: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | - name: Setup Node.js 35 | uses: actions/setup-node@v4 36 | with: 37 | node-version: '18' 38 | 39 | - name: Configure Git 40 | run: | 41 | git config --global user.name "GitHub Actions Release Bot" 42 | git config --global user.email "actions@github.com" 43 | 44 | - name: Install GitHub CLI 45 | run: | 46 | type gh >/dev/null 2>&1 || ( 47 | curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo gpg --dearmor -o /usr/share/keyrings/githubcli-archive-keyring.gpg 48 | echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null 49 | sudo apt install gh -y 50 | ) 51 | 52 | - name: Create manual release script 53 | run: | 54 | cat > manual-release.js << 'EOF' 55 | const { exec } = require('child_process'); 56 | const fs = require('fs').promises; 57 | 58 | async function runCommand(command) { 59 | return new Promise((resolve, reject) => { 60 | exec(command, { maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => { 61 | if (error) { 62 | reject(new Error(`Command failed: ${command}\n${stderr}`)); 63 | return; 64 | } 65 | resolve(stdout.trim()); 66 | }); 67 | }); 68 | } 69 | 70 | async function main() { 71 | const bumpType = process.env.BUMP_TYPE; 72 | const additionalNotes = process.env.ADDITIONAL_NOTES || ''; 73 | 74 | // Read current version 75 | let currentVersion = '1.0.0'; 76 | try { 77 | currentVersion = (await fs.readFile('VERSION', 'utf8')).trim(); 78 | } catch (error) { 79 | console.log('VERSION file not found, using 1.0.0'); 80 | } 81 | 82 | // Parse and bump version 83 | const [major, minor, patch] = currentVersion.split('.').map(Number); 84 | let newVersion; 85 | 86 | switch (bumpType) { 87 | case 'major': 88 | newVersion = `${major + 1}.0.0`; 89 | break; 90 | case 'minor': 91 | newVersion = `${major}.${minor + 1}.0`; 92 | break; 93 | case 'patch': 94 | newVersion = `${major}.${minor}.${patch + 1}`; 95 | break; 96 | } 97 | 98 | console.log(`Bumping version: ${currentVersion} → ${newVersion}`); 99 | 100 | // Update VERSION file 101 | await fs.writeFile('VERSION', newVersion); 102 | 103 | // Generate release notes 104 | const releaseDate = new Date().toISOString().split('T')[0]; 105 | let releaseNotes = `# Release ${newVersion}\n\n`; 106 | releaseNotes += `**Release Date:** ${releaseDate}\n\n`; 107 | releaseNotes += `**Release Type:** Manual ${bumpType} release\n\n`; 108 | 109 | if (additionalNotes) { 110 | releaseNotes += `## Release Notes\n\n${additionalNotes}\n\n`; 111 | } 112 | 113 | // Get recent commits 114 | try { 115 | const lastTag = await runCommand('git describe --tags --abbrev=0 2>/dev/null || echo ""'); 116 | if (lastTag) { 117 | const commits = await runCommand(`git log ${lastTag}..HEAD --pretty=format:"- %s (%h)" --no-merges`); 118 | if (commits) { 119 | releaseNotes += `## Commits since ${lastTag}\n\n${commits}\n\n`; 120 | } 121 | } 122 | } catch (error) { 123 | console.log('Could not get commit history'); 124 | } 125 | 126 | // Update CHANGELOG 127 | let changelog = '# Changelog\n\nAll notable changes to Raspberry Ninja will be documented in this file.\n\n'; 128 | try { 129 | changelog = await fs.readFile('CHANGELOG.md', 'utf8'); 130 | } catch (error) { 131 | console.log('Creating new CHANGELOG.md'); 132 | } 133 | 134 | const headerMatch = changelog.match(/^#\s+Changelog.*?\n+/m); 135 | if (headerMatch) { 136 | const insertPosition = headerMatch.index + headerMatch[0].length; 137 | changelog = 138 | changelog.slice(0, insertPosition) + 139 | releaseNotes + '\n---\n\n' + 140 | changelog.slice(insertPosition); 141 | } else { 142 | changelog = changelog + '\n\n' + releaseNotes; 143 | } 144 | 145 | await fs.writeFile('CHANGELOG.md', changelog); 146 | 147 | // Commit changes 148 | await runCommand('git add VERSION CHANGELOG.md'); 149 | await runCommand(`git commit -m "chore: release v${newVersion} [release]"`); 150 | await runCommand('git push'); 151 | 152 | // Create tag and release 153 | const tagName = `v${newVersion}`; 154 | await runCommand(`git tag -a ${tagName} -m "Release ${newVersion}"`); 155 | await runCommand(`git push origin ${tagName}`); 156 | 157 | // Create GitHub release 158 | const tempFile = `.release-notes-${Date.now()}.tmp`; 159 | await fs.writeFile(tempFile, releaseNotes); 160 | 161 | try { 162 | await runCommand(`gh release create ${tagName} --title "Release ${newVersion}" --notes-file ${tempFile}`); 163 | console.log(`GitHub release created: ${tagName}`); 164 | } catch (error) { 165 | console.log('Could not create GitHub release:', error.message); 166 | } 167 | 168 | await fs.unlink(tempFile); 169 | } 170 | 171 | main().catch(console.error); 172 | EOF 173 | 174 | - name: Run manual release 175 | env: 176 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 177 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 178 | BUMP_TYPE: ${{ inputs.version_bump }} 179 | ADDITIONAL_NOTES: ${{ inputs.release_notes }} 180 | run: node manual-release.js -------------------------------------------------------------------------------- /ndi/install_ndi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | echo "Universal NDI support installer for Linux systems" 5 | echo "Supports Ubuntu, Debian, CentOS, RHEL, Fedora, openSUSE, Arch, and more" 6 | 7 | NDI_VERSION="6" 8 | NDI_INSTALLER="Install_NDI_SDK_v${NDI_VERSION}_Linux.tar.gz" 9 | NDI_URL="https://downloads.ndi.tv/SDK/NDI_SDK_Linux/${NDI_INSTALLER}" 10 | TMP_DIR=$(mktemp -d) 11 | 12 | # Detect architecture 13 | ARCH=$(uname -m) 14 | case $ARCH in 15 | x86_64) 16 | LIB_ARCH="x86_64-linux-gnu" 17 | GST_LIB_DIR="/usr/lib/x86_64-linux-gnu/gstreamer-1.0" 18 | ;; 19 | aarch64|arm64) 20 | LIB_ARCH="aarch64-linux-gnu" 21 | GST_LIB_DIR="/usr/lib/aarch64-linux-gnu/gstreamer-1.0" 22 | ;; 23 | armv7l|armhf) 24 | LIB_ARCH="arm-linux-gnueabihf" 25 | GST_LIB_DIR="/usr/lib/arm-linux-gnueabihf/gstreamer-1.0" 26 | ;; 27 | *) 28 | echo "Warning: Unsupported architecture $ARCH, defaulting to x86_64" 29 | LIB_ARCH="x86_64-linux-gnu" 30 | GST_LIB_DIR="/usr/lib/x86_64-linux-gnu/gstreamer-1.0" 31 | ;; 32 | esac 33 | 34 | # Detect distribution and package manager 35 | detect_distro() { 36 | if [ -f /etc/os-release ]; then 37 | . /etc/os-release 38 | DISTRO=$ID 39 | VERSION_ID=${VERSION_ID:-""} 40 | elif [ -f /etc/redhat-release ]; then 41 | DISTRO="rhel" 42 | elif [ -f /etc/debian_version ]; then 43 | DISTRO="debian" 44 | else 45 | DISTRO="unknown" 46 | fi 47 | echo "Detected distribution: $DISTRO" 48 | } 49 | 50 | # Install packages based on distribution 51 | install_dependencies() { 52 | echo "Installing build dependencies..." 53 | 54 | case $DISTRO in 55 | ubuntu|debian) 56 | sudo apt-get update 57 | sudo apt-get install -y build-essential cmake curl git pkg-config \ 58 | libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev 59 | ;; 60 | fedora) 61 | sudo dnf install -y gcc gcc-c++ cmake curl git pkgconfig \ 62 | gstreamer1-devel gstreamer1-plugins-base-devel 63 | GST_LIB_DIR="/usr/lib64/gstreamer-1.0" 64 | ;; 65 | centos|rhel) 66 | # Enable EPEL for additional packages 67 | if ! rpm -q epel-release &>/dev/null; then 68 | sudo yum install -y epel-release || sudo dnf install -y epel-release 69 | fi 70 | sudo yum install -y gcc gcc-c++ cmake curl git pkgconfig \ 71 | gstreamer1-devel gstreamer1-plugins-base-devel || \ 72 | sudo dnf install -y gcc gcc-c++ cmake curl git pkgconfig \ 73 | gstreamer1-devel gstreamer1-plugins-base-devel 74 | GST_LIB_DIR="/usr/lib64/gstreamer-1.0" 75 | ;; 76 | opensuse*|sles) 77 | sudo zypper install -y gcc gcc-c++ cmake curl git pkg-config \ 78 | gstreamer-devel gstreamer-plugins-base-devel 79 | GST_LIB_DIR="/usr/lib64/gstreamer-1.0" 80 | ;; 81 | arch|manjaro) 82 | sudo pacman -Sy --noconfirm base-devel cmake curl git pkgconf \ 83 | gstreamer gst-plugins-base 84 | GST_LIB_DIR="/usr/lib/gstreamer-1.0" 85 | ;; 86 | alpine) 87 | sudo apk update 88 | sudo apk add build-base cmake curl git pkgconfig \ 89 | gstreamer-dev gst-plugins-base-dev 90 | GST_LIB_DIR="/usr/lib/gstreamer-1.0" 91 | ;; 92 | *) 93 | echo "Warning: Unknown distribution. Attempting generic installation..." 94 | echo "Please ensure you have: gcc, cmake, curl, git, pkg-config, gstreamer development headers" 95 | read -p "Continue anyway? [y/N] " -n 1 -r 96 | echo 97 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 98 | exit 1 99 | fi 100 | ;; 101 | esac 102 | } 103 | 104 | # Check and install Rust/Cargo 105 | install_rust() { 106 | echo "Checking for Rust installation..." 107 | 108 | if command -v rustc &> /dev/null && command -v cargo &> /dev/null; then 109 | echo "Rust and Cargo are already installed:" 110 | rustc --version 111 | cargo --version 112 | 113 | # Check if Rust is reasonably recent (1.70+) 114 | RUST_VERSION=$(rustc --version | cut -d' ' -f2 | cut -d'.' -f1-2) 115 | if [ "$(printf '%s\n' "1.70" "$RUST_VERSION" | sort -V | head -n1)" = "1.70" ]; then 116 | echo "Rust version is sufficient." 117 | return 0 118 | else 119 | echo "Rust version is too old, updating..." 120 | fi 121 | else 122 | echo "Rust/Cargo not found. Installing..." 123 | fi 124 | 125 | # Install or update Rust using rustup 126 | if command -v rustup &> /dev/null; then 127 | echo "Updating Rust via rustup..." 128 | rustup update stable 129 | else 130 | echo "Installing Rust via rustup..." 131 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable 132 | 133 | # Source the cargo environment 134 | if [ -f "$HOME/.cargo/env" ]; then 135 | . "$HOME/.cargo/env" 136 | fi 137 | fi 138 | 139 | # Verify installation 140 | if command -v rustc &> /dev/null && command -v cargo &> /dev/null; then 141 | echo "Rust installation successful:" 142 | rustc --version 143 | cargo --version 144 | else 145 | echo "Error: Rust installation failed" 146 | exit 1 147 | fi 148 | } 149 | 150 | # Create fallback GStreamer plugin directory if it doesn't exist 151 | ensure_gstreamer_dir() { 152 | if [ ! -d "$GST_LIB_DIR" ]; then 153 | echo "GStreamer plugin directory not found at $GST_LIB_DIR" 154 | # Try common alternatives 155 | ALTERNATIVE_DIRS=( 156 | "/usr/lib/gstreamer-1.0" 157 | "/usr/lib64/gstreamer-1.0" 158 | "/usr/local/lib/gstreamer-1.0" 159 | "/usr/lib/$LIB_ARCH/gstreamer-1.0" 160 | ) 161 | 162 | for dir in "${ALTERNATIVE_DIRS[@]}"; do 163 | if [ -d "$dir" ]; then 164 | echo "Using alternative GStreamer directory: $dir" 165 | GST_LIB_DIR="$dir" 166 | return 0 167 | fi 168 | done 169 | 170 | echo "Creating GStreamer plugin directory: $GST_LIB_DIR" 171 | sudo mkdir -p "$GST_LIB_DIR" 172 | fi 173 | } 174 | 175 | # Main installation process 176 | main() { 177 | echo "Installing NDI SDK version ${NDI_VERSION}" 178 | 179 | # Detect system 180 | detect_distro 181 | 182 | # Install dependencies 183 | install_dependencies 184 | 185 | # Install Rust if needed 186 | install_rust 187 | 188 | # Download and install NDI SDK 189 | cd "$TMP_DIR" 190 | echo "Downloading NDI SDK..." 191 | curl -LO "$NDI_URL" 192 | 193 | echo "Extracting NDI SDK..." 194 | tar -xzf "$NDI_INSTALLER" 195 | 196 | echo "Running NDI installer..." 197 | yes | PAGER="cat" sh "./Install_NDI_SDK_v${NDI_VERSION}_Linux.sh" 198 | 199 | echo "Installing NDI libraries..." 200 | if [ -d "NDI SDK for Linux/lib/$LIB_ARCH" ]; then 201 | sudo cp -P "NDI SDK for Linux/lib/$LIB_ARCH/"* /usr/local/lib/ 202 | else 203 | echo "Warning: Architecture-specific NDI libraries not found, trying x86_64..." 204 | sudo cp -P "NDI SDK for Linux/lib/x86_64-linux-gnu/"* /usr/local/lib/ 205 | fi 206 | sudo ldconfig 207 | 208 | echo "Creating compatibility symlink..." 209 | sudo ln -sf /usr/local/lib/libndi.so.${NDI_VERSION} /usr/local/lib/libndi.so.5 210 | 211 | echo "Cleaning up NDI installation..." 212 | cd - > /dev/null 213 | rm -rf "$TMP_DIR" 214 | 215 | echo "Building GStreamer NDI Plugin..." 216 | 217 | # Ensure GStreamer directory exists 218 | ensure_gstreamer_dir 219 | 220 | # Clone and build the plugin 221 | PLUGIN_DIR="gst-plugin-ndi" 222 | if [ -d "$PLUGIN_DIR" ]; then 223 | echo "Updating existing plugin repository..." 224 | cd "$PLUGIN_DIR" 225 | git pull 226 | else 227 | echo "Cloning plugin repository..." 228 | git clone https://github.com/steveseguin/gst-plugin-ndi.git 229 | cd "$PLUGIN_DIR" 230 | fi 231 | 232 | cargo build --release 233 | 234 | echo "Installing GStreamer NDI plugin..." 235 | sudo install target/release/libgstndi.so "$GST_LIB_DIR/" 236 | sudo ldconfig 237 | 238 | echo "Testing GStreamer NDI plugin..." 239 | if gst-inspect-1.0 ndi &>/dev/null; then 240 | echo "✓ GStreamer NDI plugin installed successfully!" 241 | gst-inspect-1.0 ndi | head -5 242 | else 243 | echo "⚠ Warning: GStreamer NDI plugin test failed. Manual verification may be needed." 244 | fi 245 | 246 | cd .. 247 | 248 | echo "" 249 | echo "🎉 NDI SDK installation complete!" 250 | echo "" 251 | echo "Installed components:" 252 | echo " ✓ NDI SDK v${NDI_VERSION}" 253 | echo " ✓ Rust $(rustc --version | cut -d' ' -f2)" 254 | echo " ✓ GStreamer NDI plugin" 255 | echo "" 256 | echo "You may need to restart your terminal or run: source ~/.cargo/env" 257 | } 258 | 259 | # Run main function 260 | main -------------------------------------------------------------------------------- /record.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from fastapi import FastAPI, Request, Form, HTTPException 3 | from fastapi.responses import HTMLResponse, JSONResponse 4 | from fastapi.templating import Jinja2Templates 5 | import whisper 6 | import subprocess 7 | import random 8 | import os 9 | import logging 10 | import glob 11 | import argparse 12 | import threading 13 | import time 14 | import uvicorn 15 | 16 | # Configurer la journalisation 17 | logging.basicConfig(level=logging.INFO) 18 | logger = logging.getLogger(__name__) 19 | 20 | app = FastAPI() 21 | templates = Jinja2Templates(directory="templates") 22 | model = whisper.load_model("medium") 23 | 24 | # Liste pour suivre les processus 25 | processes = [] 26 | 27 | # Fonction de surveillance des processus 28 | def monitor_processes(): 29 | while True: 30 | current_time = time.time() 31 | for process_info in processes: 32 | process, start_time = process_info 33 | if current_time - start_time > 3600: # 3600 secondes = 1 heure 34 | logger.info("Killing process with PID: %d due to timeout", process.pid) 35 | process.kill() 36 | processes.remove(process_info) 37 | time.sleep(60) # Vérifier toutes les minutes 38 | 39 | # Démarrer le thread de surveillance 40 | monitor_thread = threading.Thread(target=monitor_processes, daemon=True) 41 | monitor_thread.start() 42 | 43 | @app.get("/", response_class=HTMLResponse) 44 | async def index(request: Request, room: str = "", record: str = ""): 45 | logger.info("Serving index page with room: %s (%s)", room, record) 46 | return templates.TemplateResponse("index.html", {"request": request, "room": room, "record": record}) 47 | 48 | @app.api_route("/rec", methods=["GET", "POST"]) 49 | async def start_recording(request: Request, room: str = Form(None), record: str = Form(None)): 50 | room = room or request.query_params.get("room") 51 | record = record or request.query_params.get("record") 52 | 53 | if not room or not record: 54 | raise HTTPException(status_code=400, detail="Room and record parameters must not be empty") 55 | 56 | logger.info("Starting recording for room: %s with record ID: %s", room, record) 57 | 58 | # Créer un pipe pour rediriger les logs de publish.py 59 | read_pipe, write_pipe = os.pipe() 60 | 61 | # Lancer l'enregistrement audio en arrière-plan avec les logs redirigés vers le pipe 62 | process = subprocess.Popen(["python3", "publish.py", "--room", room, "--record", record, "--novideo"], stdout=write_pipe, stderr=write_pipe) 63 | logger.info("Started publish.py process with PID: %d", process.pid) 64 | 65 | # Fermer le côté écriture du pipe dans le processus parent 66 | os.close(write_pipe) 67 | 68 | # Ajouter le processus à la liste avec l'heure de début 69 | processes.append((process, time.time())) 70 | 71 | # Afficher un bouton pour ouvrir la nouvelle page de visioconférence 72 | return templates.TemplateResponse("recording.html", {"request": request, "room": room, "record": record, "process_pid": process.pid}) 73 | 74 | @app.post("/stop") 75 | async def stop_recording(record: str = Form(...), process_pid: int = Form(...), language: str = Form(...)): 76 | logger.info("Stopping recording for record ID: %s with process PID: %d", record, process_pid) 77 | 78 | # Arrêter le processus d'enregistrement 79 | process = subprocess.Popen(["kill", str(process_pid)]) 80 | process.wait() 81 | logger.info("Stopped publish.py process with PID: %d", process_pid) 82 | 83 | # Trouver le fichier audio correspondant 84 | audio_files = glob.glob(f"{record}_*_audio.ts") 85 | if not audio_files: 86 | logger.error("No audio file found for record ID: %s", record) 87 | return {"error": f"No audio file found for record ID: {record}"} 88 | 89 | audio_file = audio_files[0] 90 | logger.info("Transcribing audio file: %s", audio_file) 91 | 92 | try: 93 | speech = model.transcribe(audio_file, language=language)['text'] 94 | logger.info("Transcription completed for record ID: %s", record) 95 | except Exception as e: 96 | logger.error("Failed to transcribe audio file: %s", str(e)) 97 | return {"error": f"Failed to transcribe audio file: {str(e)}"} 98 | 99 | # Écrire la transcription dans un fichier texte 100 | transcript_file = f"stt/{record}_speech.txt" 101 | with open(transcript_file, "w") as f: 102 | f.write(speech) 103 | logger.info("Transcription saved to: %s", transcript_file) 104 | 105 | # Supprimer le fichier audio 106 | os.remove(audio_file) 107 | logger.info("Audio file %s removed.", audio_file) 108 | 109 | return {"transcription": speech} 110 | 111 | @app.get("/stt") 112 | async def get_transcription(id: str): 113 | transcript_file = f"stt/{id}_speech.txt" 114 | if not os.path.exists(transcript_file): 115 | logger.error("No transcription file found for record ID: %s", id) 116 | return JSONResponse(status_code=404, content={"error": f"No transcription file found for record ID: {id}"}) 117 | 118 | with open(transcript_file, "r") as f: 119 | transcription = f.read() 120 | 121 | # Ajouter le fichier à IPFS 122 | try: 123 | logger.info(f"Adding file to IPFS: {transcript_file}") 124 | result = subprocess.run(["ipfs", "add", transcript_file], capture_output=True, text=True) 125 | cid = result.stdout.split()[1] 126 | logger.info("Added file to IPFS: %s with CID: %s", transcript_file, cid) 127 | except Exception as e: 128 | logger.error("Failed to add file to IPFS: %s", str(e)) 129 | return JSONResponse(status_code=500, content={"error": f"Failed to add file to IPFS: {str(e)}"}) 130 | 131 | logger.info("Returning transcription and CID for record ID: %s", id) 132 | return {"transcription": transcription, "cid": cid} 133 | 134 | def start_recording_cli(room, record): 135 | logger.info("Starting recording for room: %s with record ID: %s", room, record) 136 | 137 | # Créer un pipe pour rediriger les logs de publish.py 138 | read_pipe, write_pipe = os.pipe() 139 | 140 | # Lancer l'enregistrement audio en arrière-plan avec les logs redirigés vers le pipe 141 | process = subprocess.Popen(["python3", "publish.py", "--room", room, "--record", record, "--novideo"], stdout=write_pipe, stderr=write_pipe) 142 | logger.info("Started publish.py process with PID: %d", process.pid) 143 | 144 | # Fermer le côté écriture du pipe dans le processus parent 145 | os.close(write_pipe) 146 | 147 | # Ajouter le processus à la liste avec l'heure de début 148 | processes.append((process, time.time())) 149 | 150 | return process.pid 151 | 152 | def stop_recording_cli(record, process_pid, language): 153 | logger.info("Stopping recording for record ID: %s with process PID: %d", record, process_pid) 154 | 155 | # Arrêter le processus d'enregistrement 156 | process = subprocess.Popen(["kill", str(process_pid)]) 157 | process.wait() 158 | logger.info("Stopped publish.py process with PID: %d", process_pid) 159 | 160 | # Trouver le fichier audio correspondant 161 | audio_files = glob.glob(f"{record}_*_audio.ts") 162 | if not audio_files: 163 | logger.error("No audio file found for record ID: %s", record) 164 | return {"error": f"No audio file found for record ID: {record}"} 165 | 166 | audio_file = audio_files[0] 167 | logger.info("Transcribing audio file: %s", audio_file) 168 | 169 | try: 170 | speech = model.transcribe(audio_file, language=language)['text'] 171 | logger.info("Transcription completed for record ID: %s", record) 172 | except Exception as e: 173 | logger.error("Failed to transcribe audio file: %s", str(e)) 174 | return {"error": f"Failed to transcribe audio file: {str(e)}"} 175 | 176 | # Écrire la transcription dans un fichier texte 177 | transcript_file = f"stt/{record}_speech.txt" 178 | with open(transcript_file, "w") as f: 179 | f.write(speech) 180 | logger.info("Transcription saved to: %s", transcript_file) 181 | 182 | # Supprimer le fichier audio 183 | os.remove(audio_file) 184 | logger.info("Audio file %s removed.", audio_file) 185 | 186 | return {"transcription": speech} 187 | 188 | if __name__ == "__main__": 189 | parser = argparse.ArgumentParser(description="Démarrer le serveur FastAPI avec des paramètres personnalisés.") 190 | parser.add_argument("--host", type=str, default="0.0.0.0", help="Adresse hôte pour le serveur FastAPI.") 191 | parser.add_argument("--port", type=int, default=9000, help="Port pour le serveur FastAPI.") 192 | parser.add_argument("--room", type=str, help="Room name for the recording session.") 193 | parser.add_argument("--record", type=str, help="Record ID for the session.") 194 | parser.add_argument("--stop", action="store_true", help="Stop the recording.") 195 | parser.add_argument("--pid", type=int, help="Process PID to stop.") 196 | parser.add_argument("--language", type=str, default="en", help="Language for transcription.") 197 | args = parser.parse_args() 198 | 199 | if args.room and args.record and not args.stop: 200 | pid = start_recording_cli(args.room, args.record) 201 | print(f"Recording started with PID: {pid}") 202 | elif args.stop and args.pid and args.record: 203 | result = stop_recording_cli(args.record, args.pid, args.language) 204 | print(result) 205 | else: 206 | logger.info("Starting FastAPI server") 207 | uvicorn.run(app, host=args.host, port=args.port) 208 | -------------------------------------------------------------------------------- /tools/combine_recordings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Combine async audio and video recordings with proper timestamp-based synchronization 4 | """ 5 | 6 | import asyncio 7 | import glob 8 | import os 9 | import sys 10 | import json 11 | import subprocess 12 | from pathlib import Path 13 | 14 | async def get_stream_start_time(filepath): 15 | """Get the actual start time of the first frame/sample in a media file""" 16 | cmd = await asyncio.create_subprocess_exec( 17 | 'ffprobe', '-v', 'quiet', '-print_format', 'json', 18 | '-show_entries', 'stream=start_time,start_pts,time_base,codec_type', 19 | filepath, 20 | stdout=asyncio.subprocess.PIPE, 21 | stderr=asyncio.subprocess.PIPE 22 | ) 23 | stdout, _ = await cmd.communicate() 24 | 25 | if cmd.returncode == 0: 26 | try: 27 | data = json.loads(stdout.decode()) 28 | streams = data.get('streams', []) 29 | 30 | # Find the first video or audio stream 31 | for stream in streams: 32 | codec_type = stream.get('codec_type') 33 | if codec_type in ['video', 'audio']: 34 | # Get the start time in seconds 35 | start_time = stream.get('start_time', '0') 36 | return float(start_time) 37 | except Exception as e: 38 | print(f" Warning: Could not parse start time: {e}") 39 | return 0.0 40 | 41 | async def get_file_info(filepath): 42 | """Get duration and codec info for a media file""" 43 | cmd = await asyncio.create_subprocess_exec( 44 | 'ffprobe', '-v', 'quiet', '-print_format', 'json', 45 | '-show_format', '-show_streams', filepath, 46 | stdout=asyncio.subprocess.PIPE, 47 | stderr=asyncio.subprocess.PIPE 48 | ) 49 | stdout, _ = await cmd.communicate() 50 | 51 | if cmd.returncode == 0: 52 | try: 53 | return json.loads(stdout.decode()) 54 | except: 55 | pass 56 | return None 57 | 58 | async def combine_files(video_file, audio_file, output_file): 59 | """Combine video and audio with proper timestamp-based sync""" 60 | print(f"\nCombining:") 61 | print(f" Video: {video_file}") 62 | print(f" Audio: {audio_file}") 63 | print(f" Output: {output_file}") 64 | 65 | # Get stream start times 66 | video_start = await get_stream_start_time(video_file) 67 | audio_start = await get_stream_start_time(audio_file) 68 | 69 | print(f" Video start time: {video_start:.3f}s") 70 | print(f" Audio start time: {audio_start:.3f}s") 71 | 72 | # Calculate the time difference 73 | time_diff = video_start - audio_start 74 | 75 | # Build ffmpeg command 76 | cmd = ['ffmpeg', '-y'] 77 | 78 | # Input files 79 | cmd.extend(['-i', video_file, '-i', audio_file]) 80 | 81 | # Always use precise sync based on stream timestamps 82 | if abs(time_diff) < 0.001: # Less than 1ms difference 83 | print(" Strategy: Direct merge (streams already in sync)") 84 | cmd.extend([ 85 | '-c:v', 'libx264', 86 | '-preset', 'fast', 87 | '-crf', '23', 88 | '-c:a', 'aac', 89 | '-b:a', '192k', 90 | '-shortest' 91 | ]) 92 | elif time_diff > 0: 93 | # Video starts later than audio - delay the audio 94 | delay_ms = int(time_diff * 1000) 95 | print(f" Strategy: Delaying audio by {delay_ms}ms to sync with video") 96 | cmd.extend([ 97 | '-filter_complex', f'[1:a]adelay={delay_ms}|{delay_ms}[delayed]', 98 | '-map', '0:v', 99 | '-map', '[delayed]', 100 | '-c:v', 'libx264', 101 | '-preset', 'fast', 102 | '-crf', '23', 103 | '-c:a', 'aac', 104 | '-b:a', '192k', 105 | '-shortest' 106 | ]) 107 | else: 108 | # Audio starts later than video - delay the video or trim audio 109 | delay_ms = int(abs(time_diff) * 1000) 110 | print(f" Strategy: Audio starts {delay_ms}ms after video") 111 | 112 | # For small delays, we can use setpts to delay video 113 | if delay_ms < 5000: # Less than 5 seconds 114 | video_delay = abs(time_diff) 115 | print(f" Delaying video by {video_delay:.3f}s") 116 | cmd.extend([ 117 | '-filter_complex', 118 | f'[0:v]setpts=PTS+{video_delay}/TB[delayed_video]', 119 | '-map', '[delayed_video]', 120 | '-map', '1:a', 121 | '-c:v', 'libx264', 122 | '-preset', 'fast', 123 | '-crf', '23', 124 | '-c:a', 'aac', 125 | '-b:a', '192k', 126 | '-shortest' 127 | ]) 128 | else: 129 | # For larger delays, trim the beginning of audio 130 | trim_start = abs(time_diff) 131 | print(f" Trimming {trim_start:.3f}s from audio start") 132 | cmd.extend([ 133 | '-filter_complex', f'[1:a]atrim=start={trim_start}[trimmed]', 134 | '-map', '0:v', 135 | '-map', '[trimmed]', 136 | '-c:v', 'libx264', 137 | '-preset', 'fast', 138 | '-crf', '23', 139 | '-c:a', 'aac', 140 | '-b:a', '192k' 141 | ]) 142 | 143 | # Output file 144 | cmd.append(output_file) 145 | 146 | # Execute 147 | process = await asyncio.create_subprocess_exec( 148 | *cmd, 149 | stdout=asyncio.subprocess.PIPE, 150 | stderr=asyncio.subprocess.PIPE 151 | ) 152 | stdout, stderr = await process.communicate() 153 | 154 | if process.returncode == 0: 155 | size = os.path.getsize(output_file) 156 | print(f" ✅ Success! Output size: {size:,} bytes") 157 | 158 | # Verify output 159 | info = await get_file_info(output_file) 160 | if info: 161 | duration = info.get('format', {}).get('duration', 'unknown') 162 | streams = len(info.get('streams', [])) 163 | print(f" Duration: {duration}s, Streams: {streams}") 164 | 165 | # Check if both audio and video are present 166 | has_video = any(s.get('codec_type') == 'video' for s in info.get('streams', [])) 167 | has_audio = any(s.get('codec_type') == 'audio' for s in info.get('streams', [])) 168 | 169 | if has_video and has_audio: 170 | print(" ✅ Both video and audio tracks present") 171 | 172 | # Verify sync by checking if streams start at the same time 173 | output_start = await get_stream_start_time(output_file) 174 | print(f" Output start time: {output_start:.3f}s") 175 | 176 | return True 177 | else: 178 | print(f" ⚠️ Missing tracks - Video: {has_video}, Audio: {has_audio}") 179 | 180 | return True 181 | else: 182 | print(f" ❌ Failed: {stderr.decode()[:200]}") 183 | return False 184 | 185 | async def main(): 186 | """Find and combine matching audio/video pairs""" 187 | print("=== Combine Audio/Video Recordings (v2 - Timestamp-based sync) ===\n") 188 | 189 | # Find all video and audio files 190 | video_files = glob.glob("testroom123999999999_*[!_audio].webm") 191 | audio_files = glob.glob("testroom123999999999_*_audio.wav") 192 | 193 | if not video_files or not audio_files: 194 | print("No files to combine!") 195 | return 196 | 197 | print(f"Found {len(video_files)} video and {len(audio_files)} audio files\n") 198 | 199 | # Match files by stream ID 200 | combined_count = 0 201 | 202 | for video_file in sorted(video_files): 203 | # Extract stream ID from video filename 204 | parts = Path(video_file).stem.split('_') 205 | if len(parts) < 3: 206 | continue 207 | 208 | stream_id = parts[1] 209 | timestamp = parts[2] 210 | 211 | # Find matching audio file with same or close timestamp (within 2 seconds) 212 | matching_audio = None 213 | video_timestamp = int(timestamp) 214 | 215 | for audio_file in audio_files: 216 | # Extract audio timestamp 217 | if f"_{stream_id}_" in audio_file and "_audio.wav" in audio_file: 218 | audio_parts = Path(audio_file).stem.split('_') 219 | if len(audio_parts) >= 3: 220 | try: 221 | audio_timestamp = int(audio_parts[2]) 222 | # Match if timestamps are within 2 seconds 223 | if abs(audio_timestamp - video_timestamp) <= 2: 224 | matching_audio = audio_file 225 | break 226 | except ValueError: 227 | continue 228 | 229 | if matching_audio: 230 | output_file = f"combined_v2_{stream_id}_{timestamp}.mp4" 231 | 232 | if os.path.exists(output_file): 233 | print(f"Skipping {output_file} - already exists") 234 | continue 235 | 236 | success = await combine_files(video_file, matching_audio, output_file) 237 | if success: 238 | combined_count += 1 239 | else: 240 | print(f"No matching audio for {video_file}") 241 | 242 | print(f"\n=== Summary ===") 243 | print(f"Combined {combined_count} file pairs") 244 | 245 | # List combined files 246 | combined_files = glob.glob("combined_v2_*.mp4") 247 | if combined_files: 248 | print("\nCombined files:") 249 | for cf in sorted(combined_files): 250 | size = os.path.getsize(cf) 251 | print(f" {cf} ({size:,} bytes)") 252 | 253 | if __name__ == "__main__": 254 | if len(sys.argv) > 1: 255 | # Allow specific file combination 256 | if len(sys.argv) == 4: 257 | asyncio.run(combine_files(sys.argv[1], sys.argv[2], sys.argv[3])) 258 | else: 259 | print("Usage: combine_recordings_v2.py [video_file audio_file output_file]") 260 | else: 261 | # Auto-combine all matching pairs 262 | asyncio.run(main()) -------------------------------------------------------------------------------- /installers/raspberry_pi/installer.sh: -------------------------------------------------------------------------------- 1 | 2 | ## Last tested on October 5th 2023, using a fresh official Bullseye lite 64-bit 2023 image on a RPI 4 3 | 4 | echo "nameserver 1.1.1.1" | sudo tee -a /etc/resolv.conf 5 | 6 | sudo chattr -V +i /etc/resolv.conf ### lock access 7 | # sudo chattr -i /etc/resolv.conf ### to re-enable write access 8 | sudo systemctl restart systemd-resolved.service 9 | 10 | export GIT_SSL_NO_VERIFY=1 11 | export GST_PLUGIN_PATH=/usr/local/lib/gstreamer-1.0:/usr/lib/gstreamer-1.0 12 | export LD_LIBRARY_PATH=/usr/local/lib/ 13 | 14 | sudo apt-get update 15 | sudo apt-get upgrade -y ## Bullseye will auto-update to 64-bit from 32-bit if you upgrade; bewarned 16 | sudo apt-get install vim git -y 17 | sudo apt-get full-upgrade -y 18 | sudo apt-get dist-upgrade -y 19 | # sudo rpi-update ## use at your own risk, if you need it 20 | 21 | ### REBOOT 22 | 23 | # sudo raspi-config ## --> Interface Options --> I2C <++++++++++++++++++++ enable i2c 24 | 25 | ### https://docs.arducam.com/Raspberry-Pi-Camera/Native-camera/Quick-Start-Guide/ (imx417/imx519) 26 | ## https://github.com/raspberrypi/firmware/blob/master/boot/overlays/README 27 | ## pivariety IMX462 ? see: https://forums.raspberrypi.com/viewtopic.php?t=331213&p=1992004#p1991478 28 | 29 | ### You may need to increase the swap size if pi zero2 or slower/smaller to avoid system crashes with compiling 30 | sudo dphys-swapfile swapoff 31 | # sudo echo "CONF_SWAPSIZE=1024" >> /etc/dphys-swapfile 32 | # sudo vi /etc/dphys-swapfile 33 | # Detect Pi model and adjust swap size 34 | PI_MODEL=$(cat /proc/device-tree/model 2>/dev/null || echo "Unknown") 35 | if [[ "$PI_MODEL" == *"Zero 2"* ]]; then 36 | CONF_SWAPSIZE=2048 # Pi Zero 2 needs more swap for building 37 | else 38 | CONF_SWAPSIZE=1024 # Default for other models 39 | fi 40 | # Update the config file with: CONF_SWAPSIZE value 41 | sudo dphys-swapfile setup 42 | sudo dphys-swapfile swapon 43 | ############################### 44 | 45 | sudo apt-get install python3 python3-pip -y 46 | sudo apt-get install build-essential cmake libtool libc6 libc6-dev unzip wget libnuma1 libnuma-dev -y 47 | # Remove EXTERNALLY-MANAGED file for any Python version 48 | PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') 49 | sudo rm /usr/lib/python${PYTHON_VERSION}/EXTERNALLY-MANAGED 2>/dev/null || true # Debian 12 protection 50 | sudo pip3 install scikit-build 51 | sudo pip3 install ninja 52 | sudo pip3 install websockets 53 | sudo pip3 install python-rtmidi 54 | sudo pip3 install cryptography 55 | 56 | sudo apt-get install apt-transport-https ca-certificates -y 57 | 58 | sudo apt-get remove python-gi-dev -y 59 | sudo apt-get install python3-gi -y 60 | sudo apt-get install python3-pyqt5 -y 61 | 62 | sudo apt-get install ccache curl bison flex \ 63 | libasound2-dev libbz2-dev libcap-dev libdrm-dev libegl1-mesa-dev \ 64 | libfaad-dev libgl1-mesa-dev libgles2-mesa-dev libgmp-dev libgsl0-dev \ 65 | libjpeg-dev libmms-dev libmpg123-dev libogg-dev \ 66 | liborc-0.4-dev libpango1.0-dev libpng-dev librtmp-dev \ 67 | libgif-dev pkg-config libmp3lame-dev \ 68 | libopencore-amrnb-dev libopencore-amrwb-dev libcurl4-openssl-dev \ 69 | libsidplay1-dev libx264-dev libusb-1.0 pulseaudio libpulse-dev \ 70 | libomxil-bellagio-dev libfreetype6-dev checkinstall fonts-freefont-ttf -y 71 | 72 | sudo apt-get install libcamera-dev -y 73 | sudo apt-get install libatk1.0-dev -y 74 | sudo apt-get install -y libgdk-pixbuf2.0-dev 75 | sudo apt-get install libffi6 libffi-dev -y 76 | sudo apt-get install -y libselinux-dev 77 | sudo apt-get install -y libmount-dev 78 | sudo apt-get install libelf-dev -y 79 | sudo apt-get install libdbus-1-dev -y 80 | 81 | sudo apt-get install autotools-dev automake autoconf \ 82 | autopoint libxml2-dev zlib1g-dev libglib2.0-dev \ 83 | gtk-doc-tools \ 84 | libgudev-1.0-dev libxt-dev libvorbis-dev libcdparanoia-dev \ 85 | libtheora-dev libvisual-0.4-dev iso-codes \ 86 | libgtk-3-dev libraw1394-dev libiec61883-dev libavc1394-dev \ 87 | libv4l-dev libcairo2-dev libcaca-dev libspeex-dev \ 88 | libshout3-dev libaa1-dev libflac-dev libdv4-dev \ 89 | libtag1-dev libwavpack-dev libsoup2.4-dev \ 90 | libcdaudio-dev ladspa-sdk libass-dev \ 91 | libcurl4-gnutls-dev libdca-dev libdvdnav-dev \ 92 | libexempi-dev libexif-dev libgme-dev libgsm1-dev \ 93 | libiptcdata0-dev libkate-dev \ 94 | libmodplug-dev libmpcdec-dev libofa0-dev libopus-dev \ 95 | librsvg2-dev \ 96 | libsndfile1-dev libsoundtouch-dev libspandsp-dev libx11-dev \ 97 | libxvidcore-dev libzbar-dev libzvbi-dev liba52-0.7.4-dev \ 98 | libcdio-dev libdvdread-dev libmad0-dev \ 99 | libmpeg2-4-dev \ 100 | libtwolame-dev \ 101 | yasm python3-dev libgirepository1.0-dev -y 102 | 103 | sudo apt-get install -y tar freeglut3 weston libssl-dev policykit-1-gnome -y 104 | sudo apt-get install libwebrtc-audio-processing-dev libvpx-dev -y 105 | sudo apt-get install libass-dev # added oct 18th; not sure whats using it 106 | 107 | ### MESON - specific version 108 | cd ~ 109 | git clone https://github.com/mesonbuild/meson.git 110 | cd meson 111 | git checkout 0.64.1 ## everything after this is version 1.x? 112 | git fetch --all 113 | sudo python3 setup.py install 114 | 115 | pip3 install pycairo 116 | 117 | # AAC - optional (needed for rtmp only really) 118 | cd ~ 119 | git clone --depth 1 https://github.com/mstorsjo/fdk-aac.git 120 | cd fdk-aac 121 | autoreconf -fiv 122 | ./configure 123 | make -j4 124 | sudo make install 125 | 126 | # AV1 - optional 127 | cd ~ 128 | git clone --depth 1 https://code.videolan.org/videolan/dav1d.git 129 | cd dav1d 130 | mkdir build 131 | cd build 132 | meson .. 133 | ninja 134 | sudo ninja install 135 | sudo ldconfig 136 | sudo libtoolize 137 | 138 | # HEVC - optional 139 | cd ~ 140 | git clone --depth 1 https://github.com/ultravideo/kvazaar.git 141 | cd kvazaar 142 | ./autogen.sh 143 | ./configure 144 | make -j4 145 | sudo make install 146 | 147 | sudo apt-get -y install \ 148 | doxygen \ 149 | graphviz \ 150 | imagemagick \ 151 | libavcodec-dev \ 152 | libavdevice-dev \ 153 | libavfilter-dev \ 154 | libavformat-dev \ 155 | libavutil-dev \ 156 | libmp3lame-dev \ 157 | libopencore-amrwb-dev \ 158 | libsdl2-dev \ 159 | libsdl2-image-dev \ 160 | libsdl2-mixer-dev \ 161 | libsdl2-net-dev \ 162 | libsdl2-ttf-dev \ 163 | libsnappy-dev \ 164 | libsoxr-dev \ 165 | libssh-dev \ 166 | libtool \ 167 | libv4l-dev \ 168 | libva-dev \ 169 | libvdpau-dev \ 170 | libvo-amrwbenc-dev \ 171 | libvorbis-dev \ 172 | libwebp-dev \ 173 | libx265-dev \ 174 | libxcb-shape0-dev \ 175 | libxcb-shm0-dev \ 176 | libxcb-xfixes0-dev \ 177 | libxcb1-dev \ 178 | libxml2-dev \ 179 | lzma-dev \ 180 | texinfo \ 181 | libaom-dev \ 182 | libsrt-gnutls-dev \ 183 | zlib1g-dev \ 184 | libgmp-dev 185 | 186 | sudo apt-get -y install libzimg-dev 187 | 188 | # SRT - optional 189 | cd ~ 190 | sudo apt-get install tclsh pkg-config cmake build-essential -y 191 | git clone https://github.com/Haivision/srt 192 | cd srt 193 | ./configure 194 | make 195 | sudo make install 196 | sudo ldconfig 197 | 198 | sudo apt-get install libdrm-dev automake libtool 199 | git clone https://github.com/intel/libva.git 200 | cd libva 201 | mkdir build 202 | cd build 203 | meson .. -Dprefix=/usr 204 | ninja 205 | sudo ninja install 206 | 207 | ### FFMPEG 208 | cd ~ 209 | [ ! -d FFmpeg ] && git clone https://github.com/FFmpeg/FFmpeg.git 210 | cd FFmpeg 211 | git pull 212 | make distclean 213 | sudo ./configure \ 214 | --extra-cflags="-I/usr/local/include" \ 215 | --arch=armhf \ 216 | --extra-ldflags="-L/usr/local/lib" \ 217 | --extra-libs="-lpthread -lm -latomic" \ 218 | --enable-libaom \ 219 | --enable-libsrt \ 220 | --enable-librtmp \ 221 | --enable-libopus \ 222 | --enable-gmp \ 223 | --enable-version3 \ 224 | --enable-libdrm \ 225 | --enable-shared \ 226 | --enable-pic \ 227 | --enable-libvpx \ 228 | --enable-libvorbis \ 229 | --enable-libfdk-aac \ 230 | --enable-libvpx \ 231 | --target-os=linux \ 232 | --enable-gpl \ 233 | --enable-pthreads \ 234 | --enable-libkvazaar \ 235 | --enable-hardcoded-tables \ 236 | --enable-libopencore-amrwb \ 237 | --enable-libopencore-amrnb \ 238 | --enable-nonfree \ 239 | --enable-libmp3lame \ 240 | --enable-libfreetype \ 241 | --enable-libx264 \ 242 | --enable-libx265 \ 243 | --enable-libwebp \ 244 | --enable-mmal \ 245 | --enable-indev=alsa \ 246 | --enable-outdev=alsa \ 247 | --enable-libsnappy \ 248 | --enable-libxml2 \ 249 | --enable-libssh \ 250 | --enable-libsoxr \ 251 | --disable-vdpau \ 252 | --enable-libdav1d \ 253 | --enable-libass \ 254 | --disable-mmal \ 255 | --arch=armel \ 256 | --enable-openssl 257 | libtoolize 258 | make -j4 259 | libtoolize 260 | sudo make install -j4 261 | sudo ldconfig 262 | 263 | export GST_PLUGIN_PATH=/usr/local/lib/gstreamer-1.0:/usr/lib/gstreamer-1.0 264 | export LD_LIBRARY_PATH=/usr/local/lib/ 265 | cd ~ 266 | wget https://download.gnome.org/sources/glib/2.78/glib-2.78.0.tar.xz -O glib.tar.xz 267 | tar -xvf glib.tar.xz 268 | cd glib-2.78.0 269 | mkdir build 270 | cd build 271 | meson --prefix=/usr/local -Dman=false .. 272 | sudo ninja 273 | sudo ninja install 274 | sudo ldconfig 275 | sudo libtoolize 276 | 277 | cd ~ 278 | git clone https://github.com/sctplab/usrsctp.git 279 | cd usrsctp 280 | mkdir build 281 | sudo meson build --prefix=/usr/local 282 | sudo ninja -C build install -j4 283 | sudo ldconfig 284 | sudo libtoolize 285 | 286 | export GST_PLUGIN_PATH=/usr/local/lib/gstreamer-1.0:/usr/lib/gstreamer-1.0 287 | export LD_LIBRARY_PATH=/usr/local/lib/ 288 | cd ~ 289 | wget https://download.gnome.org/sources/gobject-introspection/1.78/gobject-introspection-1.78.1.tar.xz -O gobject.tar.xz 290 | sudo rm gobject-introspection-1.78.1 -r || true 291 | tar -xvf gobject.tar.xz 292 | cd gobject-introspection-1.78.1 293 | mkdir build 294 | cd build 295 | sudo meson --prefix=/usr/local --buildtype=release .. 296 | sudo ninja 297 | sudo ninja install 298 | sudo ldconfig 299 | sudo libtoolize 300 | 301 | systemctl --user enable pulseaudio.socket 302 | sudo apt-get install -y wayland-protocols 303 | sudo apt-get install -y libxkbcommon-dev 304 | sudo apt-get install -y libepoxy-dev 305 | sudo apt-get install -y libatk-bridge2.0 306 | 307 | 308 | export GST_PLUGIN_PATH=/usr/local/lib/gstreamer-1.0:/usr/lib/gstreamer-1.0 309 | export LD_LIBRARY_PATH=/usr/local/lib/ 310 | cd ~ 311 | git clone https://github.com/libnice/libnice.git 312 | cd libnice 313 | mkdir build 314 | cd build 315 | meson --prefix=/usr/local --buildtype=release .. 316 | sudo ninja 317 | sudo ninja install 318 | sudo ldconfig 319 | sudo libtoolize 320 | 321 | cd ~ 322 | git clone https://github.com/cisco/libsrtp 323 | cd libsrtp 324 | ./configure --enable-openssl --prefix=/usr/local 325 | make -j4 326 | sudo make shared_library 327 | sudo make install -j4 328 | sudo ldconfig 329 | sudo libtoolize 330 | 331 | cd ~ 332 | [ ! -d gstreamer ] && git clone -b 1.22 https://gitlab.freedesktop.org/gstreamer/gstreamer/ ## 1.22 333 | export GST_PLUGIN_PATH=/usr/local/lib/gstreamer-1.0:/usr/lib/gstreamer-1.0 334 | export LD_LIBRARY_PATH=/usr/local/lib/ 335 | cd gstreamer 336 | git pull 337 | sudo rm -r build || true 338 | [ ! -d build ] && mkdir build 339 | cd build 340 | sudo meson --prefix=/usr/local -Dbuildtype=release -Dgst-plugins-base:gl_winsys=egl -Dgpl=enabled -Ddoc=disabled -Dtests=disabled -Dexamples=disabled -Dges=disabled -Dgst-examples=disabled -Ddevtools=disabled .. 341 | cd .. 342 | sudo ninja -C build install -j4 343 | sudo ldconfig 344 | 345 | ### Vanilla LibCamera -- We run it after gstreamer so it detects it and installs the right plugins. 346 | export GST_PLUGIN_PATH=/usr/local/lib/gstreamer-1.0:/usr/lib/gstreamer-1.0 347 | export LD_LIBRARY_PATH=/usr/local/lib/ 348 | sudo apt-get install libyaml-dev python3-yaml python3-ply python3-jinja2 -y 349 | cd ~ 350 | git clone https://git.libcamera.org/libcamera/libcamera.git 351 | cd libcamera 352 | meson setup build 353 | sudo ninja -C build install -j4 ## too many cores and you'll crash a raspiberry pi zero 2 354 | sudo ldconfig 355 | cd ~ 356 | 357 | ## RUST ... optional, adds native whip/whep support to gstreamer 358 | curl https://sh.rustup.rs -sSf | sh 359 | source "$HOME/.cargo/env" 360 | cargo install cargo-c # slow, bloated, with many weak depedencies; careful 361 | cd ~ 362 | git clone https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git 363 | cd gst-plugins-rs 364 | rm Cargo.toml # this won is bloated; we're going to delete some install options to speed things up and to avoid crashing things during the build 365 | wget https://raw.githubusercontent.com/steveseguin/raspberry_ninja/main/raspberry_pi/Cargo.toml 366 | cargo cinstall --prefix=./tmp # whip/whep plugins for gstreamer 367 | sudo cp ./tmp/lib/gstreamer-1.0/* /usr/local/lib/aarch64-linux-gnu/gstreamer-1.0 368 | sudo cp ./tmp/lib/pkgconfig/* /usr/local/lib/aarch64-linux-gnu/pkgconfig 369 | sudo cp ./tmp/lib/gstreamer-1.0/* /usr/local/lib/gstreamer-1.0 370 | sudo cp ./tmp/lib/pkgconfig/* /usr/local/lib/pkgconfig 371 | # sudo cp ./tmp/lib/gstreamer-1.0/* /usr/lib/aarch64-linux-gnu/gstreamer-1.0 # orangepi 372 | # sudo cp ./tmp/lib/pkgconfig/* /usr/lib/aarch64-linux-gnu/pkgconfig # orangepi 373 | 374 | # modprobe bcm2835-codecfg 375 | 376 | ## see https://raspberry.ninja for further system optimizations and settings 377 | 378 | systemctl --user restart pulseaudio.socket 379 | 380 | ## Turn off the swap file, to avoid issues caused by slow sd caching? 381 | sudo dphys-swapfile swapoff 382 | 383 | ## If things still don't work, run it all again, a section at a time, making sure it all passes 384 | -------------------------------------------------------------------------------- /installers/nvidia_jetson/setup_autostart.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Raspberry Ninja NVIDIA Jetson autostart helper. 4 | # Guides the user through creating a systemd service that launches publish.py 5 | # with a chosen command line and optionally disables the desktop GUI. 6 | 7 | set -euo pipefail 8 | 9 | if [[ ${EUID:-$(id -u)} -ne 0 ]]; then 10 | echo "Please run this script with sudo (eg. sudo $0)." 11 | exit 1 12 | fi 13 | 14 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 15 | REPO_ROOT="$(realpath "${SCRIPT_DIR}/../..")" 16 | 17 | if [[ ! -f "${REPO_ROOT}/publish.py" ]]; then 18 | echo "Could not locate publish.py in ${REPO_ROOT}. Aborting." 19 | exit 1 20 | fi 21 | 22 | chvt_path="$(command -v chvt || true)" 23 | setterm_path="$(command -v setterm || true)" 24 | stty_path="$(command -v stty || true)" 25 | 26 | if [[ -z "${chvt_path}" ]]; then 27 | echo "Could not locate 'chvt'. Aborting." 28 | exit 1 29 | fi 30 | 31 | if [[ -z "${setterm_path}" ]]; then 32 | echo "Could not locate 'setterm'. Aborting." 33 | exit 1 34 | fi 35 | 36 | if [[ -z "${stty_path}" ]]; then 37 | echo "Could not locate 'stty'. Aborting." 38 | exit 1 39 | fi 40 | 41 | escape_for_single_quotes() { 42 | printf "%s" "$1" | sed "s/'/'\"'\"'/g" 43 | } 44 | 45 | playback_tty="/dev/tty4" 46 | playback_tty_name="${playback_tty#/dev/}" 47 | playback_vt_number="${playback_tty_name#tty}" 48 | playback_getty_unit="getty@${playback_tty_name}.service" 49 | playback_console_status="pending" 50 | 51 | blanking_status="Not configured" 52 | 53 | default_user="${SUDO_USER:-}" 54 | if [[ -z "${default_user}" || "${default_user}" == "root" ]]; then 55 | default_user="vdo" 56 | fi 57 | 58 | read -rp "System user that should run publish.py [${default_user}]: " service_user 59 | service_user="${service_user:-$default_user}" 60 | 61 | if ! id "${service_user}" >/dev/null 2>&1; then 62 | echo "User '${service_user}' does not exist. Aborting." 63 | exit 1 64 | fi 65 | 66 | user_home="$(eval echo "~${service_user}")" 67 | 68 | default_cmd="python3 ${REPO_ROOT}/publish.py --view steve123 --stretch-display" 69 | read -rp "Command to run on startup [${default_cmd}]: " start_command 70 | start_command="${start_command:-$default_cmd}" 71 | 72 | if [[ -z "${start_command}" ]]; then 73 | echo "A command is required. Aborting." 74 | exit 1 75 | fi 76 | 77 | default_splash_color="black" 78 | read -rp "Splash background color (named or #RRGGBB) [${default_splash_color}]: " splash_color_input 79 | splash_color_input="${splash_color_input:-$default_splash_color}" 80 | splash_color_lower="$(echo "${splash_color_input}" | tr '[:upper:]' '[:lower:]')" 81 | 82 | palette_hex="" 83 | splash_color_summary="${splash_color_input}" 84 | 85 | if [[ "${splash_color_lower}" =~ ^#?[0-9a-f]{6}$ ]]; then 86 | splash_hex="${splash_color_lower#\#}" 87 | splash_hex="$(echo "${splash_hex}" | tr '[:lower:]' '[:upper:]')" 88 | palette_hex="${splash_hex}" 89 | setterm_background="black" 90 | splash_color_summary="#${splash_hex}" 91 | else 92 | case "${splash_color_lower}" in 93 | black|blue|cyan|green|magenta|red|white|yellow) 94 | setterm_background="${splash_color_lower}" 95 | ;; 96 | *) 97 | echo "Unsupported splash color '${splash_color_input}'. Use one of: black, blue, cyan, green, magenta, red, white, yellow, or #RRGGBB." >&2 98 | exit 1 99 | ;; 100 | esac 101 | fi 102 | 103 | read -rp "Keep the desktop GUI running? [y/N]: " keep_gui 104 | keep_gui="$(echo "${keep_gui:-N}" | tr '[:upper:]' '[:lower:]')" 105 | 106 | use_x_env="n" 107 | if [[ "${keep_gui}" == "y" ]]; then 108 | read -rp "Add DISPLAY/XAUTHORITY variables to the service? [y/N]: " use_x_env 109 | use_x_env="$(echo "${use_x_env:-N}" | tr '[:upper:]' '[:lower:]')" 110 | fi 111 | 112 | detect_dm() { 113 | local candidate 114 | for candidate in gdm gdm3 lightdm; do 115 | if systemctl list-unit-files --type=service --no-legend | awk '{print $1}' | grep -qx "${candidate}.service"; then 116 | echo "${candidate}" 117 | return 118 | fi 119 | done 120 | echo "" 121 | } 122 | 123 | configure_display_blanking() { 124 | local x11_dir="/etc/X11/xorg.conf.d" 125 | local x11_conf="${x11_dir}/20-raspberry-ninja-nodpms.conf" 126 | local systemd_service="/etc/systemd/system/disable-console-blanking.service" 127 | local extlinux_conf="/boot/extlinux/extlinux.conf" 128 | 129 | echo 130 | echo "Configuring Jetson screen blanking settings..." 131 | echo " - X11 config : ${x11_conf}" 132 | echo " - Console service: ${systemd_service}" 133 | 134 | mkdir -p "${x11_dir}" 135 | 136 | cat <<'X11CONF' > "${x11_conf}" 137 | Section "Extensions" 138 | Option "DPMS" "Disable" 139 | EndSection 140 | 141 | Section "ServerFlags" 142 | Option "BlankTime" "0" 143 | Option "StandbyTime" "0" 144 | Option "SuspendTime" "0" 145 | Option "OffTime" "0" 146 | EndSection 147 | X11CONF 148 | 149 | chmod 644 "${x11_conf}" 150 | 151 | cat <<'SERVICECONF' > "${systemd_service}" 152 | [Unit] 153 | Description=Disable Linux console screen blanking for Raspberry Ninja 154 | After=multi-user.target 155 | 156 | [Service] 157 | Type=oneshot 158 | ExecStart=/bin/sh -c 'printf 0 | tee /sys/module/kernel/parameters/consoleblank >/dev/null; for tty in /dev/tty[0-9]*; do TERM=linux setterm --term linux --blank 0 --powerdown 0 --powersave off < "$tty"; done' 159 | 160 | [Install] 161 | WantedBy=multi-user.target 162 | SERVICECONF 163 | 164 | chmod 644 "${systemd_service}" 165 | 166 | if [[ -f "${extlinux_conf}" ]] && ! grep -q 'consoleblank=0' "${extlinux_conf}"; then 167 | sed -i '/^[[:space:]]*APPEND / s/$/ consoleblank=0/' "${extlinux_conf}" 168 | fi 169 | 170 | systemctl daemon-reload 171 | systemctl enable disable-console-blanking.service 172 | 173 | local console_service_status="started" 174 | if systemctl start disable-console-blanking.service; then 175 | console_service_status="started" 176 | else 177 | console_service_status="failed" 178 | fi 179 | 180 | local display_manager_unit="display-manager.service" 181 | local display_manager_status="not-found" 182 | if systemctl list-unit-files --type=service "${display_manager_unit}" >/dev/null 2>&1; then 183 | display_manager_status="inactive" 184 | if systemctl is-active --quiet "${display_manager_unit}"; then 185 | if systemctl restart "${display_manager_unit}"; then 186 | display_manager_status="restarted" 187 | else 188 | display_manager_status="restart-failed" 189 | fi 190 | fi 191 | fi 192 | 193 | echo "Screen blanking timers have been disabled:" 194 | echo " - Console blanking service status : ${console_service_status}" 195 | 196 | if [[ "${console_service_status}" == "failed" ]]; then 197 | echo "The disable-console-blanking service failed to start; run 'systemctl status disable-console-blanking.service' for details." >&2 198 | blanking_status="Failed (disable-console-blanking service)" 199 | exit 1 200 | fi 201 | 202 | case "${display_manager_status}" in 203 | restarted) 204 | echo " - Display manager restarted to load X11 DPMS settings." 205 | ;; 206 | inactive) 207 | echo " - Display manager not running; restart later to load X11 settings." 208 | ;; 209 | restart-failed) 210 | echo " - Display manager restart failed; check 'journalctl -u ${display_manager_unit}'." 211 | ;; 212 | not-found) 213 | echo " - Display manager service not found; reboot if you use a GUI." 214 | ;; 215 | esac 216 | 217 | echo 218 | echo "Rebooting still applies everything cleanly, but the changes should already be in effect." 219 | echo 220 | 221 | blanking_status="Disabled (console service ${console_service_status}; display manager ${display_manager_status})" 222 | } 223 | 224 | env_lines="" 225 | if [[ "${use_x_env}" == "y" ]]; then 226 | read -rp "DISPLAY value [:0]: " display_value 227 | display_value="${display_value:-:0}" 228 | read -rp "Path to XAUTHORITY file [${user_home}/.Xauthority]: " xauth_path 229 | xauth_path="${xauth_path:-${user_home}/.Xauthority}" 230 | env_lines+="Environment=DISPLAY=${display_value}"$'\n' 231 | env_lines+="Environment=XAUTHORITY=${xauth_path}"$'\n' 232 | env_lines+="Environment=XDG_RUNTIME_DIR=/run/user/$(id -u "${service_user}")"$'\n' 233 | fi 234 | 235 | read -rp "Disable screen blanking and power saving timers? [Y/n]: " disable_blanking 236 | disable_blanking="$(echo "${disable_blanking:-Y}" | tr '[:upper:]' '[:lower:]')" 237 | if [[ "${disable_blanking}" == "n" ]]; then 238 | disable_blanking_choice="n" 239 | blanking_status="Skipped (user opted out)" 240 | else 241 | disable_blanking_choice="y" 242 | blanking_status="Pending" 243 | fi 244 | 245 | read -rp "Hide cursor and lock keyboard input on ${playback_tty}? This makes the local console unresponsive until the service stops. [y/N]: " disable_local_input 246 | disable_local_input="$(echo "${disable_local_input:-N}" | tr '[:upper:]' '[:lower:]')" 247 | 248 | setterm_cursor_flag="" 249 | input_lock_summary="disabled (local keyboard active)" 250 | restore_shell_commands=() 251 | 252 | if [[ "${disable_local_input}" == "y" ]]; then 253 | setterm_cursor_flag="--cursor off" 254 | input_lock_summary="enabled (requires remote access)" 255 | restore_shell_commands+=("${stty_path} -F ${playback_tty} sane") 256 | restore_shell_commands+=("TERM=linux ${setterm_path} --term linux --cursor on >${playback_tty}") 257 | fi 258 | 259 | service_name="raspberry-ninja" 260 | service_path="/etc/systemd/system/${service_name}.service" 261 | 262 | if [[ -f "${service_path}" ]]; then 263 | read -rp "${service_path} exists. Overwrite? [y/N]: " overwrite 264 | overwrite="$(echo "${overwrite:-N}" | tr '[:upper:]' '[:lower:]')" 265 | if [[ "${overwrite}" != "y" ]]; then 266 | echo "Aborting without changes." 267 | exit 1 268 | fi 269 | fi 270 | 271 | pre_shell_commands=() 272 | if [[ "${disable_local_input}" == "y" ]]; then 273 | pre_shell_commands+=("${stty_path} -F ${playback_tty} -echo -icanon") 274 | fi 275 | 276 | setterm_args="--term linux --clear all --foreground ${setterm_background} --background ${setterm_background}" 277 | if [[ -n "${setterm_cursor_flag}" ]]; then 278 | setterm_args+=" ${setterm_cursor_flag}" 279 | fi 280 | 281 | if [[ -n "${palette_hex}" ]]; then 282 | pre_shell_commands+=("printf '\\033]P0${palette_hex}\\033\\\\' >${playback_tty}") 283 | fi 284 | 285 | pre_shell_commands+=("TERM=linux ${setterm_path} ${setterm_args} >${playback_tty}") 286 | 287 | escaped_start_cmd="$(escape_for_single_quotes "${start_command}")" 288 | { 289 | echo "[Unit]" 290 | echo "Description=Raspberry Ninja Autostart (Jetson)" 291 | echo "After=network-online.target" 292 | echo "Wants=network-online.target" 293 | echo 294 | echo "[Service]" 295 | echo "Type=simple" 296 | echo "User=${service_user}" 297 | echo "Restart=always" 298 | echo "RestartSec=5" 299 | if [[ -n "${env_lines}" ]]; then 300 | printf "%s" "${env_lines}" 301 | fi 302 | echo "PermissionsStartOnly=yes" 303 | echo "TTYPath=${playback_tty}" 304 | echo "TTYReset=yes" 305 | echo "TTYVTDisallocate=yes" 306 | echo "StandardInput=tty" 307 | echo "StandardOutput=journal" 308 | echo "StandardError=journal" 309 | echo "ExecStartPre=${chvt_path} ${playback_vt_number}" 310 | if [[ "${#pre_shell_commands[@]}" -gt 0 ]]; then 311 | for cmd in "${pre_shell_commands[@]}"; do 312 | escaped_pre="$(escape_for_single_quotes "${cmd}")" 313 | echo "ExecStartPre=/bin/sh -c '${escaped_pre}'" 314 | done 315 | fi 316 | if [[ "${#restore_shell_commands[@]}" -gt 0 ]]; then 317 | for cmd in "${restore_shell_commands[@]}"; do 318 | escaped_restore="$(escape_for_single_quotes "${cmd}")" 319 | echo "ExecStopPost=/bin/sh -c '${escaped_restore}'" 320 | done 321 | fi 322 | echo "ExecStart=/bin/bash -lc '${escaped_start_cmd}'" 323 | echo 324 | echo "[Install]" 325 | echo "WantedBy=multi-user.target" 326 | } > "${service_path}" 327 | 328 | chmod 644 "${service_path}" 329 | systemctl daemon-reload 330 | systemctl enable "${service_name}.service" 331 | 332 | if systemctl list-unit-files --type=service --no-legend | awk '{print $1}' | grep -qx "${playback_getty_unit}"; then 333 | systemctl stop "${playback_getty_unit}" >/dev/null 2>&1 || true 334 | systemctl mask "${playback_getty_unit}" >/dev/null 2>&1 || true 335 | playback_console_status="getty masked" 336 | else 337 | playback_console_status="no getty service" 338 | fi 339 | 340 | dm_service="$(detect_dm)" 341 | gui_status="kept" 342 | 343 | if [[ "${keep_gui}" != "y" ]]; then 344 | echo "Configuring system to boot without the desktop GUI..." 345 | systemctl set-default multi-user.target 346 | if [[ -n "${dm_service}" ]]; then 347 | systemctl disable "${dm_service}.service" >/dev/null 2>&1 || true 348 | systemctl stop "${dm_service}.service" >/dev/null 2>&1 || true 349 | gui_status="disabled (${dm_service})" 350 | else 351 | gui_status="disabled (no display manager detected)" 352 | fi 353 | else 354 | systemctl set-default graphical.target 355 | if [[ -n "${dm_service}" ]]; then 356 | systemctl enable "${dm_service}.service" >/dev/null 2>&1 || true 357 | fi 358 | fi 359 | 360 | if [[ "${disable_blanking_choice}" == "y" ]]; then 361 | configure_display_blanking 362 | if [[ "${blanking_status}" == "Pending" ]]; then 363 | blanking_status="Disabled" 364 | fi 365 | fi 366 | 367 | cat </dev/null || nproc || echo 4)} 5 | PYTHON_VERSION=${PYTHON_VERSION:-3.11.9} 6 | MESON_VERSION=${MESON_VERSION:-1.4.1} 7 | GSTREAMER_VERSION=${GSTREAMER_VERSION:-1.26.7} 8 | GSTREAMER_SERIES="${GSTREAMER_VERSION%.*}" 9 | GLIB_VERSION=${GLIB_VERSION:-2.80.5} 10 | GOBJECT_INTROSPECTION_VERSION=${GOBJECT_INTROSPECTION_VERSION:-1.80.1} 11 | GLIB_SERIES="${GLIB_VERSION%.*}" 12 | GOBJECT_INTROSPECTION_SERIES="${GOBJECT_INTROSPECTION_VERSION%.*}" 13 | LIBCAMERA_TAG=${LIBCAMERA_TAG:-v0.3.2} 14 | SRT_TAG=${SRT_TAG:-v1.5.3} 15 | LIBSRTP_TAG=${LIBSRTP_TAG:-v2.6.0} 16 | LIBNICE_TAG=${LIBNICE_TAG:-0.1.21} 17 | LIBVPX_VERSION=${LIBVPX_VERSION:-1.13.1} 18 | DAV1D_VERSION=${DAV1D_VERSION:-1.3.0} 19 | KVAZAAR_VERSION=${KVAZAAR_VERSION:-2.3.0} 20 | FFMPEG_TAG=${FFMPEG_TAG:-n7.0} 21 | X264_GIT_REF=${X264_GIT_REF:-stable} 22 | PYTHON_PREFIX=${PYTHON_PREFIX:-/usr/local} 23 | PYTHON_BIN="${PYTHON_PREFIX}/bin/python${PYTHON_VERSION%.*}" 24 | INCLUDE_DISTRO_UPGRADE=${INCLUDE_DISTRO_UPGRADE:-0} 25 | 26 | prepend_env_path() { 27 | local var="$1" 28 | local value="$2" 29 | local current="${!var:-}" 30 | if [[ -z "${current}" ]]; then 31 | printf -v "${var}" '%s' "${value}" 32 | elif [[ ":${current}:" != *":${value}:"* ]]; then 33 | printf -v "${var}" '%s' "${value}:${current}" 34 | fi 35 | export "${var}" 36 | } 37 | 38 | DEBIAN_FRONTEND=${DEBIAN_FRONTEND:-noninteractive} 39 | export DEBIAN_FRONTEND 40 | 41 | prepend_env_path PATH "${PYTHON_PREFIX}/bin" 42 | prepend_env_path PATH "/usr/local/sbin" 43 | prepend_env_path PATH "/usr/local/bin" 44 | prepend_env_path PKG_CONFIG_PATH "/usr/local/lib/pkgconfig" 45 | prepend_env_path PKG_CONFIG_PATH "/usr/local/share/pkgconfig" 46 | prepend_env_path LD_LIBRARY_PATH "/usr/local/lib" 47 | prepend_env_path LIBRARY_PATH "/usr/local/lib" 48 | prepend_env_path CMAKE_PREFIX_PATH "/usr/local" 49 | prepend_env_path PYTHONPATH "${PYTHON_PREFIX}/lib/python${PYTHON_VERSION%.*}/site-packages" 50 | 51 | ### Updated for 2024 builds targeting Jetson Nano 2GB/4GB with JetPack 4 base images 52 | ## Manual involvement is needed at steps... 53 | 54 | cd ~ 55 | mkdir -p nvgst 56 | if [[ -d /usr/lib/aarch64-linux-gnu/gstreamer-1.0 ]]; then 57 | sudo cp -a /usr/lib/aarch64-linux-gnu/gstreamer-1.0/libgstomx.so ./nvgst/ || true 58 | sudo cp -a /usr/lib/aarch64-linux-gnu/gstreamer-1.0/libgstnv* ./nvgst/ || true 59 | fi 60 | 61 | if [[ "${INCLUDE_DISTRO_UPGRADE}" == "1" ]]; then 62 | echo "[info] Running distro-upgrade block (INCLUDE_DISTRO_UPGRADE=1)." 63 | sudo apt-get update 64 | sudo apt autoremove -y || true 65 | sudo apt-get -f install || true 66 | sudo apt-get upgrade -y 67 | sudo apt-get dist-upgrade -y 68 | else 69 | echo "[info] Skipping legacy distro upgrade steps (default). Set INCLUDE_DISTRO_UPGRADE=1 to enable." 70 | sudo apt-get update 71 | fi 72 | 73 | APT_BUILD_PACKAGES=( 74 | apt-transport-https 75 | autoconf 76 | automake 77 | autopoint 78 | autotools-dev 79 | bison 80 | build-essential 81 | ca-certificates 82 | ccache 83 | checkinstall 84 | cmake 85 | curl 86 | flex 87 | git 88 | libgdbm-compat-dev 89 | libgdbm-dev 90 | libboost-dev 91 | libncurses5-dev 92 | libncursesw5-dev 93 | libnss3-dev 94 | libgnutls28-dev 95 | libkmod-dev 96 | libreadline-dev 97 | libsqlite3-dev 98 | libtiff5-dev 99 | libudev-dev 100 | libtool 101 | pkg-config 102 | python3 103 | python3-dev 104 | python3-pip 105 | python3-rtmidi 106 | tar 107 | tk-dev 108 | unzip 109 | uuid-dev 110 | wget 111 | yasm 112 | ) 113 | 114 | APT_MEDIA_PACKAGES=( 115 | desktop-file-utils 116 | doxygen 117 | fonts-freefont-ttf 118 | freeglut3 119 | graphviz 120 | gtk-doc-tools 121 | imagemagick 122 | iso-codes 123 | ladspa-sdk 124 | libaa1-dev 125 | liba52-0.7.4-dev 126 | libaom-dev 127 | libasound2-dev 128 | libass-dev 129 | libatk-bridge2.0-dev 130 | libatk1.0-dev 131 | libavc1394-dev 132 | libavcodec-dev 133 | libavdevice-dev 134 | libavfilter-dev 135 | libavformat-dev 136 | libavutil-dev 137 | libbz2-dev 138 | libc6 139 | libc6-dev 140 | libcaca-dev 141 | libcairo2-dev 142 | libcdaudio-dev 143 | libcdio-dev 144 | libcdparanoia-dev 145 | libcap-dev 146 | libcurl4-openssl-dev 147 | libdca-dev 148 | libdbus-1-dev 149 | libdc1394-22-dev 150 | libdrm-dev 151 | libdv4-dev 152 | libdvdnav-dev 153 | libdvdread-dev 154 | libdw-dev 155 | libegl1-mesa-dev 156 | libelf-dev 157 | libepoxy-dev 158 | libexempi-dev 159 | libexif-dev 160 | libfaad-dev 161 | libffi-dev 162 | libflac-dev 163 | libfreetype6-dev 164 | libgdk-pixbuf2.0-dev 165 | libgif-dev 166 | libgirepository1.0-dev 167 | libgl1-mesa-dev 168 | libglib2.0-dev 169 | libgles2-mesa-dev 170 | libgtk-3-dev 171 | libgme-dev 172 | libgmp-dev 173 | libgsl0-dev 174 | libgsm1-dev 175 | libgudev-1.0-dev 176 | libgupnp-igd-1.0-dev 177 | libiec61883-dev 178 | libraw1394-dev 179 | libiptcdata0-dev 180 | libjpeg-dev 181 | libjson-glib-dev 182 | libkate-dev 183 | liblzma-dev 184 | libmad0-dev 185 | libmodplug-dev 186 | libmms-dev 187 | libmount-dev 188 | libmp3lame-dev 189 | libmpcdec-dev 190 | libmpg123-dev 191 | libmpeg2-4-dev 192 | libnuma-dev 193 | libnuma1 194 | libogg-dev 195 | libomxil-bellagio-dev 196 | libopencore-amrnb-dev 197 | libopencore-amrwb-dev 198 | libopus-dev 199 | liborc-0.4-dev 200 | libofa0-dev 201 | libpango1.0-dev 202 | libpng-dev 203 | libpulse-dev 204 | librsvg2-dev 205 | librtmp-dev 206 | libsdl2-dev 207 | libsdl2-image-dev 208 | libsdl2-mixer-dev 209 | libsdl2-net-dev 210 | libsdl2-ttf-dev 211 | libselinux-dev 212 | libshout3-dev 213 | libsidplay1-dev 214 | libsnappy-dev 215 | libsoxr-dev 216 | libsoup2.4-dev 217 | libsoundtouch-dev 218 | libsndfile1-dev 219 | libspandsp-dev 220 | libspeex-dev 221 | libssh-dev 222 | libssl-dev 223 | libtag1-dev 224 | libtheora-dev 225 | libtwolame-dev 226 | libunwind-dev 227 | libusb-1.0-0-dev 228 | libva-dev 229 | libv4l-dev 230 | libvdpau-dev 231 | libvisual-0.4-dev 232 | libvo-amrwbenc-dev 233 | libvorbis-dev 234 | libvorbisidec-dev 235 | libopenjp2-7-dev 236 | libvpx-dev 237 | libwavpack-dev 238 | libwebrtc-audio-processing-dev 239 | libwebp-dev 240 | libx11-dev 241 | libx264-dev 242 | libx265-dev 243 | libxcb-shape0-dev 244 | libxcb-shm0-dev 245 | libxcb-xfixes0-dev 246 | libxcb1-dev 247 | libxt-dev 248 | libxkbcommon-dev 249 | libxml2-dev 250 | libxvidcore-dev 251 | libyaml-dev 252 | libzbar-dev 253 | libzvbi-dev 254 | zlib1g-dev 255 | policykit-1-gnome 256 | pulseaudio 257 | python3-jinja2 258 | python3-ply 259 | python3-yaml 260 | shared-mime-info 261 | texinfo 262 | tclsh 263 | wayland-protocols 264 | ) 265 | 266 | sudo apt-get install -y "${APT_BUILD_PACKAGES[@]}" 267 | sudo apt-get install -y "${APT_MEDIA_PACKAGES[@]}" 268 | sudo apt-get autoremove -y 269 | 270 | if ! command -v "${PYTHON_BIN}" >/dev/null 2>&1 || [[ "$("${PYTHON_BIN}" -V 2>/dev/null)" != "Python ${PYTHON_VERSION}" ]]; then 271 | cd ~ 272 | rm -rf "Python-${PYTHON_VERSION}" python.tgz 273 | wget "https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz" -O python.tgz 274 | tar -xf python.tgz 275 | cd "Python-${PYTHON_VERSION}" 276 | ./configure --prefix="${PYTHON_PREFIX}" --enable-optimizations --with-lto --enable-shared 277 | make -j"${JOBS}" 278 | sudo make altinstall 279 | sudo ldconfig 280 | cd ~ 281 | rm -rf "Python-${PYTHON_VERSION}" python.tgz 282 | fi 283 | 284 | sudo "${PYTHON_BIN}" -m ensurepip --upgrade 285 | sudo "${PYTHON_BIN}" -m pip install --upgrade pip setuptools wheel 286 | sudo "${PYTHON_BIN}" -m pip install --upgrade scikit-build ninja websockets jinja2 PyYAML mako ply 287 | export GIT_SSL_NO_VERIFY=1 288 | 289 | ### MESON - specific version 290 | sudo "${PYTHON_BIN}" -m pip install --upgrade "meson==${MESON_VERSION}" 291 | sudo "${PYTHON_BIN}" -m pip install --upgrade pycairo 292 | 293 | # AAC - optional (needed for rtmp only really) 294 | cd ~ 295 | rm -rf fdk-aac 296 | git clone --depth 1 https://github.com/mstorsjo/fdk-aac.git 297 | cd fdk-aac 298 | autoreconf -fiv 299 | ./configure --prefix=/usr/local 300 | make -j"${JOBS}" 301 | sudo make install 302 | sudo ldconfig 303 | 304 | # AV1 - optional 305 | cd ~ 306 | rm -rf dav1d 307 | git clone --depth 1 --branch "${DAV1D_VERSION}" https://code.videolan.org/videolan/dav1d.git 308 | cd dav1d 309 | meson setup build --buildtype=release --prefix=/usr/local \ 310 | -Denable_tools=false \ 311 | -Denable_tests=false \ 312 | -Denable_docs=false \ 313 | -Denable_examples=false 314 | ninja -C build -j "${JOBS}" 315 | sudo ninja -C build -j "${JOBS}" install 316 | sudo ldconfig 317 | 318 | # HEVC - optional 319 | cd ~ 320 | rm -rf kvazaar 321 | git clone --depth 1 --branch "v${KVAZAAR_VERSION}" https://github.com/ultravideo/kvazaar.git 322 | cd kvazaar 323 | git submodule update --init --recursive || true 324 | cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local -DBUILD_SHARED_LIBS=ON 325 | cmake --build build -j"${JOBS}" 326 | sudo cmake --install build 327 | sudo ldconfig 328 | 329 | # H.264 - from source for latest optimisations 330 | cd ~ 331 | rm -rf x264 332 | git clone --depth 1 --branch "${X264_GIT_REF}" https://code.videolan.org/videolan/x264.git 333 | cd x264 334 | ./configure --prefix=/usr/local --enable-pic --enable-shared --enable-strip 335 | make -j"${JOBS}" 336 | sudo make install 337 | sudo ldconfig 338 | 339 | # VP8/VP9 - multi-thread capable build 340 | cd ~ 341 | rm -rf libvpx 342 | git clone --depth 1 --branch "v${LIBVPX_VERSION}" https://github.com/webmproject/libvpx.git 343 | cd libvpx 344 | ./configure --prefix=/usr/local --enable-vp8 --enable-vp9 --enable-shared --enable-pic --enable-multithread --enable-runtime-cpu-detect --disable-examples --disable-unit-tests --disable-docs 345 | make -j"${JOBS}" 346 | sudo make install 347 | sudo ldconfig 348 | 349 | # SRT - optional 350 | cd ~ 351 | rm -rf srt 352 | git clone --branch "${SRT_TAG}" --depth 1 https://github.com/Haivision/srt.git 353 | cd srt 354 | cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local -DENABLE_SHARED=ON -DENABLE_STATIC=OFF 355 | cmake --build build -j"${JOBS}" 356 | sudo cmake --install build 357 | sudo ldconfig 358 | 359 | ### FFMPEG 360 | cd ~ 361 | [ ! -d FFmpeg ] && git clone --branch "${FFMPEG_TAG}" --depth 1 https://github.com/FFmpeg/FFmpeg.git 362 | cd FFmpeg 363 | git fetch --tags 364 | git checkout "${FFMPEG_TAG}" 365 | make distclean || true 366 | ./configure \ 367 | --prefix=/usr/local \ 368 | --arch=aarch64 \ 369 | --extra-cflags="-I/usr/local/include" \ 370 | --extra-ldflags="-L/usr/local/lib" \ 371 | --extra-libs="-lpthread -lm -latomic" \ 372 | --enable-gpl \ 373 | --enable-version3 \ 374 | --enable-nonfree \ 375 | --enable-shared \ 376 | --disable-static \ 377 | --enable-pthreads \ 378 | --enable-libfdk-aac \ 379 | --enable-libx264 \ 380 | --enable-libx265 \ 381 | --enable-libvpx \ 382 | --enable-libdav1d \ 383 | --enable-libkvazaar \ 384 | --enable-libaom \ 385 | --enable-libsrt \ 386 | --enable-librtmp \ 387 | --enable-libopus \ 388 | --enable-libvorbis \ 389 | --enable-libmp3lame \ 390 | --enable-libass \ 391 | --enable-libfreetype \ 392 | --enable-libwebp \ 393 | --enable-libsnappy \ 394 | --enable-libsoxr \ 395 | --enable-libssh \ 396 | --enable-libxml2 \ 397 | --enable-libdrm \ 398 | --enable-libopencore-amrnb \ 399 | --enable-libopencore-amrwb \ 400 | --enable-libvo-amrwbenc \ 401 | --enable-libpulse \ 402 | --enable-omx \ 403 | --enable-indev=alsa \ 404 | --enable-outdev=alsa \ 405 | --enable-hardcoded-tables \ 406 | --enable-openssl 407 | make -j"${JOBS}" 408 | sudo make install 409 | sudo ldconfig 410 | 411 | sudo apt-get remove gstreamer1.0* -y 412 | sudo rm -rf /usr/lib/aarch64-linux-gnu/gstreamer-1.0/ 413 | sudo rm -rf /usr/lib/gst* 414 | sudo rm -rf /usr/bin/gst* 415 | sudo rm -rf /usr/include/gstreamer-1.0 416 | sudo rm -rf /usr/local/lib/aarch64-linux-gnu/gstreamer-1.0/ 417 | 418 | cd ~ 419 | rm -rf "glib-${GLIB_VERSION}" glib.tar.xz 420 | wget "https://download.gnome.org/sources/glib/${GLIB_SERIES}/glib-${GLIB_VERSION}.tar.xz" -O glib.tar.xz 421 | tar -xvf glib.tar.xz 422 | cd "glib-${GLIB_VERSION}" 423 | mkdir build 424 | cd build 425 | meson --prefix=/usr/local -Dman=false .. 426 | ninja -j "${JOBS}" 427 | sudo ninja -j "${JOBS}" install 428 | sudo ldconfig 429 | 430 | cd ~ 431 | rm -rf usrsctp 432 | git clone --depth 1 https://github.com/sctplab/usrsctp.git 433 | cd usrsctp 434 | git fetch --tags 435 | git checkout master 436 | rm -rf build 437 | meson setup build --prefix=/usr/local --buildtype=release 438 | ninja -C build -j "${JOBS}" 439 | sudo ninja -C build -j "${JOBS}" install 440 | sudo ldconfig 441 | 442 | cd ~ 443 | rm -f gobject.tar.xz 444 | wget "https://download.gnome.org/sources/gobject-introspection/${GOBJECT_INTROSPECTION_SERIES}/gobject-introspection-${GOBJECT_INTROSPECTION_VERSION}.tar.xz" -O gobject.tar.xz 445 | sudo rm -rf "gobject-introspection-${GOBJECT_INTROSPECTION_VERSION}" || true 446 | tar -xvf gobject.tar.xz 447 | cd "gobject-introspection-${GOBJECT_INTROSPECTION_VERSION}" 448 | rm -rf build 449 | meson setup build --prefix=/usr/local --buildtype=release 450 | ninja -C build -j "${JOBS}" 451 | sudo ninja -C build -j "${JOBS}" install 452 | sudo ldconfig 453 | systemctl --user enable pulseaudio.socket || true 454 | 455 | cd ~ 456 | rm -rf libnice 457 | git clone --branch "${LIBNICE_TAG}" --depth 1 https://gitlab.freedesktop.org/libnice/libnice.git 458 | cd libnice 459 | git fetch --tags 460 | git checkout "${LIBNICE_TAG}" 461 | rm -rf build 462 | meson setup build --prefix=/usr/local --buildtype=release 463 | ninja -C build -j "${JOBS}" 464 | sudo ninja -C build -j "${JOBS}" install 465 | sudo ldconfig 466 | 467 | cd ~ 468 | rm -rf libsrtp 469 | git clone --branch "${LIBSRTP_TAG}" --depth 1 https://github.com/cisco/libsrtp.git 470 | cd libsrtp 471 | git fetch --tags 472 | git checkout "${LIBSRTP_TAG}" 473 | ./configure --enable-openssl --enable-pic --prefix=/usr/local 474 | make -j"${JOBS}" 475 | sudo make shared_library 476 | sudo make install -j"${JOBS}" 477 | sudo ldconfig 478 | 479 | cd ~ 480 | rm -rf gstreamer 481 | git clone https://gitlab.freedesktop.org/gstreamer/gstreamer.git 482 | cd gstreamer 483 | git checkout "refs/tags/${GSTREAMER_VERSION}" 484 | git submodule update --init --recursive 485 | rm -rf build 486 | meson setup build --prefix=/usr/local --buildtype=release \ 487 | -Dgst-plugins-base:gl_winsys=egl \ 488 | -Ddoc=disabled \ 489 | -Dtests=disabled \ 490 | -Dexamples=disabled \ 491 | -Dges=disabled \ 492 | -Ddevtools=disabled \ 493 | -Dauto_features=enabled \ 494 | -Dintrospection=enabled \ 495 | -Dlibav=disabled \ 496 | -Dgst-plugins-good:qt5=disabled \ 497 | -Dgst-plugins-good:qt6=disabled \ 498 | -Dgst-plugins-good:rpicamsrc=disabled \ 499 | -Dgstreamer:libunwind=disabled \ 500 | -Dgstreamer:libdw=disabled \ 501 | -Dgstreamer:dbghelp=disabled \ 502 | -Dgstreamer:ptp-helper=disabled \ 503 | -Dgst-plugins-bad:va=disabled \ 504 | -Dgst-plugins-bad:mse=disabled \ 505 | -Dgst-plugins-bad:build-gir=false \ 506 | -Dgst-plugins-base:gl-gir=false 507 | ninja -C build -j "${JOBS}" 508 | sudo ninja -C build -j "${JOBS}" install 509 | sudo ldconfig 510 | cd ~ 511 | rm -rf libcamera 512 | git clone https://git.libcamera.org/libcamera/libcamera.git 513 | cd libcamera 514 | git fetch --tags 515 | git checkout "${LIBCAMERA_TAG}" 516 | git submodule update --init --recursive 517 | rm -rf build 518 | meson setup build --buildtype=release --prefix=/usr/local 519 | ninja -C build -j "${JOBS}" 520 | sudo ninja -C build -j "${JOBS}" install ## too many cores and you'll crash a raspiberry pi zero 2 521 | sudo ldconfig 522 | cd ~ 523 | 524 | sudo mkdir -p /usr/local/lib/aarch64-linux-gnu/gstreamer-1.0 525 | for plugin in \ 526 | libgstnvarguscamerasrc.so \ 527 | libgstnvivafilter.so \ 528 | libgstnvv4l2camerasrc.so \ 529 | libgstnvvideocuda.so \ 530 | libgstomx.so \ 531 | libgstnvjpeg.so \ 532 | libgstnvvidconv.so \ 533 | libgstnvvideosink.so \ 534 | libgstnvtee.so \ 535 | libgstnvvideo4linux2.so \ 536 | libgstnvvideosinks.so; do 537 | if [[ -f "${HOME}/nvgst/${plugin}" ]]; then 538 | sudo cp "${HOME}/nvgst/${plugin}" /usr/local/lib/aarch64-linux-gnu/gstreamer-1.0/ 539 | fi 540 | done 541 | sudo rm -f /usr/local/lib/aarch64-linux-gnu/gstreamer-1.0/libgstnvcompositor.so # isn't compatible anymore 542 | -------------------------------------------------------------------------------- /.github/scripts/auto-release.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process'); 2 | const fs = require('fs').promises; 3 | const path = require('path'); 4 | const crypto = require('crypto'); 5 | 6 | // Configuration 7 | const VERSION_FILE = 'VERSION'; 8 | const CHANGELOG_FILE = 'CHANGELOG.md'; 9 | 10 | // Logging utility 11 | function log(level, message, context = {}) { 12 | const timestamp = new Date().toISOString(); 13 | console.log(`[${timestamp}] [${level.toUpperCase()}] ${message}`, context); 14 | } 15 | 16 | // Run shell command utility with proper escaping 17 | async function runCommand(command, options = {}) { 18 | log('debug', `Running command: ${command}`); 19 | return new Promise((resolve, reject) => { 20 | // Use shell: false when possible for security 21 | const execOptions = { 22 | maxBuffer: 10 * 1024 * 1024, 23 | ...options 24 | }; 25 | 26 | exec(command, execOptions, (error, stdout, stderr) => { 27 | if (error) { 28 | log('error', `Command failed: ${command}`, { error: error.message, stderr }); 29 | reject(new Error(`Command failed: ${command}\n${stderr}`)); 30 | return; 31 | } 32 | if (stderr && !stderr.includes('warning')) { 33 | log('warn', `Command stderr: ${command}`, { stderr }); 34 | } 35 | resolve(stdout.trim()); 36 | }); 37 | }); 38 | } 39 | 40 | // Escape shell arguments 41 | function escapeShellArg(arg) { 42 | return `'${arg.replace(/'/g, "'\\''")}'`; 43 | } 44 | 45 | // Parse semantic version 46 | function parseVersion(version) { 47 | const match = version.match(/^v?(\d+)\.(\d+)\.(\d+)(-(.+))?(\+(.+))?$/); 48 | if (!match) { 49 | throw new Error(`Invalid version format: ${version}`); 50 | } 51 | return { 52 | major: parseInt(match[1]), 53 | minor: parseInt(match[2]), 54 | patch: parseInt(match[3]), 55 | prerelease: match[5] || null, 56 | build: match[7] || null, 57 | full: version 58 | }; 59 | } 60 | 61 | // Compare semantic versions 62 | function compareVersions(v1, v2) { 63 | const ver1 = typeof v1 === 'string' ? parseVersion(v1) : v1; 64 | const ver2 = typeof v2 === 'string' ? parseVersion(v2) : v2; 65 | 66 | if (ver1.major !== ver2.major) return ver1.major - ver2.major; 67 | if (ver1.minor !== ver2.minor) return ver1.minor - ver2.minor; 68 | if (ver1.patch !== ver2.patch) return ver1.patch - ver2.patch; 69 | 70 | // Pre-release versions have lower precedence 71 | if (ver1.prerelease && !ver2.prerelease) return -1; 72 | if (!ver1.prerelease && ver2.prerelease) return 1; 73 | 74 | return 0; 75 | } 76 | 77 | // Format version string 78 | function formatVersion(major, minor, patch, prerelease = null) { 79 | let version = `${major}.${minor}.${patch}`; 80 | if (prerelease) { 81 | version += `-${prerelease}`; 82 | } 83 | return version; 84 | } 85 | 86 | // Get current version from VERSION file or git tags 87 | async function getCurrentVersion() { 88 | try { 89 | // First try to read VERSION file 90 | const versionContent = await fs.readFile(VERSION_FILE, 'utf8'); 91 | return versionContent.trim(); 92 | } catch (error) { 93 | log('info', 'VERSION file not found, checking git tags...'); 94 | 95 | // Fallback to git tags 96 | try { 97 | const tags = await runCommand('git tag -l "v*" --sort=-v:refname'); 98 | if (tags) { 99 | const latestTag = tags.split('\n')[0]; 100 | return latestTag.replace(/^v/, ''); 101 | } 102 | } catch (gitError) { 103 | log('warn', 'No git tags found'); 104 | } 105 | 106 | // Default to 0.0.0 if no version found 107 | log('info', 'No existing version found, starting at 0.0.0'); 108 | return '0.0.0'; 109 | } 110 | } 111 | 112 | // Analyze changes to determine version bump type 113 | async function analyzeChanges(lastCommitSha) { 114 | log('info', 'Analyzing changes to determine version bump...'); 115 | 116 | try { 117 | // Get the diff for the last commit 118 | const changedFiles = await runCommand(`git show --name-status --oneline ${lastCommitSha}`); 119 | const commitMessage = await runCommand(`git log -1 --pretty=%B ${lastCommitSha}`); 120 | 121 | // Check what files were changed 122 | const hasPublishPyChanges = changedFiles.includes('publish.py'); 123 | const hasInstallerChanges = changedFiles.match(/installer\.sh|setup.*\.sh/); 124 | const hasServiceChanges = changedFiles.includes('.service'); 125 | const hasDocChanges = changedFiles.match(/README|\.md$/); 126 | const hasConfigChanges = changedFiles.match(/\.yml|\.yaml|\.json|\.toml/); 127 | 128 | // Check commit message for keywords 129 | const commitLower = commitMessage.toLowerCase(); 130 | const isBreaking = commitLower.includes('breaking') || commitLower.includes('!:'); 131 | const isFeat = commitLower.startsWith('feat') || commitLower.includes('feature'); 132 | const isFix = commitLower.startsWith('fix'); 133 | const isPerf = commitLower.includes('perf') || commitLower.includes('performance'); 134 | const isRefactor = commitLower.includes('refactor'); 135 | 136 | // Get detailed diff for publish.py if it changed 137 | let publishPyAnalysis = { major: false, minor: false, patch: false }; 138 | if (hasPublishPyChanges) { 139 | try { 140 | const diff = await runCommand(`git show ${lastCommitSha} -- publish.py`); 141 | 142 | // Analyze the nature of changes in publish.py 143 | const addedLines = (diff.match(/^\+[^+]/gm) || []).length; 144 | const removedLines = (diff.match(/^-[^-]/gm) || []).length; 145 | const hasNewFunctions = diff.match(/^\+def\s+\w+/m); 146 | const hasNewClasses = diff.match(/^\+class\s+\w+/m); 147 | const hasApiChanges = diff.match(/websocket|rtmp|stream|publish|connect/i); 148 | const hasProtocolChanges = diff.match(/protocol|format|codec|encoding/i); 149 | 150 | // Determine impact 151 | if (isBreaking || hasProtocolChanges || removedLines > 50) { 152 | publishPyAnalysis.major = true; 153 | } else if (hasNewFunctions || hasNewClasses || hasApiChanges || addedLines > 30) { 154 | publishPyAnalysis.minor = true; 155 | } else { 156 | publishPyAnalysis.patch = true; 157 | } 158 | 159 | log('info', 'publish.py analysis', { 160 | addedLines, 161 | removedLines, 162 | hasNewFunctions: !!hasNewFunctions, 163 | hasNewClasses: !!hasNewClasses, 164 | hasApiChanges: !!hasApiChanges, 165 | hasProtocolChanges: !!hasProtocolChanges 166 | }); 167 | } catch (error) { 168 | log('warn', 'Could not analyze publish.py diff in detail', { error: error.message }); 169 | publishPyAnalysis.minor = true; // Default to minor for publish.py changes 170 | } 171 | } 172 | 173 | // Determine version bump 174 | let bumpType = 'patch'; // Default 175 | 176 | if (isBreaking || publishPyAnalysis.major) { 177 | bumpType = 'major'; 178 | } else if (isFeat || publishPyAnalysis.minor || hasInstallerChanges || hasServiceChanges) { 179 | bumpType = 'minor'; 180 | } else if (isFix || isPerf || isRefactor || publishPyAnalysis.patch || hasConfigChanges) { 181 | bumpType = 'patch'; 182 | } else if (hasDocChanges && !hasPublishPyChanges) { 183 | bumpType = 'none'; // Don't bump for docs-only changes 184 | } 185 | 186 | log('info', 'Version bump analysis complete', { 187 | bumpType, 188 | hasPublishPyChanges, 189 | commitType: commitLower.split(':')[0], 190 | isBreaking 191 | }); 192 | 193 | return { 194 | bumpType, 195 | hasPublishPyChanges, 196 | commitMessage, 197 | changedFiles 198 | }; 199 | } catch (error) { 200 | log('error', 'Error analyzing changes', { error: error.message }); 201 | throw error; 202 | } 203 | } 204 | 205 | // Generate release notes 206 | async function generateReleaseNotes(currentVersion, newVersion, lastCommitSha, analysis) { 207 | log('info', 'Generating release notes...'); 208 | 209 | try { 210 | // Get recent commits since last tag/version 211 | let commitsSinceLastRelease = ''; 212 | try { 213 | const lastTag = await runCommand('git describe --tags --abbrev=0 HEAD~1 2>/dev/null || echo ""'); 214 | if (lastTag) { 215 | commitsSinceLastRelease = await runCommand(`git log ${lastTag}..HEAD --pretty=format:"- %s (%h)" --no-merges`); 216 | } else { 217 | // If no previous tag, get last 10 commits 218 | commitsSinceLastRelease = await runCommand('git log -10 --pretty=format:"- %s (%h)" --no-merges'); 219 | } 220 | } catch (error) { 221 | log('warn', 'Could not get commits for release notes', { error: error.message }); 222 | } 223 | 224 | // Build release notes 225 | const releaseDate = new Date().toISOString().split('T')[0]; 226 | let releaseNotes = `# Release ${newVersion}\n\n`; 227 | releaseNotes += `**Release Date:** ${releaseDate}\n\n`; 228 | 229 | // Add summary based on bump type 230 | if (analysis.bumpType === 'major') { 231 | releaseNotes += `## 🚀 Major Release\n\n`; 232 | releaseNotes += `This release includes breaking changes or significant new features.\n\n`; 233 | } else if (analysis.bumpType === 'minor') { 234 | releaseNotes += `## ✨ Minor Release\n\n`; 235 | releaseNotes += `This release includes new features and improvements.\n\n`; 236 | } else { 237 | releaseNotes += `## 🐛 Patch Release\n\n`; 238 | releaseNotes += `This release includes bug fixes and minor improvements.\n\n`; 239 | } 240 | 241 | // Add specific changes for publish.py 242 | if (analysis.hasPublishPyChanges) { 243 | releaseNotes += `### Core Changes (publish.py)\n\n`; 244 | releaseNotes += `The main streaming script has been updated. `; 245 | 246 | if (analysis.bumpType === 'major') { 247 | releaseNotes += `This includes significant changes that may affect compatibility.\n\n`; 248 | } else if (analysis.bumpType === 'minor') { 249 | releaseNotes += `New features or improvements have been added.\n\n`; 250 | } else { 251 | releaseNotes += `Bug fixes or minor improvements have been applied.\n\n`; 252 | } 253 | } 254 | 255 | // Add commit list 256 | if (commitsSinceLastRelease) { 257 | releaseNotes += `### Commits\n\n`; 258 | releaseNotes += commitsSinceLastRelease + '\n\n'; 259 | } 260 | 261 | // Add installation reminder 262 | releaseNotes += `### Installation\n\n`; 263 | releaseNotes += `For installation instructions, please refer to the platform-specific guides:\n`; 264 | releaseNotes += `- [Raspberry Pi](./raspberry_pi/README.md)\n`; 265 | releaseNotes += `- [NVIDIA Jetson](./nvidia_jetson/README.md)\n`; 266 | releaseNotes += `- [Orange Pi](./orangepi/README.md)\n`; 267 | releaseNotes += `- [Ubuntu](./ubuntu/README.md)\n\n`; 268 | 269 | // Add upgrade notes if major version 270 | if (analysis.bumpType === 'major') { 271 | releaseNotes += `### ⚠️ Upgrade Notes\n\n`; 272 | releaseNotes += `This is a major version upgrade. Please review the changes carefully before updating.\n`; 273 | releaseNotes += `It's recommended to backup your configuration before upgrading.\n\n`; 274 | } 275 | 276 | return releaseNotes; 277 | } catch (error) { 278 | log('error', 'Error generating release notes', { error: error.message }); 279 | throw error; 280 | } 281 | } 282 | 283 | // Update CHANGELOG.md 284 | async function updateChangelog(releaseNotes) { 285 | log('info', 'Updating CHANGELOG.md...'); 286 | 287 | try { 288 | let existingChangelog = ''; 289 | try { 290 | existingChangelog = await fs.readFile(CHANGELOG_FILE, 'utf8'); 291 | } catch (error) { 292 | log('info', 'CHANGELOG.md not found, creating new one'); 293 | existingChangelog = '# Changelog\n\nAll notable changes to Raspberry Ninja will be documented in this file.\n\n'; 294 | } 295 | 296 | // Insert new release notes after the header 297 | const headerMatch = existingChangelog.match(/^#\s+Changelog.*?\n+/m); 298 | if (headerMatch) { 299 | const insertPosition = headerMatch.index + headerMatch[0].length; 300 | const updatedChangelog = 301 | existingChangelog.slice(0, insertPosition) + 302 | releaseNotes + '\n---\n\n' + 303 | existingChangelog.slice(insertPosition); 304 | 305 | await fs.writeFile(CHANGELOG_FILE, updatedChangelog); 306 | log('info', 'CHANGELOG.md updated successfully'); 307 | } else { 308 | // No header found, prepend 309 | await fs.writeFile(CHANGELOG_FILE, existingChangelog + '\n\n' + releaseNotes); 310 | log('info', 'CHANGELOG.md updated (appended)'); 311 | } 312 | } catch (error) { 313 | log('error', 'Error updating CHANGELOG.md', { error: error.message }); 314 | throw error; 315 | } 316 | } 317 | 318 | // Create GitHub release 319 | async function createGitHubRelease(version, releaseNotes) { 320 | log('info', `Creating GitHub release for v${version}...`); 321 | 322 | const tagName = `v${version}`; 323 | 324 | try { 325 | // Create and push tag 326 | await runCommand(`git tag -a ${tagName} -m "Release ${version}"`); 327 | await runCommand(`git push origin ${tagName}`); 328 | 329 | // Create release using GitHub CLI if available 330 | try { 331 | await runCommand('gh --version'); 332 | 333 | // Write release notes to temp file 334 | const tempFile = `.release-notes-${Date.now()}.tmp`; 335 | await fs.writeFile(tempFile, releaseNotes); 336 | 337 | // Create release 338 | await runCommand(`gh release create ${tagName} --title "Release ${version}" --notes-file ${tempFile}`); 339 | 340 | // Cleanup 341 | await fs.unlink(tempFile); 342 | 343 | log('info', `GitHub release created successfully: ${tagName}`); 344 | } catch (ghError) { 345 | log('warn', 'GitHub CLI not available, tag pushed but release must be created manually', { error: ghError.message }); 346 | } 347 | } catch (error) { 348 | log('error', 'Error creating GitHub release', { error: error.message }); 349 | throw error; 350 | } 351 | } 352 | 353 | // Main function 354 | async function main() { 355 | log('info', 'Starting auto-release process...'); 356 | 357 | try { 358 | // Get last commit info 359 | const lastCommitSha = await runCommand('git log -1 --pretty=%H'); 360 | const lastCommitMessage = await runCommand('git log -1 --pretty=%B'); 361 | 362 | // Skip if commit is already a release commit 363 | if (lastCommitMessage.includes('[release]') || lastCommitMessage.includes('[skip-release]')) { 364 | log('info', 'Skipping release for release-related commit'); 365 | process.exit(0); 366 | } 367 | 368 | // Analyze changes 369 | const analysis = await analyzeChanges(lastCommitSha); 370 | 371 | if (analysis.bumpType === 'none') { 372 | log('info', 'No version bump needed for this commit'); 373 | process.exit(0); 374 | } 375 | 376 | // Get current version 377 | const currentVersion = await getCurrentVersion(); 378 | const version = parseVersion(currentVersion); 379 | 380 | // Calculate new version 381 | let newMajor = version.major; 382 | let newMinor = version.minor; 383 | let newPatch = version.patch; 384 | 385 | switch (analysis.bumpType) { 386 | case 'major': 387 | newMajor++; 388 | newMinor = 0; 389 | newPatch = 0; 390 | break; 391 | case 'minor': 392 | newMinor++; 393 | newPatch = 0; 394 | break; 395 | case 'patch': 396 | newPatch++; 397 | break; 398 | } 399 | 400 | const newVersion = formatVersion(newMajor, newMinor, newPatch); 401 | log('info', `Version bump: ${currentVersion} → ${newVersion} (${analysis.bumpType})`); 402 | 403 | // Validate version is actually higher 404 | if (compareVersions(newVersion, currentVersion) <= 0) { 405 | log('error', `New version ${newVersion} is not higher than current version ${currentVersion}`); 406 | throw new Error('Version validation failed: new version must be higher than current version'); 407 | } 408 | 409 | // Update VERSION file 410 | await fs.writeFile(VERSION_FILE, newVersion); 411 | log('info', 'VERSION file updated'); 412 | 413 | // Generate release notes 414 | const releaseNotes = await generateReleaseNotes(currentVersion, newVersion, lastCommitSha, analysis); 415 | 416 | // Update CHANGELOG 417 | await updateChangelog(releaseNotes); 418 | 419 | // Commit version bump and changelog 420 | await runCommand('git add VERSION CHANGELOG.md'); 421 | await runCommand(`git commit -m "chore: release v${newVersion} [release]"`); 422 | await runCommand('git push'); 423 | 424 | // Create GitHub release 425 | await createGitHubRelease(newVersion, releaseNotes); 426 | 427 | log('info', `Release v${newVersion} completed successfully!`); 428 | process.exit(0); 429 | } catch (error) { 430 | log('error', 'Auto-release failed', { error: error.message, stack: error.stack }); 431 | process.exit(1); 432 | } 433 | } 434 | 435 | // Run main function 436 | main().catch(error => { 437 | log('error', 'Unhandled error in main', { error: error.message, stack: error.stack }); 438 | process.exit(1); 439 | }); --------------------------------------------------------------------------------