Skip to content
Merged
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
8 changes: 4 additions & 4 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ no `DYLD_LIBRARY_PATH` is needed.

**Camera connects but the tile stays black, log full of `vt decoder cb`
/ `hardware accelerator failed to decode picture`.** VideoToolbox rejects
HEVC frames whose reference picture is missing (typical when joining a
smart-GOP stream mid-GOP). After ~2.5s of such errors with no frame the app
switches that camera to software decode until restart; if it still stays
black, attach the log to an issue.
HEVC frames whose reference picture is missing. After ~2.5s of such errors
with no frame the app reconnects that camera with software decode and keeps
it there until restart; if it still stays black or grey, attach the log to
an issue.

**Keychain prompt on every credential read.** Means the app's
codesign identity changed between builds. Trust the keychain access
Expand Down
55 changes: 39 additions & 16 deletions src/OpenIPC.Viewer.Video/Pipeline/FfmpegVideoSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ internal sealed class FfmpegVideoSession : IVideoSession
private AVPixelFormat _selectedHwPixFmt = AVPixelFormat.AV_PIX_FMT_NONE;

// How long HW decode may keep failing (send/receive/transfer errors, no
// frame out) before the session drops to software. Time-based, not a
// count: a 1 fps camera and a 30 fps one get the same grace for the next
// keyframe, and both switch well inside the reconnect watchdog's 5s
// no-frame window, which would otherwise restart HW from scratch.
// frame out) before the session gives up on HW and reconnects in software.
// Time-based, not a count: a 1 fps camera and a 30 fps one get the same
// grace for the next keyframe, and both trip well inside the reconnect
// watchdog's 5s no-frame window, which would otherwise retry HW.
private const long HwFallbackAfterMs = 2_500;
// Streams whose HW decode broke during this run. Static because every
// reconnect builds a new session; keyed without credentials. 2.5s of
Expand Down Expand Up @@ -195,7 +195,26 @@ public async ValueTask DisposeAsync()
_decodeGate.Dispose();
}

private unsafe void Run()
private void Run()
{
// A pass that gives up on HW decode (#94) is retried once on a fresh
// RTSP session inside this same session, so one-shot callers without
// the reconnect wrapper (connect check, snapshot) get the software
// fallback too. The stream is in HwFailedStreams by then, so the
// second pass decodes in software and can't ask for another retry.
if (!RunOnce()) return;
if (_cts!.IsCancellationRequested)
{
SetState(SessionState.Idle);
return;
}
SetState(SessionState.Connecting);
if (RunOnce()) SetState(SessionState.Idle);
}

// One RTSP session from open to close. Returns true when it stopped to be
// retried in software; otherwise it has already reported Idle or Failed.
private unsafe bool RunOnce()
{
AVFormatContext* fmtCtx = null;
AVCodecContext* codecCtx = null;
Expand All @@ -212,6 +231,7 @@ private unsafe void Run()
var hwActive = false;
var hwErrors = 0;
var lastFrameMs = 0L;
var retryInSoftware = false;

try
{
Expand Down Expand Up @@ -338,21 +358,22 @@ private unsafe void Run()
Interlocked.Add(ref _bytesSinceFpsTick, packet->size);

// The HW decoder can open fine and then reject every frame —
// VideoToolbox does this on HEVC when the stream is joined
// mid-GOP and a reference picture is missing (#94), where the
// software decoder would conceal and carry on. Swap to software
// once it has failed for HwFallbackAfterMs without a frame,
// instead of stalling into a reconnect loop.
// VideoToolbox does this on HEVC when a reference picture is
// missing (#94). Once it has failed for HwFallbackAfterMs
// without a frame, mark the stream and close this RTSP session
// so Run reopens it in software. Swapping decoders in place
// doesn't work — the new decoder starts mid-stream, and on
// cameras whose frames keep referencing the keyframe sent at
// session start it never sees that keyframe and draws grey.
var sinceFrameMs = Environment.TickCount64 - lastFrameMs;
if (hwActive && hwErrors > 0 && sinceFrameMs >= HwFallbackAfterMs)
{
_logger.LogWarning("HW decode failed {Count} times over {Ms} ms on {Host}; switching to software",
_logger.LogWarning("HW decode failed {Count} times over {Ms} ms on {Host}; reopening with software decode",
hwErrors, sinceFrameMs, _options.RtspUri.Host);
HwFailedStreams.TryAdd(HwStreamKey, 0);
ffmpeg.avcodec_free_context(&codecCtx);
if (hwDeviceCtx != null) ffmpeg.av_buffer_unref(&hwDeviceCtx);
hwActive = OpenDecoder(&codecCtx, &hwDeviceCtx, codecpar, codec, HwAccelHint.None);
hwErrors = 0;
ffmpeg.av_packet_unref(packet);
retryInSoftware = true;
break;
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
}

ret = ffmpeg.avcodec_send_packet(codecCtx, packet);
Expand Down Expand Up @@ -418,7 +439,7 @@ private unsafe void Run()
{
_logger.LogError(ex, "Video session loop failed");
SetState(SessionState.Failed, ex.Message);
return;
return false;
}
finally
{
Expand All @@ -436,7 +457,9 @@ private unsafe void Run()
if (opts != null) ffmpeg.av_dict_free(&opts);
}

if (retryInSoftware) return true;
SetState(SessionState.Idle);
return false;
}

// The RTSP demuxer reports an interrupted read as "Invalid data found", so
Expand Down
Loading