Add Raven Eve network ingest setup
This commit is contained in:
parent
140cac4f2e
commit
19ed000cd8
12 changed files with 609 additions and 6 deletions
98
config/mimir-runtime.raven-eve.example.json
Normal file
98
config/mimir-runtime.raven-eve.example.json
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
{
|
||||
"bufferSeconds": 5.0,
|
||||
"audioSync": {
|
||||
"mode": "chirp-only",
|
||||
"referenceSourceId": "asio-ch2",
|
||||
"calibrationGain": 2.0,
|
||||
"watermarkGain": 0.035,
|
||||
"calibrationModelPath": "../calibration/chirp-bin/scarlett-artifact.json",
|
||||
"complexContourChannelModelPath": "../calibration/bioacoustic/complex-contour-channel-model.json",
|
||||
"bioacousticWitnessProfileId": "canary-packet-trill",
|
||||
"enableComplexContourRuntime": true
|
||||
},
|
||||
"streams": [
|
||||
{
|
||||
"sourceId": "focusrite-asio",
|
||||
"kind": "Audio",
|
||||
"origin": "LocalDevice",
|
||||
"enabled": true,
|
||||
"adapter": "asio",
|
||||
"command": "../native/asio_capture/build/Release/mimir_asio_capture.dll",
|
||||
"sampleRate": 192000,
|
||||
"acceptSourceIds": [
|
||||
"asio-ch0",
|
||||
"asio-ch1",
|
||||
"asio-ch2",
|
||||
"asio-ch3"
|
||||
],
|
||||
"sourceLabels": {
|
||||
"asio-ch0": "Focusrite Input 1",
|
||||
"asio-ch1": "Focusrite Input 2",
|
||||
"asio-ch2": "Focusrite Loopback 1",
|
||||
"asio-ch3": "Focusrite Loopback 2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"sourceId": "raven-display",
|
||||
"displayName": "Raven screen capture",
|
||||
"clockDomainId": "raven-sync",
|
||||
"kind": "Video",
|
||||
"origin": "Network",
|
||||
"enabled": true,
|
||||
"adapter": "ffmpeg-rawvideo",
|
||||
"command": "ffmpeg",
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"pixelFormat": "Bgra8",
|
||||
"arguments": [
|
||||
"-hide_banner",
|
||||
"-nostdin",
|
||||
"-loglevel",
|
||||
"warning",
|
||||
"-fflags",
|
||||
"nobuffer",
|
||||
"-flags",
|
||||
"low_delay",
|
||||
"-i",
|
||||
"srt://0.0.0.0:5200?mode=listener&latency=120000&timeout=5000000",
|
||||
"-an",
|
||||
"-f",
|
||||
"rawvideo",
|
||||
"-pix_fmt",
|
||||
"bgra",
|
||||
"-"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sourceId": "eve-camera",
|
||||
"displayName": "Eve camera capture",
|
||||
"clockDomainId": "eve-camera",
|
||||
"kind": "Video",
|
||||
"origin": "Network",
|
||||
"enabled": true,
|
||||
"adapter": "ffmpeg-rawvideo",
|
||||
"command": "ffmpeg",
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"pixelFormat": "Bgra8",
|
||||
"arguments": [
|
||||
"-hide_banner",
|
||||
"-nostdin",
|
||||
"-loglevel",
|
||||
"warning",
|
||||
"-fflags",
|
||||
"nobuffer",
|
||||
"-flags",
|
||||
"low_delay",
|
||||
"-i",
|
||||
"srt://0.0.0.0:5201?mode=listener&latency=120000&timeout=5000000",
|
||||
"-an",
|
||||
"-f",
|
||||
"rawvideo",
|
||||
"-pix_fmt",
|
||||
"bgra",
|
||||
"-"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -216,6 +216,12 @@ a named invariant that the native runtime cannot protect yet.
|
|||
The display feed remains derived evidence, not an independent timing owner.
|
||||
`Mimir.BufferSmoke --synchronized-buffer-planner-smoke` proves the aligned
|
||||
local-camera, Raven-display, loopback, and mic buffer shape.
|
||||
- `MimirFfmpegRawVideoStreamSource` is the current network-pixel adapter for
|
||||
Raven/Eve ingest. FFmpeg owns SRT receive/decode, Mimir owns exact raw-video
|
||||
frame reads and synchronized buffer samples. `config/mimir-runtime.raven-eve.example.json`
|
||||
listens for Raven screen capture on port `5200` and Eve camera capture on
|
||||
port `5201`. `Mimir.BufferSmoke --ffmpeg-rawvideo-source-smoke` proves the
|
||||
local raw-video adapter without Raven or Eve online.
|
||||
- `MimirPresentationControlState` owns operator intent for the Fensalir program
|
||||
surface: video feed visibility/solo/opacity/layer order, audio mute/solo/gain,
|
||||
and global LUT preset selection. The `Mimir Program` UI panel is deliberately
|
||||
|
|
|
|||
|
|
@ -31,15 +31,23 @@ Practical shape:
|
|||
applies that clock-domain correction to the `raven-display` video buffer.
|
||||
5. Fensalir receives aligned buffer slices and still owns composition.
|
||||
|
||||
The disabled example in `config/mimir-runtime.asio.example.json` tags the
|
||||
network display stream with `clockDomainId: raven-sync`. The display receiver is
|
||||
expected to emit frame-event metadata with Raven source timestamps and frame
|
||||
identity. The Scarlett channel carrying Raven's sync audio is the evidence path
|
||||
that earns the `raven-sync` timing correction.
|
||||
`config/mimir-runtime.raven-eve.example.json` is the active setup shape for the
|
||||
network producer pass:
|
||||
|
||||
- `raven-display` listens for Raven screen capture on SRT port `5200`, decodes
|
||||
through FFmpeg, and emits BGRA video samples into the runtime buffer path.
|
||||
- `eve-camera` listens for Eve camera capture on SRT port `5201`, using the same
|
||||
raw-video adapter.
|
||||
|
||||
The older disabled `frame-events` example in
|
||||
`config/mimir-runtime.asio.example.json` remains diagnostic only. It can witness
|
||||
source timestamps and frame identity, but it does not carry pixels. The Scarlett
|
||||
channel carrying Raven's sync audio is still the evidence path that earns the
|
||||
`raven-sync` timing correction.
|
||||
|
||||
Smoke proof:
|
||||
|
||||
```powershell
|
||||
dotnet run --project .\src\Mimir.BufferSmoke\Mimir.BufferSmoke.csproj -- --synchronized-buffer-planner-smoke
|
||||
dotnet run --project .\src\Mimir.BufferSmoke\Mimir.BufferSmoke.csproj -- --ffmpeg-rawvideo-source-smoke
|
||||
```
|
||||
|
||||
|
|
|
|||
101
docs/raven-eve-ingest.md
Normal file
101
docs/raven-eve-ingest.md
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# Raven And Eve Ingest
|
||||
|
||||
## Objective
|
||||
|
||||
Ingest Raven screen capture and Eve camera capture into Mimir as ordinary
|
||||
network video sources, then let the existing synchronization hub place those
|
||||
samples in rolling buffers beside Scarlett audio and local camera sources.
|
||||
|
||||
## Authority Map
|
||||
|
||||
- Owner: `Mimir.Runtime` owns decoded video samples and rolling-buffer
|
||||
placement.
|
||||
- Inputs: Raven sends H.264/MPEG-TS over SRT to Starfire port `5200`; Eve sends
|
||||
camera video over SRT to Starfire port `5201`; Scarlett/ASIO carries audio and
|
||||
sync evidence.
|
||||
- Outputs: `raven-display` and `eve-camera` emit `MimirStreamSample` frames with
|
||||
BGRA payloads and `MimirVideoFrameDescriptor` metadata.
|
||||
- Derived state: network arrival timestamps are capture metadata only. Raven
|
||||
audio-in-Scarlett remains the timing witness for global alignment.
|
||||
- Forbidden writers: OBS, FFmpeg sender clocks, and SRT arrival time do not own
|
||||
final sync truth.
|
||||
- Cut line: no custom transport daemon and no second compositor path until this
|
||||
FFmpeg/SRT edge fails a concrete invariant.
|
||||
|
||||
## Starfire Receiver
|
||||
|
||||
Use the Raven/Eve runtime config:
|
||||
|
||||
```powershell
|
||||
$env:MIMIR_RUNTIME_CONFIG = "E:\Projects\Mimir\config\mimir-runtime.raven-eve.example.json"
|
||||
dotnet run --project .\src\Mimir.BufferSmoke\Mimir.BufferSmoke.csproj -- --seconds 5
|
||||
```
|
||||
|
||||
When both senders are active, require sample arrival:
|
||||
|
||||
```powershell
|
||||
$env:MIMIR_RUNTIME_CONFIG = "E:\Projects\Mimir\config\mimir-runtime.raven-eve.example.json"
|
||||
dotnet run --project .\src\Mimir.BufferSmoke\Mimir.BufferSmoke.csproj -- --seconds 5 --require-samples
|
||||
```
|
||||
|
||||
The config starts two FFmpeg listener processes:
|
||||
|
||||
- `raven-display`: `srt://0.0.0.0:5200?mode=listener&latency=120000&timeout=5000000`
|
||||
- `eve-camera`: `srt://0.0.0.0:5201?mode=listener&latency=120000&timeout=5000000`
|
||||
|
||||
Each listener decodes to raw BGRA frames on stdout. Mimir reads exact frame
|
||||
boundaries and stores those frames in the normal synchronization buffers.
|
||||
|
||||
## Raven Sender
|
||||
|
||||
Run this on Raven from the logged-in desktop session:
|
||||
|
||||
```powershell
|
||||
E:\Projects\Mimir\scripts\start-raven-screen-capture-sender.ps1 -TargetHost 192.168.1.66 -Port 5200
|
||||
```
|
||||
|
||||
The script uses `gdigrab` plus `h264_nvenc`, then calls Starfire's SRT listener.
|
||||
Do not launch the capture itself through a blind SSH session unless the desktop
|
||||
capture path has been proven there; `gdigrab` captures the interactive Windows
|
||||
desktop, and service/session boundaries can turn screen capture into a black
|
||||
box with excellent logs.
|
||||
|
||||
Raven sync signal: route Raven audio or a Raven-generated chirp into the
|
||||
Scarlett alongside the microphone. Mimir should treat that audio evidence as
|
||||
the authority for aligning Raven video with the rest of the program.
|
||||
|
||||
## Eve Sender Status
|
||||
|
||||
Eve is configured as a receiver lane now, and SSH is reachable, but the actual
|
||||
camera producer is not implemented in this repo yet. The existing Eve app is a
|
||||
view/control surface for receiving Starfire video and sending touch events; it
|
||||
is not currently an AVFoundation camera broadcaster. No `ffmpeg` or obvious
|
||||
camera-capture helper is installed on the iPad path checked during setup.
|
||||
|
||||
Current next options:
|
||||
|
||||
- Add AVFoundation capture to Eve and send H.264 over SRT or MPEG-TS/TCP to
|
||||
Starfire. This keeps Eve as the source owner and matches the network-producer
|
||||
model, but needs the iPad online and a native sender cut.
|
||||
- Use a known iOS camera streaming app that can publish RTSP, SRT, NDI, or
|
||||
MPEG-TS, then point the `eve-camera` FFmpeg listener/receiver at that stream.
|
||||
This is fastest for a room test, but delegates camera authority to an app
|
||||
outside the Mimir/Eve codebase.
|
||||
- Capture Eve over USB/Continuity on Starfire and expose it as a local camera
|
||||
source. This is operationally simple if Windows sees the device, but Eve stops
|
||||
being a networked producer and the clock-domain model changes.
|
||||
|
||||
The coherent default is the AVFoundation sender, because it gives Mimir one
|
||||
clean producer per physical source and leaves sync ownership explicit.
|
||||
|
||||
## Smoke Test
|
||||
|
||||
The raw-video source has a local synthetic smoke that does not require Raven or
|
||||
Eve:
|
||||
|
||||
```powershell
|
||||
dotnet run --project .\src\Mimir.BufferSmoke\Mimir.BufferSmoke.csproj -- --ffmpeg-rawvideo-source-smoke
|
||||
```
|
||||
|
||||
That smoke starts FFmpeg with `testsrc2`, decodes BGRA frames through stdout,
|
||||
and verifies the sample metadata and payload size that Mimir receives.
|
||||
|
|
@ -82,6 +82,12 @@ probes can emit per-frame JSON metadata so Fensalir sees real sensor cadence in
|
|||
the rolling buffers while the direct ABI driver is being cut. It does not carry
|
||||
pixels and does not own the final six-camera hot path. Multi-camera probes are
|
||||
one process with declared accepted source ids, not one process per camera.
|
||||
The `ffmpeg-rawvideo` adapter is the live network-pixel edge for Raven/Eve
|
||||
setup: FFmpeg owns SRT receive/decode and writes exact raw frames to stdout;
|
||||
`MimirFfmpegRawVideoStreamSource` owns frame-boundary reads, BGRA/NV12 payload
|
||||
geometry, sample descriptors, and insertion into the normal synchronization
|
||||
path. `config/mimir-runtime.raven-eve.example.json` declares `raven-display`
|
||||
on SRT port `5200` and `eve-camera` on SRT port `5201`.
|
||||
|
||||
## OBS Bridge Utility
|
||||
|
||||
|
|
@ -387,6 +393,11 @@ shape is explicit: Raven controls both display pixels and an audio timing signal
|
|||
routed into Scarlett, so Scarlett-decoded Raven audio evidence can earn a
|
||||
`raven-sync` correction that is applied to the `raven-display` buffer. Network
|
||||
arrival timestamps remain metadata, not timing authority.
|
||||
The first operational ingest config is `config/mimir-runtime.raven-eve.example.json`:
|
||||
Raven screen capture and Eve camera capture arrive over SRT, decode to raw BGRA
|
||||
through FFmpeg, and enter the same rolling-buffer model as local sources. Eve's
|
||||
receiver lane exists; the iPad-side camera producer still needs either a native
|
||||
AVFoundation sender or an explicitly chosen external camera-streaming app.
|
||||
`MimirPresentationControlState` now owns the Fensalir program-control intent:
|
||||
video feed visibility/solo/opacity/layer order, audio mute/solo/gain, and
|
||||
global LUT preset selection. The `Mimir Program` panel exposes those controls
|
||||
|
|
|
|||
|
|
@ -116,6 +116,13 @@ Get-Content .\state\evidence.jsonl -Tail 8
|
|||
static viewer belongs to the root GameCult site at
|
||||
`https://gamecult.org/livestream` and reads
|
||||
`https://streampixels.gamecult.org/mimir/live/hls/mimir.m3u8`.
|
||||
- Raven/Eve ingest now has a network-pixel receiver config:
|
||||
`config/mimir-runtime.raven-eve.example.json` starts FFmpeg-backed raw-video
|
||||
listeners for `raven-display` on SRT port `5200` and `eve-camera` on port
|
||||
`5201`. `scripts/start-raven-screen-capture-sender.ps1` launches Raven desktop
|
||||
capture with `gdigrab` plus NVENC. Eve's Mimir receiver lane exists, but the
|
||||
iPad-side camera producer is still an open cut: add native AVFoundation send
|
||||
support or deliberately choose an external iOS streaming app.
|
||||
- Faust/native DSP owns hot audio alignment, separation, spatialization, and
|
||||
synchronized stems.
|
||||
- OBS receives final program surfaces; it does not own synchronization.
|
||||
|
|
|
|||
66
scripts/start-raven-screen-capture-sender.ps1
Normal file
66
scripts/start-raven-screen-capture-sender.ps1
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
param(
|
||||
[string]$TargetHost = "192.168.1.66",
|
||||
[int]$Port = 5200,
|
||||
[int]$Width = 1920,
|
||||
[int]$Height = 1080,
|
||||
[int]$Framerate = 30,
|
||||
[string]$FfmpegPath = "ffmpeg",
|
||||
[string]$Source = "desktop",
|
||||
[string]$VideoBitrate = "12000k",
|
||||
[string]$LogRoot = "C:\Meta\Mimir\logs",
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $LogRoot | Out-Null
|
||||
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$stdoutLog = Join-Path $LogRoot "raven-screen-srt-$timestamp.out.log"
|
||||
$stderrLog = Join-Path $LogRoot "raven-screen-srt-$timestamp.err.log"
|
||||
$endpoint = "srt://${TargetHost}:${Port}?mode=caller&latency=120000"
|
||||
$videoSize = "${Width}x${Height}"
|
||||
$gop = [Math]::Max(1, $Framerate * 2)
|
||||
|
||||
$arguments = @(
|
||||
"-hide_banner",
|
||||
"-nostdin",
|
||||
"-loglevel", "warning",
|
||||
"-f", "gdigrab",
|
||||
"-framerate", $Framerate.ToString(),
|
||||
"-video_size", $videoSize,
|
||||
"-i", $Source,
|
||||
"-an",
|
||||
"-c:v", "h264_nvenc",
|
||||
"-preset", "p4",
|
||||
"-tune", "ll",
|
||||
"-b:v", $VideoBitrate,
|
||||
"-maxrate", $VideoBitrate,
|
||||
"-bufsize", "24000k",
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-g", $gop.ToString(),
|
||||
"-f", "mpegts",
|
||||
$endpoint
|
||||
)
|
||||
|
||||
Write-Host "Raven screen sender:"
|
||||
Write-Host " ffmpeg: $FfmpegPath"
|
||||
Write-Host " target: $endpoint"
|
||||
Write-Host " source: $Source $videoSize@$Framerate"
|
||||
Write-Host " stdout: $stdoutLog"
|
||||
Write-Host " stderr: $stderrLog"
|
||||
|
||||
if ($DryRun)
|
||||
{
|
||||
Write-Host ($FfmpegPath + " " + ($arguments -join " "))
|
||||
exit 0
|
||||
}
|
||||
|
||||
$process = Start-Process `
|
||||
-FilePath $FfmpegPath `
|
||||
-ArgumentList $arguments `
|
||||
-WindowStyle Hidden `
|
||||
-RedirectStandardOutput $stdoutLog `
|
||||
-RedirectStandardError $stderrLog `
|
||||
-PassThru
|
||||
|
||||
Write-Host "Started Raven screen sender pid=$($process.Id)"
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
|
|
@ -169,6 +170,11 @@ if (args.Any(arg => string.Equals(arg, "--mf-gpu-driver-smoke", StringComparison
|
|||
return RunMfGpuDriverSmoke(args);
|
||||
}
|
||||
|
||||
if (args.Any(arg => string.Equals(arg, "--ffmpeg-rawvideo-source-smoke", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return RunFfmpegRawVideoSourceSmoke(args);
|
||||
}
|
||||
|
||||
if (args.Any(arg => string.Equals(arg, "--mimir-spectrum-upload-smoke", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return RunMimirSpectrumUploadSmoke();
|
||||
|
|
@ -1039,6 +1045,69 @@ static int RunMfGpuDriverSmoke(string[] args)
|
|||
return 1;
|
||||
}
|
||||
|
||||
static int RunFfmpegRawVideoSourceSmoke(string[] args)
|
||||
{
|
||||
var ffmpeg = ParseStringOption(args, "--ffmpeg", "ffmpeg");
|
||||
var sourceId = ParseStringOption(args, "--source-id", "ffmpeg-rawvideo-smoke");
|
||||
var width = ParseIntOption(args, "--width", 160);
|
||||
var height = ParseIntOption(args, "--height", 90);
|
||||
var fps = ParseIntOption(args, "--fps", 10);
|
||||
var frames = ParseIntOption(args, "--frames", 3);
|
||||
var timeoutMs = ParseIntOption(args, "--timeout-ms", 3000);
|
||||
|
||||
using var source = new MimirFfmpegRawVideoStreamSource(
|
||||
new MimirStreamDescriptor(sourceId, MimirStreamKind.Video, MimirStreamOrigin.Network),
|
||||
new MimirFfmpegRawVideoStreamSourceOptions(
|
||||
ffmpeg,
|
||||
[
|
||||
"-hide_banner",
|
||||
"-nostdin",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
$"testsrc2=size={width}x{height}:rate={fps}",
|
||||
"-frames:v",
|
||||
frames.ToString(CultureInfo.InvariantCulture),
|
||||
"-f",
|
||||
"rawvideo",
|
||||
"-pix_fmt",
|
||||
"bgra",
|
||||
"-"
|
||||
],
|
||||
width,
|
||||
height,
|
||||
MimirVideoPixelFormat.Bgra8));
|
||||
|
||||
var deadline = Stopwatch.GetTimestamp() + (long)(timeoutMs / 1000.0 * Stopwatch.Frequency);
|
||||
while (Stopwatch.GetTimestamp() < deadline)
|
||||
{
|
||||
if (!source.TryRead(out var sample))
|
||||
{
|
||||
Thread.Sleep(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
var frame = sample.VideoFrame;
|
||||
var expectedBytes = width * height * 4;
|
||||
Console.WriteLine(
|
||||
$"ffmpeg-rawvideo-source-smoke source={sample.SourceId} {frame?.Width}x{frame?.Height} format={frame?.PixelFormat} liveBytes={sample.ByteLength} payloadBytes={sample.Data.Length} stride={frame?.StrideBytes}");
|
||||
return frame != null &&
|
||||
frame.Width == width &&
|
||||
frame.Height == height &&
|
||||
frame.PixelFormat == MimirVideoPixelFormat.Bgra8 &&
|
||||
frame.StrideBytes == width * 4 &&
|
||||
sample.ByteLength == expectedBytes &&
|
||||
sample.Data.Length == expectedBytes
|
||||
? 0
|
||||
: 1;
|
||||
}
|
||||
|
||||
Console.WriteLine($"ffmpeg-rawvideo-source-smoke timeout source={sourceId} {width}x{height}@{fps}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
static long StopwatchTicksToNs(long ticks)
|
||||
{
|
||||
return checked((long)(ticks * (1_000_000_000.0 / Stopwatch.Frequency)));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,183 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Mimir.Runtime.Synchronization;
|
||||
|
||||
public sealed record MimirFfmpegRawVideoStreamSourceOptions(
|
||||
string Command,
|
||||
IReadOnlyList<string> Arguments,
|
||||
int Width,
|
||||
int Height,
|
||||
MimirVideoPixelFormat PixelFormat,
|
||||
int FrameBytes = 0,
|
||||
int StrideBytes = 0);
|
||||
|
||||
public sealed class MimirFfmpegRawVideoStreamSource : IMimirStreamSource
|
||||
{
|
||||
private readonly ConcurrentQueue<MimirStreamSample> samples = new();
|
||||
private readonly CancellationTokenSource cancellation = new();
|
||||
private readonly Process process;
|
||||
private readonly Task stdoutTask;
|
||||
private readonly Task stderrTask;
|
||||
private readonly int frameBytes;
|
||||
private readonly int strideBytes;
|
||||
private ulong sequence;
|
||||
private bool disposed;
|
||||
|
||||
public MimirFfmpegRawVideoStreamSource(
|
||||
MimirStreamDescriptor descriptor,
|
||||
MimirFfmpegRawVideoStreamSourceOptions options)
|
||||
{
|
||||
if (descriptor.Kind != MimirStreamKind.Video)
|
||||
{
|
||||
throw new ArgumentException("Raw FFmpeg video sources require a video descriptor.", nameof(descriptor));
|
||||
}
|
||||
|
||||
if (options.Width <= 0 || options.Height <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(options), "Raw FFmpeg video sources require positive width and height.");
|
||||
}
|
||||
|
||||
Descriptor = descriptor;
|
||||
Options = options;
|
||||
strideBytes = options.StrideBytes > 0 ? options.StrideBytes : InferStride(options.Width, options.PixelFormat);
|
||||
frameBytes = options.FrameBytes > 0 ? options.FrameBytes : InferFrameBytes(options.Width, options.Height, options.PixelFormat, strideBytes);
|
||||
if (strideBytes <= 0 || frameBytes <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(options), "Could not infer raw video frame size.");
|
||||
}
|
||||
|
||||
process = StartProcess(options);
|
||||
stdoutTask = Task.Run(() => ReadStdout(cancellation.Token));
|
||||
stderrTask = Task.Run(() => DrainStderr(cancellation.Token));
|
||||
}
|
||||
|
||||
public MimirStreamDescriptor Descriptor { get; }
|
||||
|
||||
public MimirFfmpegRawVideoStreamSourceOptions Options { get; }
|
||||
|
||||
public bool TryRead(out MimirStreamSample sample)
|
||||
{
|
||||
return samples.TryDequeue(out sample);
|
||||
}
|
||||
|
||||
private static Process StartProcess(MimirFfmpegRawVideoStreamSourceOptions options)
|
||||
{
|
||||
var startInfo = new ProcessStartInfo(options.Command)
|
||||
{
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
|
||||
foreach (var argument in options.Arguments)
|
||||
{
|
||||
startInfo.ArgumentList.Add(argument);
|
||||
}
|
||||
|
||||
return Process.Start(startInfo)
|
||||
?? throw new InvalidOperationException($"Could not start FFmpeg raw video source process: {options.Command}");
|
||||
}
|
||||
|
||||
private async Task ReadStdout(CancellationToken token)
|
||||
{
|
||||
var stream = process.StandardOutput.BaseStream;
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
var buffer = new byte[frameBytes];
|
||||
var offset = 0;
|
||||
while (offset < buffer.Length)
|
||||
{
|
||||
var read = await stream.ReadAsync(buffer.AsMemory(offset, buffer.Length - offset), token).ConfigureAwait(false);
|
||||
if (read <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
offset += read;
|
||||
}
|
||||
|
||||
var timestampNs = StopwatchTicksToNs(Stopwatch.GetTimestamp());
|
||||
var videoFrame = new MimirVideoFrameDescriptor(
|
||||
Options.Width,
|
||||
Options.Height,
|
||||
Options.PixelFormat,
|
||||
strideBytes,
|
||||
timestampNs);
|
||||
samples.Enqueue(new MimirStreamSample(
|
||||
Descriptor.SourceId,
|
||||
Descriptor.Kind,
|
||||
Descriptor.Origin,
|
||||
timestampNs,
|
||||
timestampNs,
|
||||
sequence++,
|
||||
0,
|
||||
buffer.Length,
|
||||
buffer,
|
||||
videoFrame));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DrainStderr(CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
var line = await process.StandardError.ReadLineAsync(token).ConfigureAwait(false);
|
||||
if (line == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int InferStride(int width, MimirVideoPixelFormat pixelFormat)
|
||||
{
|
||||
return pixelFormat switch
|
||||
{
|
||||
MimirVideoPixelFormat.Bgra8 => checked(width * 4),
|
||||
MimirVideoPixelFormat.Nv12 => width,
|
||||
MimirVideoPixelFormat.R8 or MimirVideoPixelFormat.Gray8 or MimirVideoPixelFormat.Bayer8 => width,
|
||||
MimirVideoPixelFormat.Rg8 or MimirVideoPixelFormat.Yuy2 or MimirVideoPixelFormat.LeapStereoIr => checked(width * 2),
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
|
||||
private static int InferFrameBytes(int width, int height, MimirVideoPixelFormat pixelFormat, int strideBytes)
|
||||
{
|
||||
return pixelFormat switch
|
||||
{
|
||||
MimirVideoPixelFormat.Nv12 => checked(width * height * 3 / 2),
|
||||
_ => checked(strideBytes * height),
|
||||
};
|
||||
}
|
||||
|
||||
private static long StopwatchTicksToNs(long ticks)
|
||||
{
|
||||
return checked((long)(ticks * (1_000_000_000.0 / Stopwatch.Frequency)));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
disposed = true;
|
||||
cancellation.Cancel();
|
||||
try
|
||||
{
|
||||
if (!process.HasExited)
|
||||
{
|
||||
process.Kill(entireProcessTree: true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
process.Dispose();
|
||||
cancellation.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -234,6 +234,26 @@ public sealed class MimirRuntimeConfiguration
|
|||
: null)));
|
||||
}
|
||||
|
||||
if (string.Equals(stream.Adapter, "ffmpeg-rawvideo", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(stream.Adapter, "rawvideo-process", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(stream.Command))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new MimirStreamSourceFactory(descriptor, () => new MimirFfmpegRawVideoStreamSource(
|
||||
descriptor,
|
||||
new MimirFfmpegRawVideoStreamSourceOptions(
|
||||
ResolveCommand(stream.Command, configDirectory),
|
||||
stream.Arguments,
|
||||
stream.Width,
|
||||
stream.Height,
|
||||
ParsePixelFormat(stream.PixelFormat),
|
||||
stream.FrameBytes,
|
||||
stream.StrideBytes)));
|
||||
}
|
||||
|
||||
if (!string.Equals(stream.Adapter, "process", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
|
|
@ -282,6 +302,26 @@ public sealed class MimirRuntimeConfiguration
|
|||
? origin
|
||||
: throw new InvalidOperationException($"Unknown Mimir stream origin: {value}");
|
||||
}
|
||||
|
||||
private static MimirVideoPixelFormat ParsePixelFormat(string value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value)
|
||||
? MimirVideoPixelFormat.Unknown
|
||||
: value.Trim().ToUpperInvariant() switch
|
||||
{
|
||||
"BAYER" or "BAYER8" => MimirVideoPixelFormat.Bayer8,
|
||||
"GRAY8" or "Y8" => MimirVideoPixelFormat.Gray8,
|
||||
"R8" => MimirVideoPixelFormat.R8,
|
||||
"RG8" => MimirVideoPixelFormat.Rg8,
|
||||
"YUY2" => MimirVideoPixelFormat.Yuy2,
|
||||
"MJPG" or "MJPEG" => MimirVideoPixelFormat.Mjpg,
|
||||
"H264" => MimirVideoPixelFormat.H264,
|
||||
"NV12" => MimirVideoPixelFormat.Nv12,
|
||||
"BGRA8" or "BGRA" => MimirVideoPixelFormat.Bgra8,
|
||||
"LEAP_STEREO_IR" or "LEAPSTEREOIR" => MimirVideoPixelFormat.LeapStereoIr,
|
||||
_ => MimirVideoPixelFormat.Unknown,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record MimirStreamSourceFactory(
|
||||
|
|
@ -394,6 +434,10 @@ public sealed class MimirStreamConfig
|
|||
|
||||
public string PixelFormat { get; set; } = "";
|
||||
|
||||
public int FrameBytes { get; set; }
|
||||
|
||||
public int StrideBytes { get; set; }
|
||||
|
||||
public string InputFormat { get; set; } = "";
|
||||
|
||||
public string OutputFormat { get; set; } = "Nv12";
|
||||
|
|
|
|||
|
|
@ -240,3 +240,5 @@
|
|||
{"ts": "2026-05-30T22:01:42+01:00", "type": "implementation", "status": "ok", "note": "Self-hosted Mimir live edge has a deployed origin proof. Mimir.Broadcast builds the Starfire FFmpeg/NVENC RTMP push to rtmp://127.0.0.1:11935/live/mimir through an SSH local forward; Yggdrasil nginx now has libnginx-mod-rtmp, localhost RTMP ingest, /srv/mimir-live/hls, and a live.mimir.gamecult.org HTTP vhost with CORS for mimir.gamecult.org. An eight-second synthetic NVENC push on 2026-05-30 generated mimir.m3u8 and four TS segments on Yggdrasil. Public DNS/TLS for live.mimir.gamecult.org remains the activation blocker."}
|
||||
{"ts": "2026-05-30T22:04:09+01:00", "type": "verification", "status": "ok", "note": "Yggdrasil target-host fanout smoke ran during an active Starfire synthetic NVENC push through the SSH RTMP tunnel. With LIVE_URL pinned to localhost via curl --resolve, the benchmark fetched a live HLS segment from nginx 320 times with CLIENTS=16 and REQUESTS_PER_CLIENT=20 in 1256 ms. wrk is not installed yet, so p95 latency remains future instrumentation."}
|
||||
{"ts": "2026-05-30T22:18:07+01:00", "type": "decision", "status": "ok", "note": "Viewer authority moved to root gamecult-site at https://gamecult.org/livestream. Mimir no longer owns a static live viewer page; Mimir.Broadcast remains the Starfire sender, and Yggdrasil serves HLS through the existing StreamPixels TLS route https://streampixels.gamecult.org/mimir/live/hls/mimir.m3u8. The deployed StreamPixels nginx route health check returned mimir-live-ok, and an active synthetic NVENC push generated playlist/segments served publicly through StreamPixels; curl fallback benchmark fetched 320 segment requests in 6756 ms over HTTPS from Yggdrasil."}
|
||||
{"ts": "2026-05-30T22:45:01+01:00", "type": "decision", "status": "ok", "note": "Added Raven/Eve ingest setup: ffmpeg-rawvideo runtime source decodes SRT feeds into Mimir video samples; raven-display listens on 5200, eve-camera on 5201; Raven desktop sender script uses gdigrab plus NVENC. Eve receiver lane exists but the iPad camera producer remains an explicit AVFoundation/external-app decision."}
|
||||
{"ts": "2026-05-30T22:46:17+01:00", "type": "verification", "status": "warn", "note": "Raven/Eve ingest host probe: Eve SSH is reachable and reports iPadOS Darwin 20.3.0, but /var/mobile/Projects/Eve only contains the receiver/control app sources and no ffmpeg/avcapture helper was found. Raven SSH with the recorded key still fails publickey authentication, so Raven screen capture is prepared as a desktop-run script until SSH authority is repaired."}
|
||||
|
|
|
|||
|
|
@ -259,6 +259,14 @@ accepted_design:
|
|||
shared `clockDomainId`; the Raven path uses audio evidence routed into
|
||||
Scarlett to earn a `raven-sync` correction for Raven display frames.
|
||||
Network arrival timestamps remain metadata only.
|
||||
`MimirFfmpegRawVideoStreamSource` is the network-pixel adapter for the
|
||||
Raven/Eve ingest setup: FFmpeg owns SRT receive/decode, while Mimir owns
|
||||
exact raw-video frame reads, payload geometry, video descriptors, and
|
||||
rolling-buffer samples. `config/mimir-runtime.raven-eve.example.json`
|
||||
listens for `raven-display` on port 5200 and `eve-camera` on port 5201.
|
||||
Raven can send desktop capture with `scripts/start-raven-screen-capture-sender.ps1`.
|
||||
Eve's receiver lane exists, but the iPad-side camera producer still needs a
|
||||
native AVFoundation sender or a chosen external camera streaming app.
|
||||
`MimirPresentationControlState` owns program-control intent for Fensalir:
|
||||
video feed visibility/solo/opacity/layer order, audio mute/solo/gain, and
|
||||
global LUT preset selection. The `Mimir Program` panel filters video
|
||||
|
|
|
|||
Loading…
Reference in a new issue