Skip to content

fix(video): fall back to software when HW decode fails mid-stream (#94) - #95

Merged
keyldev merged 2 commits into
mainfrom
fix/issue-94
Oct 19, 2026
Merged

keyldev merged 2 commits into
mainfrom
fix/issue-94

Conversation

@keyldev

@keyldev keyldev commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator
  • Swap to software after 12 consecutive HW decode errors and remember the stream for the run, so reconnects skip HW
  • macOS: find Homebrew ffmpeg@7 (keg-only) libs and CLI without DYLD_LIBRARY_PATH
  • Docs, load-error text and CI: ffmpeg@7, plain brew ffmpeg is past the 7.x ABI

Summary

Related

Type

  • Bug fix
  • Feature
  • Refactor / cleanup
  • Docs / CI
  • Other:

Checklist

  • Builds with 0 warnings (TreatWarningsAsErrors=true).
  • Tests pass (dotnet test); new Core logic has unit tests.
  • No layering violation — App references Core only (Infrastructure / Video / Devices wired via DI in a head).
  • Scope stays within one phase (didn't pull work from a later phase's "Не входит").
  • README / docs updated if public commands, options, or setup changed.

Platforms tested

  • Windows
  • Linux
  • macOS
  • Android
  • iOS
  • CI build only

Screenshots / notes

- Swap to software after 12 consecutive HW decode errors and remember the stream for the run, so reconnects skip HW
- macOS: find Homebrew ffmpeg@7 (keg-only) libs and CLI without DYLD_LIBRARY_PATH
- Docs, load-error text and CI: ffmpeg@7, plain brew ffmpeg is past the 7.x ABI
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Fall back to software when hardware video decoding fails mid-stream

🐞 Bug fix 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Switch to software after 12 consecutive hardware decode errors, preventing stalled video and
 repeated failures on reconnect.
• Find Homebrew’s keg-only FFmpeg 7 libraries and CLI on macOS without loader-path configuration.
• Align macOS CI, setup guidance, and load errors with the required FFmpeg 7 ABI.
Diagram

graph TD
  S["RTSP stream"] --> O["Open decoder"] --> H{"HW eligible?"}
  H -- "yes" --> D["HW decode"] -- "error" --> E{"12 errors?"}
  E -- "yes" --> F["Software decode"] --> P["Video frames"]
  E -- "not yet" --> D
  D -- "frame" --> P
  H -- "no or cached" --> F
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Reconnect using software decoding
  • ➕ Reuses the existing session startup and decoder cleanup paths.
  • ➖ Interrupts playback for a new RTSP connection and risks another stall before recovery.

Recommendation: Keep the in-session switch: it addresses mid-stream failures without waiting for a reconnect, while the per-run cache prevents repeating them. Add targeted coverage for the error threshold, reset on a successful frame, and software selection after reconnect.

Files changed (10) +165 / -55

Bug fix (5) +135 / -49
FfmpegRuntime.csDiscover compatible macOS FFmpeg libraries +31/-3

Discover compatible macOS FFmpeg libraries

• Searches explicit loader-path entries and known Homebrew and MacPorts locations for libavcodec.61 before falling back to the platform loader. Updates native-load errors to distinguish FFmpeg 7 from incompatible formulas.

src/OpenIPC.Viewer.Video/Pipeline/FfmpegRuntime.cs

FfmpegVideoSession.csRecover from repeated hardware decode errors +71/-24

Recover from repeated hardware decode errors

• Counts consecutive send, receive, and hardware-frame-transfer errors, then reopens the decoder in software after 12 errors. Remembers failed stream URLs for the process run so new sessions skip hardware decoding, and extracts decoder opening for reuse.

src/OpenIPC.Viewer.Video/Pipeline/FfmpegVideoSession.cs

RuntimeIds.csCentralize FFmpeg CLI discovery +28/-0

Centralize FFmpeg CLI discovery

• Adds shared executable resolution that prefers a bundled binary, then known Homebrew paths on macOS, then PATH.

src/OpenIPC.Viewer.Video/Pipeline/RuntimeIds.cs

FfmpegSubprocessClipExporter.csUse shared FFmpeg CLI resolution for exports +1/-10

Use shared FFmpeg CLI resolution for exports

• Replaces the clip exporter's duplicated default-path lookup with the resolver that can find keg-only ffmpeg@7 on macOS.

src/OpenIPC.Viewer.Video/Recording/FfmpegSubprocessClipExporter.cs

FfmpegSubprocessRecorder.csUse shared FFmpeg CLI resolution for recording +4/-12

Use shared FFmpeg CLI resolution for recording

• Replaces the recorder's duplicated default-path lookup with the shared resolver and documents its macOS search order. Explicit path overrides remain unchanged.

src/OpenIPC.Viewer.Video/Recording/FfmpegSubprocessRecorder.cs

Documentation (4) +29 / -5
README.mdClarify macOS FFmpeg requirements and fallback +7/-3

Clarify macOS FFmpeg requirements and fallback

• Documents ffmpeg@7 installation, automatic discovery of its keg-only libraries, and software fallback for streams hardware decoding cannot handle.

README.md

troubleshooting.mdDocument macOS load and hardware decode failures +14/-0

Document macOS load and hardware decode failures

• Adds guidance for FFmpeg ABI load errors and black video tiles caused by VideoToolbox decode failures.

docs/troubleshooting.md

web-server.mdSpecify macOS FFmpeg 7 libraries and CLI setup +4/-1

Specify macOS FFmpeg 7 libraries and CLI setup

• Explains that live decoding needs ffmpeg@7 and that the ffmpeg CLI must also be available for web-server video operations.

docs/web-server.md

web-server.ru.mdUpdate Russian macOS FFmpeg setup guidance +4/-1

Update Russian macOS FFmpeg setup guidance

• Mirrors the FFmpeg 7 library and CLI installation guidance in the Russian web-server documentation.

docs/web-server.ru.md

Other (1) +1 / -1
build.ymlInstall FFmpeg 7 in macOS CI +1/-1

Install FFmpeg 7 in macOS CI

• Changes the macOS Homebrew install from the unversioned formula to ffmpeg@7, matching the native ABI the app requires.

.github/workflows/build.yml

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Slow cameras keep retrying broken hardware ✓ Resolved
Description
Run switches to software only after 12 hardware decode errors in one session, but the reconnect
watchdog ends a session after five seconds without a decoded frame. On a low-frame-rate stream that
produces fewer than 12 errors in that window, each reconnect starts a new counter and retries
hardware, so the software fallback is never reached.
Code

src/OpenIPC.Viewer.Video/Pipeline/FfmpegVideoSession.cs[338]

+                if (hwActive && hwErrors >= HwDecodeFailureLimit)
Evidence
The new counter is local to Run and the fallback requires 12 errors. The wrapper's watchdog
instead reconnects a Playing session after five seconds without a frame, creating another inner
session with a fresh counter.

src/OpenIPC.Viewer.Video/Pipeline/FfmpegVideoSession.cs[75-82]
src/OpenIPC.Viewer.Video/Pipeline/FfmpegVideoSession.cs[207-210]
src/OpenIPC.Viewer.Video/Pipeline/FfmpegVideoSession.cs[333-347]
src/OpenIPC.Viewer.Video/Pipeline/AutoReconnectingVideoSession.cs[22-24]
src/OpenIPC.Viewer.Video/Pipeline/AutoReconnectingVideoSession.cs[135-165]
src/OpenIPC.Viewer.Video/Pipeline/AutoReconnectingVideoSession.cs[245-258]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The per-session threshold of 12 decode errors can be unreachable before the five-second no-frame watchdog reconnects a slow stream. Each new session resets the error count and retries hardware.
## Fix Focus Areas
- src/OpenIPC.Viewer.Video/Pipeline/FfmpegVideoSession.cs[75-82]
- src/OpenIPC.Viewer.Video/Pipeline/FfmpegVideoSession.cs[333-347]
- src/OpenIPC.Viewer.Video/Pipeline/AutoReconnectingVideoSession.cs[245-258]
## Recommended Fix
Coordinate fallback with the stall deadline so a session with hardware decode failures can switch to software before the watchdog reconnects it, even when it has accumulated fewer than 12 errors. Preserve the existing consecutive-error behavior for faster streams and reset the stall tracking after a decoded frame.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. HW decode can be switched off for the whole run within half a second ✓ Resolved
Description
HwDecodeFailureLimit = 12 counts consecutive failed packets, not elapsed time, so a 25–30 fps
camera reaches the limit in about 0.4–0.5 s, not the ~1.2 s that the "~10 errors/s" comment assumes.
Any HW decoder that briefly rejects frames after joining a stream mid-GOP, or after a short burst of
packet loss, therefore gets added to the static HwFailedStreams set before the next keyframe can
let it recover, and that stream then stays on software decode on every reconnect until the app
restarts.
Code

src/OpenIPC.Viewer.Video/Pipeline/FfmpegVideoSession.cs[R76-81]

+    // between before the session drops to software. At ~10 errors/s it trips
+    // well inside the 5s stall window that would otherwise force a reconnect.
+    private const int HwDecodeFailureLimit = 12;
+    // Streams whose HW decode broke during this run. Static because every
+    // reconnect builds a new session; keyed without credentials.
+    private static readonly ConcurrentDictionary<string, byte> HwFailedStreams = new();
Evidence
Every failed send or receive on a HW decode adds one to hwErrors. Only a decoded frame resets it,
and nothing ever removes entries from the static dictionary. The switch also fires on any error,
including network corruption, with no time window. The added troubleshooting doc says this situation
is typical when joining a smart-GOP stream mid-GOP, which happens on almost every connect.

src/OpenIPC.Viewer.Video/Pipeline/FfmpegVideoSession.cs[338-347]
src/OpenIPC.Viewer.Video/Pipeline/FfmpegVideoSession.cs[349-384]
src/OpenIPC.Viewer.Video/Pipeline/FfmpegVideoSession.cs[256-263]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The HW-to-software fallback trips after 12 consecutive errors, which takes under half a second at normal frame rates. It permanently pins the stream to software for the rest of the run, even when HW would recover at the next keyframe.
## Fix Focus Areas
- src/OpenIPC.Viewer.Video/Pipeline/FfmpegVideoSession.cs[75-82]
- src/OpenIPC.Viewer.Video/Pipeline/FfmpegVideoSession.cs[338-347]
## Recommended Fix
Record when the first error in the current run of errors happened, and only fall back once errors have continued for a set time (for example 2–3 s, still under the 5 s stall window), or once a keyframe has been fed and still failed. Update the comment to match the actual rate.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Web live view still can't find Homebrew ffmpeg on macOS ✓ Resolved
Description
LiveFfmpeg.Resolve in the web server keeps its own copy of the path lookup: it probes only
runtimes/osx-x64 and then falls back to ffmpeg on PATH. It does not use the new
RuntimeIds.ResolveFfmpegExe, which checks the keg-only ffmpeg@7 and Homebrew prefixes. So the
recorder and clip exporter find /opt/homebrew/opt/ffmpeg@7/bin/ffmpeg, but web live view fails
unless the user edits PATH, as the updated docs now ask them to do.
Code

src/OpenIPC.Viewer.Video/Pipeline/RuntimeIds.cs[R42-53]

+        if (OperatingSystem.IsMacOS())
+        {
+            string[] brew =
+            {
+                "/opt/homebrew/opt/ffmpeg@7/bin/ffmpeg",
+                "/usr/local/opt/ffmpeg@7/bin/ffmpeg",
+                "/opt/homebrew/bin/ffmpeg",
+                "/usr/local/bin/ffmpeg",
+            };
+            foreach (var path in brew)
+                if (File.Exists(path)) return path;
+        }
Evidence
The PR puts the Homebrew lookup in a new shared resolver, but LiveFfmpeg keeps its own resolver
without it, and that copy even hard-codes osx-x64 on Apple Silicon. The docs work around this by
telling users to change PATH.

src/OpenIPC.Viewer.Web/Api/LiveFfmpeg.cs[125-141]
src/OpenIPC.Viewer.Video/Pipeline/RuntimeIds.cs[33-55]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The web server's LiveFfmpeg.Resolve repeats the ffmpeg path lookup and skips the new macOS Homebrew prefixes, so the web live view can't find keg-only ffmpeg@7.
## Fix Focus Areas
- src/OpenIPC.Viewer.Web/Api/LiveFfmpeg.cs[125-141]
- src/OpenIPC.Viewer.Video/Pipeline/RuntimeIds.cs[33-55]
## Recommended Fix
Make the resolver available to the Web project (make it public, or wrap it in a small public helper) and call it from LiveFfmpeg.Resolve. Then simplify the macOS section in docs/web-server*.md.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can import rules and skills only from the repositories your team follows

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/OpenIPC.Viewer.Video/Pipeline/FfmpegVideoSession.cs Outdated
Comment thread src/OpenIPC.Viewer.Video/Pipeline/FfmpegVideoSession.cs Outdated
Comment thread src/OpenIPC.Viewer.Video/Pipeline/RuntimeIds.cs Outdated
- Fall back after 2.5s of HW errors with no frame instead of 12 errors, so slow and fast cameras both switch before the 5s stall reconnect
- Share the macOS Homebrew ffmpeg paths via Core; web live view uses them and picks osx-arm64 on Apple Silicon
- Docs: no PATH change needed for ffmpeg@7
@keyldev
keyldev merged commit f79a60e into main Oct 19, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant