IP Cameras
Skynet can serve live video from cameras at your site — a pier cam aimed at the mount, an all-sky cam, a dome/room cam — to anyone with permission to view the telescope, straight in the browser.
The design follows the same posture as SkyNode itself: your site only ever
makes outbound connections. A publisher on your LAN pulls each camera once
and pushes it out to a UNC-operated streaming origin
(stream.skynetgo.org), which fans the stream out to as many web viewers as
connect. No inbound firewall rule, no port-forward, no static IP — the same as
the SkyNode link you already run. Your uplink carries exactly one stream per
camera regardless of how many people are watching.
This page covers the two owner tasks: registering a camera (which issues publish credentials) and pointing a publisher at it with ffmpeg and/or Blue Iris.
1. Register a camera
Cameras attach to either a telescope (e.g. a pier cam) or an observatory (e.g. an all-sky or room cam). Open the parent's IP Cameras tab to manage them.
Creating and editing a camera requires telescope/observatory update permission; deleting requires manage. Owners hold both implicitly.
Click Add camera and fill in:
| Field | Notes |
|---|---|
| Name | A label, e.g. "Dome cam". |
| Role | pier (aimed at the telescope), all_sky, finder/guide, room/interior, or other. |
| Vendor | Optional, e.g. "Amcrest". |
| PTZ | Whether the camera pans/tilts/zooms (informational for now). |
| Primary | Marks the camera shown first for the parent. |
You do not enter a URL. On save, the server provisions the camera, creates
its webrtc + hls playback streams automatically, generates a per-camera
publish secret, and displays a one-time Publish setup panel.
The publish credentials (shown once)
The Publish setup dialog provides everything the publisher needs:
| Item | Example | What it is |
|---|---|---|
| SRT URL | srt://stream.skynetgo.org:8890?streamid=publish:<uid>:publisher:<secret>&passphrase=<passphrase>&pbkeylen=16 |
The full URL to paste into a publisher. |
| Host / Port | stream.skynetgo.org / 8890 |
The streaming origin's SRT ingest. |
| Stream ID | publish:<uid>:publisher:<secret> |
Identifies the camera path and carries the secret. |
| Passphrase | (10–79 chars) | Encrypts the SRT link. The same passphrase is used for all publishers; the per-camera secret is what actually authorizes this camera. |
| Publish secret | (per-camera) | The credential the origin checks. Embedded in the Stream ID. |
The publish secret is shown only once and stored only as a hash — it cannot be retrieved again. Copy the SRT URL now. If you lose it (or it leaks), use Rotate publish secret on the camera to generate a new one; the old URL immediately stops working.
The SRT URL already encodes everything the publisher needs, so copying that single field is normally sufficient.
2. Publish with ffmpeg
A publisher pulls each camera's H.264 RTSP stream on your LAN and pushes it outbound to the SRT URL as MPEG-TS. ffmpeg does this in one line. The streaming origin accepts SRT only (not inbound RTSP/RTMP), so something on your side has to speak SRT to it — ffmpeg is that bridge.
Quick test — one camera
Paste your camera's RTSP URL and the SRT URL from the Publish setup panel:
ffmpeg -rtsp_transport tcp -i "rtsp://USER:PASS@192.168.1.50:81/cam1" `
-c:v copy -c:a libopus -b:a 64k -ac 1 -f mpegts `
"srt://stream.skynetgo.org:8890?streamid=publish:<uid>:publisher:<secret>&passphrase=<passphrase>&pbkeylen=16"
-c:v copy forwards the video untouched (no re-encode, no CPU/GPU cost).
-c:a libopus transcodes the audio to Opus — see Audio for why.
Then open the camera on the telescope/observatory page; you should see live
video within a second or two.
Many cameras — auto-restarting PowerShell script
For more than one camera, this PowerShell script runs one ffmpeg publisher per
camera and automatically restarts any that stop — a camera reboot, a network
blip, or a dropped RTSP connection no longer means restarting things by hand.
Each camera's ffmpeg output goes to logs\<Name>.log next to the script, so the
console stays readable. Fill in the $Cameras list and run it; press Ctrl+C to
stop every publisher.
<#
Start a resilient ffmpeg SRT publisher for each camera. A supervisor loop
watches every publisher and restarts any that exit, so streams recover on
their own. Run with:
powershell -ExecutionPolicy Bypass -File .\start-cameras.ps1
Press Ctrl+C to stop every publisher. Per-camera fields:
- Name : a short label used in console messages and the log file name.
- Source : the camera's H.264 RTSP URL (Blue Iris re-stream or the camera).
WebRTC needs H.264 — don't use an H.265 stream.
- Srt : the full SRT URL from the website's "Publish setup" panel.
Keep it in single quotes so '&' isn't treated specially.
#>
$FfmpegPath = 'ffmpeg' # or the full path, e.g. 'C:\ffmpeg\bin\ffmpeg.exe'
# Supervisor timing, and the RTSP read timeout (microseconds) that makes ffmpeg
# exit on a stalled feed. The matching ffmpeg option is detected below.
$PollSeconds = 3
$RestartDelaySeconds = 5
$ReadTimeoutMicros = 5000000
$Cameras = @(
@{
Name = 'cam1'
Source = 'rtsp://USER:PASS@192.168.1.50:81/cam1'
Srt = 'srt://stream.skynetgo.org:8890?streamid=publish:UID-1:publisher:SECRET-1&passphrase=PASSPHRASE&pbkeylen=16'
}
@{
Name = 'cam2'
Source = 'rtsp://USER:PASS@192.168.1.51:81/cam2'
Srt = 'srt://stream.skynetgo.org:8890?streamid=publish:UID-2:publisher:SECRET-2&passphrase=PASSPHRASE&pbkeylen=16'
}
)
$ffmpeg = (Get-Command $FfmpegPath -ErrorAction SilentlyContinue).Source
if (-not $ffmpeg) {
Write-Error "ffmpeg not found ('$FfmpegPath'). Install it or set `$FfmpegPath to ffmpeg.exe."
exit 1
}
# The RTSP socket-timeout option differs by ffmpeg version: -timeout (modern) or
# -stimeout (older). (-rw_timeout is a protocol option the RTSP demuxer rejects
# with "Option not found".) Detect which this build exposes, or skip it.
$TimeoutOption = $null
if ($ReadTimeoutMicros -gt 0) {
try {
$rtspHelp = (& $ffmpeg -hide_banner -h demuxer=rtsp 2>&1 | Out-String)
foreach ($opt in 'timeout', 'stimeout') {
if ($rtspHelp -match "(?m)^\s*-$opt\s") { $TimeoutOption = "-$opt"; break }
}
} catch { }
if (-not $TimeoutOption) {
Write-Warning 'No RTSP socket-timeout option found; running without stall detection (auto-restart still works).'
}
}
$logDir = if ($PSScriptRoot) { Join-Path $PSScriptRoot 'logs' } else { Join-Path (Get-Location) 'logs' }
if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir | Out-Null }
function Start-Publisher {
param($Cam)
$inputArgs = @()
if ($TimeoutOption) { $inputArgs += @($TimeoutOption, "$ReadTimeoutMicros") }
$inputArgs += @('-rtsp_transport', 'tcp', '-i', $Cam.Source)
# -c:v copy forwards video untouched; -c:a libopus transcodes audio to Opus
# (WebRTC drops AAC/mp2). See the Audio section for why.
$ffArgs = @('-hide_banner', '-loglevel', 'warning') + $inputArgs + @(
'-c:v', 'copy', '-c:a', 'libopus', '-b:a', '64k', '-ac', '1',
'-f', 'mpegts',
$Cam.Srt
)
$log = Join-Path $logDir ("{0}.log" -f $Cam.Name)
# -NoNewWindow avoids the SmartScreen block on a downloaded ffmpeg.exe;
# stderr is redirected to the per-camera log.
return Start-Process -FilePath $ffmpeg -ArgumentList $ffArgs `
-NoNewWindow -PassThru -RedirectStandardError $log
}
# Default a Name for any entry that omits one so log files don't collide.
$n = 0
foreach ($cam in $Cameras) { $n++; if (-not $cam.Name) { $cam.Name = "cam$n" } }
$publishers = @(foreach ($cam in $Cameras) {
[pscustomobject]@{ Name = $cam.Name; Cam = $cam; Process = (Start-Publisher -Cam $cam); Restarts = 0; NextStartAt = $null }
})
Write-Host ("Publishing {0} camera(s). Logs in {1}. Press Ctrl+C to stop." -f $publishers.Count, $logDir)
try {
while ($true) {
Start-Sleep -Seconds $PollSeconds
$now = Get-Date
foreach ($p in $publishers) {
if ($p.Process -and -not $p.Process.HasExited) { continue } # healthy
if ($null -eq $p.NextStartAt) {
$why = Get-Content (Join-Path $logDir ("{0}.log" -f $p.Name)) -Tail 1 -ErrorAction SilentlyContinue
$suffix = if ($why) { " - $($why.Trim())" } else { '' }
Write-Warning ("[{0}] {1} stopped{2}; restarting in {3}s" -f $now.ToString('HH:mm:ss'), $p.Name, $suffix, $RestartDelaySeconds)
$p.NextStartAt = $now.AddSeconds($RestartDelaySeconds)
}
elseif ($now -ge $p.NextStartAt) {
$p.Process = Start-Publisher -Cam $p.Cam; $p.Restarts++; $p.NextStartAt = $null
Write-Host ("[{0}] {1} restarted (restart #{2})" -f $now.ToString('HH:mm:ss'), $p.Name, $p.Restarts)
}
}
}
}
finally {
Write-Host 'Stopping all publishers...'
foreach ($p in $publishers) {
if ($p.Process -and -not $p.Process.HasExited) { Stop-Process -Id $p.Process.Id -Force -ErrorAction SilentlyContinue }
}
}
The socket-timeout flag makes ffmpeg exit if the feed stalls instead of
hanging on a half-open connection — that exit is what lets the supervisor detect
the failure and restart it. Its name differs by ffmpeg version (-timeout on
modern builds, -stimeout on older ones), so the script detects which your
build supports; set $ReadTimeoutMicros = 0 to disable it.
On Linux/macOS, wrap the publisher in a restart loop for the same behaviour:
while true; do
# -timeout is the modern RTSP socket-timeout option; older ffmpeg uses -stimeout.
ffmpeg -hide_banner -loglevel warning -timeout 5000000 \
-rtsp_transport tcp -i "rtsp://USER:PASS@192.168.1.50:81/cam1" \
-c:v copy -c:a libopus -b:a 64k -ac 1 -f mpegts \
"srt://stream.skynetgo.org:8890?streamid=publish:UID:publisher:SECRET&passphrase=PASSPHRASE&pbkeylen=16"
echo "stream exited; restarting in 5s" >&2
sleep 5
done
For survival across host reboots, run the publisher as a service or scheduled task (Task Scheduler on Windows, a systemd unit on Linux) so it starts on boot; the loop above keeps it alive within a session.
3. Audio
Audio requires the most attention, because the streaming origin does not transcode: whatever codec you publish is what viewers must be able to play, and the two playback paths accept different audio codecs:
- WebRTC (the primary in-browser player) carries Opus or G711
only. It silently drops AAC — and also drops mp2, which is what the
MPEG-TS muxer picks by default if you forget to set
-c:a. - HLS (the fallback) carries AAC fine.
Most IP cameras emit AAC audio. With AAC, video plays but the WebRTC viewer has no sound and reports no error. The options:
| You want… | Use | Result |
|---|---|---|
| Audio on the WebRTC player | -c:a libopus -b:a 64k -ac 1 |
Transcodes the camera's audio to Opus. Works everywhere. Recommended. |
| Audio, HLS-only viewing | -c:a copy |
Passes AAC through. Plays on HLS, not WebRTC. |
| No audio at all | -an |
Strips audio entirely. |
Audio transcoding is inexpensive relative to video, so -c:a libopus is a safe
default even though the video stays -c:v copy.
Check what your source actually sends first:
ffprobe -rtsp_transport tcp "rtsp://USER:PASS@192.168.1.50:81/cam1"
If the dump shows no Audio: stream, the camera/feed isn't sending audio at
all — enable the camera's microphone (and, in Blue Iris, that camera's audio)
before any ffmpeg flag can help. If it shows Audio: aac, transcode to Opus
as above.
The in-browser player starts muted (browsers require this for autoplay). Click unmute in the video controls to hear audio, even when it's flowing correctly.
4. Video codec
WebRTC playback requires H.264 video. Don't publish an H.265/HEVC
stream — browsers can't decode H.265 over WebRTC, and the origin won't
transcode it. Most cameras have an H.264 sub-stream or main-stream profile;
use that. Since the publisher uses -c:v copy, the codec it pulls is the codec
viewers get.
5. Blue Iris (recommended for multiple cameras)
Blue Iris is a Windows NVR that can ingest many cameras (RTSP, ONVIF, vendor protocols) and re-stream each one as a clean, uniform H.264 RTSP feed. If you have more than a couple of cameras, running Blue Iris as the aggregator is the easiest setup:
- One place to manage every camera, with recording, motion alerts, and a consistent RTSP URL per camera regardless of the camera's own quirks.
- You can normalize resolution/codec there, so the ffmpeg publisher just does
-c:v copyagainst a predictable H.264 stream. - Point the ffmpeg publisher (§2) at Blue Iris's RTSP re-stream URL instead of the camera directly — everything else is identical.
In this arrangement Blue Iris is the RTSP source aggregator and ffmpeg is the SRT bridge to Skynet. Make sure the Blue Iris re-stream you pull is the H.264 profile, and that audio is enabled on that camera in Blue Iris if you want sound (its default re-stream profile may be video-only).
6. Native camera streams (Blue Iris is optional)
Blue Iris is not required. If a camera exposes its own RTSP stream — almost all do — you can point the ffmpeg publisher (§2) straight at it:
rtsp://USER:PASS@<camera-ip>:<port>/<path>
Pulling natively is fully supported and removes a moving part. The requirements are the same as above, verified per camera instead of once at the aggregator:
- The stream the publisher pulls must be H.264 video (use the camera's H.264 profile/sub-stream, not H.265).
- Audio, if you want it on WebRTC, gets transcoded to Opus by the publisher
(
-c:a libopus) — the camera can send AAC; ffmpeg converts it. - The camera must be reachable from the publisher on the LAN.
Use Blue Iris when you want central management/recording across many cameras; pull natively when you have a few cameras and just want them live.
Reference
IPCamera Schema
Properties
| Name | Type | Description |
|---|---|---|
| id | Integer |
No description |
| uid | UUID |
No description |
| telescope_id | Integer (Optional) |
No description |
| observatory_id | Integer (Optional) |
No description |
| name | String(120) |
No description |
| role | Enum(pier, all_sky, finder, room, other) |
No description |
| vendor | String(80) (Optional) |
No description |
| onvif_host | String(255) (Optional) |
No description |
| ptz | Boolean |
No description |
| pose | JSON (Optional) |
No description |
| is_primary | Boolean |
No description |
| order | Integer |
No description |
| secret_ref | String(255) (Optional) |
No description |
| created_at | DateTime |
No description |
| updated_at | DateTime |
No description |
| last_seen | DateTime (Optional) |
No description |
Relationships
| Relationship Name | Type |
|---|---|
| streams | IPCameraStream |
| telescope | Telescope |
| observatory | Observatory |
IPCameraStream Schema
Properties
| Name | Type | Description |
|---|---|---|
| id | Integer |
No description |
| camera_id | Integer |
No description |
| kind | Enum(rtsp, rtmp, mjpeg, hls, webrtc, snapshot) |
No description |
| quality | Enum(main, sub, thumbnail) (Optional) |
No description |
| url_template | String(1024) |
No description |
| is_public | Boolean |
No description |
| expires_secs | Integer (Optional) |
No description |
| active | Boolean |
No description |
Relationships
| Relationship Name | Type |
|---|---|
| camera | IPCamera |