Skip to content

Commit 65d8f87

Browse files
feat: Add Media Relay service with Docker configuration and streaming utility
1 parent 27a35df commit 65d8f87

File tree

7 files changed

+328
-0
lines changed

7 files changed

+328
-0
lines changed

.github/workflows/deploy.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ jobs:
2929
path: visual_controller
3030
application: "visual-controller"
3131
environment: "Visual-controller-env"
32+
- service: media-relay
33+
path: media_relay
34+
application: "media-relay"
35+
environment: "Media-relay-env"
3236
steps:
3337
- name: Checkout repository
3438
uses: actions/checkout@v4

media_relay/Dockerfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
FROM docker.io/bluenviron/mediamtx:1.7.1
2+
3+
COPY mediamtx.yaml /mediamtx.yaml
4+
5+
CMD ["/mediamtx", "--config", "/mediamtx.yaml"]

media_relay/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Media Relay Service
2+
3+
This folder contains the container bundle deployed to the dedicated Elastic Beanstalk environment that serves as the low-latency relay (`rtsp.nene.02labs.me`). The container ships [Mediamtx](https://github.com/bluenviron/mediamtx), providing RTSP ingest together with WebRTC and LL-HLS playback for the robot camera stream.
4+
5+
## Runtime configuration
6+
7+
The container reads the following environment variables that must be configured in the Elastic Beanstalk environment before deploying:
8+
9+
| Variable | Purpose |
10+
| --- | --- |
11+
| `RELAY_PUBLISH_USER` | Username the SBC uses while pushing the RTSP stream |
12+
| `RELAY_PUBLISH_PASS` | Password the SBC uses while pushing the RTSP stream |
13+
| `RELAY_VIEWER_USER` | Optional username required by frontend clients (leave empty to disable auth) |
14+
| `RELAY_VIEWER_PASS` | Optional password required by frontend clients (leave empty to disable auth) |
15+
16+
If you enable viewer authentication, update the frontend to use the same credentials when negotiating playback.
17+
18+
Ports exposed by the container:
19+
20+
- `8554/tcp`: RTSP ingest from the SBC
21+
- `1935/tcp`: RTMP ingest (alternative to RTSP)
22+
- `8888/tcp`: HTTP server (HLS playback and API)
23+
- `8889/tcp` plus `UDP 8200-8299`: WebRTC signalling and media
24+
- `9998/tcp`: Prometheus metrics (optional)
25+
- `9999/tcp`: pprof endpoint (optional)
26+
27+
Ensure the Elastic Beanstalk load balancer and security groups forward the required ports. For WebRTC, terminate TLS at the load balancer and forward HTTPS traffic to port `8889`.
28+
29+
## Deployment bundle
30+
31+
The GitHub Actions workflow zips the contents of this folder and uploads the archive as an application version. The Dockerfile simply copies the Mediamtx configuration and launches the server binary.

media_relay/mediamtx.yaml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Mediamtx configuration for the low-latency relay
2+
logLevel: info
3+
4+
rtsp:
5+
listenAddress: :8554
6+
7+
rtmp:
8+
listenAddress: :1935
9+
10+
hls:
11+
enabled: true
12+
listenAddress: :8888
13+
segmentDuration: 1s
14+
partDuration: 200ms
15+
16+
webrtc:
17+
enabled: true
18+
listenAddress: :8889
19+
candidates: [{type: "server", host: 0.0.0.0, portRange: [8200, 8299]}]
20+
iceServers:
21+
- url: stun:stun.l.google.com:19302
22+
23+
metrics:
24+
enabled: true
25+
listenAddress: :9998
26+
27+
pprof:
28+
enabled: true
29+
listenAddress: :9999
30+
31+
paths:
32+
robot:
33+
# Publisher credentials supplied via environment variables at runtime
34+
publishUser: '{{ env "RELAY_PUBLISH_USER" }}'
35+
publishPass: '{{ env "RELAY_PUBLISH_PASS" }}'
36+
# Viewer credentials (optional) supplied via environment variables
37+
readUser: '{{ env "RELAY_VIEWER_USER" }}'
38+
readPass: '{{ env "RELAY_VIEWER_PASS" }}'
39+
overridePublisher: yes

sbc_streamer/README.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Radxa SBC Streamer
2+
3+
This Go utility supervises an `ffmpeg` process that pushes the USB camera feed from the Radxa Rock 3C to the media relay hosted at `rtsp.nene.02labs.me`.
4+
5+
## Prerequisites
6+
7+
- Go 1.22 or newer (for building the binary)
8+
- `ffmpeg` installed on the SBC with hardware access to `/dev/video0`
9+
- Network connectivity from the SBC to the relay host on TCP port `8554`
10+
11+
## Configuration
12+
13+
The streamer is configured through environment variables (values shown with defaults):
14+
15+
| Variable | Description |
16+
| --- | --- |
17+
| `FFMPEG_BINARY=ffmpeg` | Path to the `ffmpeg` executable |
18+
| `CAMERA_DEVICE=/dev/video0` | Video4Linux device supplying the camera feed |
19+
| `AUDIO_DEVICE` | Optional ALSA device (e.g. `hw:1,0`) to include audio |
20+
| `INPUT_FORMAT` | Force a V4L2 input format (e.g. `mjpeg`, `yuyv422`) when autodetect fails |
21+
| `FRAME_RATE=30` | Capture frame rate |
22+
| `VIDEO_SIZE=1280x720` | Capture resolution |
23+
| `VIDEO_BITRATE=1500k` | Target video bitrate |
24+
| `STREAM_NAME=robot` | Path name on the relay |
25+
| `RELAY_HOST=rtsp.nene.02labs.me:8554` | Relay host (host:port) |
26+
| `RELAY_PUBLISH_USER` | Username for RTSP publish authentication |
27+
| `RELAY_PUBLISH_PASS` | Password for RTSP publish authentication |
28+
| `RTSP_TRANSPORT=tcp` | Transport to use when pushing (`tcp` or `udp`) |
29+
30+
## Building
31+
32+
```powershell
33+
# on the SBC or a build machine
34+
cd sbc_streamer
35+
go build -o streamer
36+
```
37+
38+
Copy the resulting `streamer` binary to the Radxa.
39+
40+
## Running as a service
41+
42+
1. Create an environment file `/opt/streamer/streamer.env` with the variables above. Example:
43+
44+
```bash
45+
RELAY_PUBLISH_USER=robot
46+
RELAY_PUBLISH_PASS=secret
47+
STREAM_NAME=robot
48+
```
49+
50+
2. Install the binary at `/opt/streamer/streamer` and make it executable.
51+
52+
3. Add a systemd unit (`/etc/systemd/system/robot-stream.service`):
53+
54+
```ini
55+
[Unit]
56+
Description=Robot camera RTSP uplink
57+
After=network.target
58+
59+
[Service]
60+
Type=simple
61+
EnvironmentFile=/opt/streamer/streamer.env
62+
ExecStart=/opt/streamer/streamer
63+
Restart=always
64+
RestartSec=3
65+
66+
[Install]
67+
WantedBy=multi-user.target
68+
```
69+
70+
4. Enable and start the service:
71+
72+
```bash
73+
sudo systemctl daemon-reload
74+
sudo systemctl enable --now robot-stream.service
75+
```
76+
77+
The service will restart automatically if `ffmpeg` exits and will reconnect after transient network failures.

sbc_streamer/go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/02loveslollipop/ESP32-L293D/sbc_streamer
2+
3+
go 1.22.3

sbc_streamer/main.go

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
"os"
8+
"os/exec"
9+
"os/signal"
10+
"syscall"
11+
"time"
12+
)
13+
14+
type streamerConfig struct {
15+
ffmpegBinary string
16+
cameraDevice string
17+
audioDevice string
18+
frameRate string
19+
resolution string
20+
bitrate string
21+
streamName string
22+
targetHost string
23+
publishUser string
24+
publishPass string
25+
rtspTransport string
26+
inputFormat string
27+
}
28+
29+
func main() {
30+
cfg := loadConfig()
31+
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
32+
defer cancel()
33+
34+
logger := log.New(os.Stdout, "streamer: ", log.LstdFlags|log.Lmicroseconds)
35+
logger.Printf("starting streamer with camera=%s target=%s stream=%s", cfg.cameraDevice, cfg.targetHost, cfg.streamName)
36+
if cfg.publishUser == "" {
37+
logger.Println("warning: RELAY_PUBLISH_USER is empty; publishing will fail if the relay requires authentication")
38+
}
39+
if cfg.publishUser != "" && cfg.publishPass == "" {
40+
logger.Println("warning: RELAY_PUBLISH_PASS is empty while RELAY_PUBLISH_USER is set")
41+
}
42+
43+
retryDelay := 3 * time.Second
44+
45+
for {
46+
err := runFFmpeg(ctx, cfg, logger)
47+
if ctx.Err() != nil {
48+
logger.Println("shutdown requested, exiting")
49+
return
50+
}
51+
logger.Printf("ffmpeg exited: %v", err)
52+
logger.Printf("retrying in %s", retryDelay)
53+
select {
54+
case <-time.After(retryDelay):
55+
case <-ctx.Done():
56+
logger.Println("shutdown requested during backoff, exiting")
57+
return
58+
}
59+
}
60+
}
61+
62+
func loadConfig() streamerConfig {
63+
return streamerConfig{
64+
ffmpegBinary: readEnv("FFMPEG_BINARY", "ffmpeg"),
65+
cameraDevice: readEnv("CAMERA_DEVICE", "/dev/video0"),
66+
audioDevice: os.Getenv("AUDIO_DEVICE"),
67+
frameRate: readEnv("FRAME_RATE", "30"),
68+
resolution: readEnv("VIDEO_SIZE", "1280x720"),
69+
bitrate: readEnv("VIDEO_BITRATE", "1500k"),
70+
streamName: readEnv("STREAM_NAME", "robot"),
71+
targetHost: readEnv("RELAY_HOST", "rtsp.nene.02labs.me:8554"),
72+
publishUser: readEnv("RELAY_PUBLISH_USER", ""),
73+
publishPass: readEnv("RELAY_PUBLISH_PASS", ""),
74+
rtspTransport: readEnv("RTSP_TRANSPORT", "tcp"),
75+
inputFormat: os.Getenv("INPUT_FORMAT"),
76+
}
77+
}
78+
79+
func runFFmpeg(ctx context.Context, cfg streamerConfig, logger *log.Logger) error {
80+
args := buildFFmpegArgs(cfg)
81+
logger.Printf("launching ffmpeg (%d args)", len(args))
82+
83+
cmd := exec.CommandContext(ctx, cfg.ffmpegBinary, args...)
84+
cmd.Stdout = os.Stdout
85+
cmd.Stderr = os.Stderr
86+
87+
if err := cmd.Start(); err != nil {
88+
return fmt.Errorf("failed to start ffmpeg: %w", err)
89+
}
90+
91+
return cmd.Wait()
92+
}
93+
94+
func buildFFmpegArgs(cfg streamerConfig) []string {
95+
args := []string{
96+
"-f", "v4l2",
97+
}
98+
if cfg.inputFormat != "" {
99+
args = append(args, "-input_format", cfg.inputFormat)
100+
}
101+
args = append(args,
102+
"-thread_queue_size", "256",
103+
"-framerate", cfg.frameRate,
104+
"-video_size", cfg.resolution,
105+
"-i", cfg.cameraDevice,
106+
)
107+
108+
if cfg.audioDevice != "" {
109+
audioArgs := []string{
110+
"-f", "alsa",
111+
"-thread_queue_size", "256",
112+
"-i", cfg.audioDevice,
113+
}
114+
args = append(args, audioArgs...)
115+
}
116+
117+
videoOut := []string{
118+
"-vf", "format=yuv420p",
119+
"-c:v", "libx264",
120+
"-preset", "ultrafast",
121+
"-tune", "zerolatency",
122+
"-pix_fmt", "yuv420p",
123+
"-b:v", cfg.bitrate,
124+
"-maxrate", cfg.bitrate,
125+
"-bufsize", cfg.bitrate,
126+
"-g", "60",
127+
"-keyint_min", "30",
128+
}
129+
args = append(args, videoOut...)
130+
131+
if cfg.audioDevice != "" {
132+
audioOut := []string{
133+
"-c:a", "aac",
134+
"-b:a", "128k",
135+
"-ar", "48000",
136+
"-ac", "2",
137+
}
138+
args = append(args, audioOut...)
139+
}
140+
141+
args = append(args,
142+
"-rtsp_transport", cfg.rtspTransport,
143+
"-f", "rtsp",
144+
)
145+
146+
rtspURL := buildRTSPURL(cfg)
147+
return append(args, rtspURL)
148+
}
149+
150+
func buildRTSPURL(cfg streamerConfig) string {
151+
var auth string
152+
if cfg.publishUser != "" {
153+
auth = cfg.publishUser
154+
if cfg.publishPass != "" {
155+
auth = fmt.Sprintf("%s:%s", cfg.publishUser, cfg.publishPass)
156+
}
157+
}
158+
if auth != "" {
159+
auth += "@"
160+
}
161+
return fmt.Sprintf("rtsp://%s%s/%s", auth, cfg.targetHost, cfg.streamName)
162+
}
163+
164+
func readEnv(key, fallback string) string {
165+
if v, ok := os.LookupEnv(key); ok && v != "" {
166+
return v
167+
}
168+
return fallback
169+
}

0 commit comments

Comments
 (0)