Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions src/framegrab/cli/balena_rtsp_tunnel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import os
import subprocess
from pathlib import Path

import click

COMMAND_NAME = "balena-rtsp-tunnel"


@click.command(COMMAND_NAME)
@click.argument("device_id")
@click.argument("rtsp_ip")
@click.argument("pem_file", required=False)
@click.option("-l", "--local-port", default=8554, help="Local port for RTSP stream")
@click.option("-r", "--remote-port", default=554, help="Remote RTSP port")
def balena_rtsp_tunnel(
device_id: str, rtsp_ip: str, pem_file: str = None, local_port: int = 8554, remote_port: int = 554
):
"""Tunnel an RTSP stream from a Balena device via SSH tunneling

\b
DEVICE_ID: Balena device ID
RTSP_IP: IP address of RTSP camera (e.g. 192.168.2.219)
PEM_FILE: Optional path to PEM file for SSH authentication
"""
if os.name == "nt":
raise RuntimeError(
f"{COMMAND_NAME} is not supported on Windows. Try running this command on a Unix-like system."
)

script_path = (
Path(__file__).parent / "balena_rtsp_tunnel.sh"
) # Poetry packages .sh files alongside .py files, so this works
cmd = [str(script_path), device_id, rtsp_ip, pem_file or "", str(local_port), str(remote_port)]
subprocess.run(cmd)
60 changes: 60 additions & 0 deletions src/framegrab/cli/balena_rtsp_tunnel.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/bin/bash

# Check arguments
if [ $# -lt 2 ]; then
echo "Usage: $0 <device-id> <rtsp-ip> [pem-file]"
echo "Example: $0 abc123def456 192.168.2.219 ~/.ssh/id_ed25519_balena"
exit 1
fi

DEVICE_ID="$1"
RTSP_IP="$2"
PEM_FILE="$3"
LOCAL_PORT="${4:-8554}"
REMOTE_PORT="${5:-554}"

# Check balena CLI
if ! command -v balena &>/dev/null; then
echo "Error: Balena CLI not found"
exit 1
fi

# Check login and get username
WHOAMI_OUTPUT=$(balena whoami 2>&1)
if [ $? -ne 0 ]; then
echo "$WHOAMI_OUTPUT"
exit 1
fi

USERNAME=$(echo "$WHOAMI_OUTPUT" | grep "USERNAME:" | awk '{print $2}')

# Cleanup function
cleanup() {
echo "Shutting down..."
pkill -f "balena device tunnel.*$DEVICE_ID" 2>/dev/null || true
pkill -f "ssh.*$USERNAME@localhost.*4321" 2>/dev/null || true
exit 0
}
trap cleanup SIGINT SIGTERM EXIT

# Start tunnels
echo "Starting tunnel to $DEVICE_ID..."
balena device tunnel "$DEVICE_ID" -p 22222:4321 &

sleep 3

# Build SSH command
SSH_CMD="ssh -N -L $LOCAL_PORT:$RTSP_IP:$REMOTE_PORT $USERNAME@localhost -p 4321 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
if [ -n "$PEM_FILE" ]; then
SSH_CMD="$SSH_CMD -i $PEM_FILE"
fi

$SSH_CMD &

echo "Tunnel established successfully from $RTSP_IP:$REMOTE_PORT!"
echo "RTSP should now be available at localhost:$LOCAL_PORT"
echo "Example: rtsp://username:password@localhost:$LOCAL_PORT/your-stream-path-here"
echo "Press Ctrl+C to stop"

# Wait
wait
3 changes: 2 additions & 1 deletion src/framegrab/cli/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
import click

from . import autodiscover, preview
from . import autodiscover, balena_rtsp_tunnel, preview


@click.group()
Expand All @@ -11,6 +11,7 @@ def climain():


climain.add_command(autodiscover.autodiscover)
climain.add_command(balena_rtsp_tunnel.balena_rtsp_tunnel)
climain.add_command(preview.preview)

if __name__ == "__main__":
Expand Down