From 5465db3ef90794d90b1a38a55ee060bd7c293680 Mon Sep 17 00:00:00 2001 From: keyldev Date: Fri, 12 Jun 2026 23:49:46 +0300 Subject: [PATCH] fix(video): ship FFmpeg DLLs as loose files, not inside single-file exe - ExcludeFromSingleFile on native Content: bundled DLLs extracted to a temp subpath the loader never probes -> every av_* call threw NotSupportedException - FfmpegRuntime: classify NotSupportedException as native-load failure and replay the failure on later init calls (no endless cryptic reconnects) - ONVIF: tolerate truncated GetSystemDateAndTime reply, assume zero time shift --- .../Onvif/OnvifClientBuilder.cs | 9 ++++++++ .../OpenIPC.Viewer.Video.csproj | 12 +++++++++++ .../Pipeline/FfmpegRuntime.cs | 21 +++++++++++++++++-- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/OpenIPC.Viewer.Devices/Onvif/OnvifClientBuilder.cs b/src/OpenIPC.Viewer.Devices/Onvif/OnvifClientBuilder.cs index ab0ac31..b681362 100644 --- a/src/OpenIPC.Viewer.Devices/Onvif/OnvifClientBuilder.cs +++ b/src/OpenIPC.Viewer.Devices/Onvif/OnvifClientBuilder.cs @@ -97,6 +97,15 @@ private static async Task CreatePreAuthDeviceAsync(Uri uri) { shift = await probe.GetDeviceTimeShift().ConfigureAwait(false); } + catch (CommunicationException) + { + // Some firmwares answer GetSystemDateAndTime with an empty/truncated + // body ("Unexpected end of file"), which used to abort the whole add + // flow. The shift only compensates camera clock skew for the + // WS-UsernameToken digest — assume zero and proceed; if the camera + // truly rejects the digest, the next call surfaces the real error. + shift = TimeSpan.Zero; + } finally { CloseQuietly(probe); diff --git a/src/OpenIPC.Viewer.Video/OpenIPC.Viewer.Video.csproj b/src/OpenIPC.Viewer.Video/OpenIPC.Viewer.Video.csproj index 17302ed..08960d5 100644 --- a/src/OpenIPC.Viewer.Video/OpenIPC.Viewer.Video.csproj +++ b/src/OpenIPC.Viewer.Video/OpenIPC.Viewer.Video.csproj @@ -25,30 +25,42 @@ the directory not existing, so contributors without a given RID's binaries aren't blocked. Linux/macOS users can alternatively rely on system apt/brew-installed FFmpeg — FfmpegRuntime falls back to the loader path. + + ExcludeFromSingleFile keeps these out of the PublishSingleFile bundle. + Bundled (IncludeNativeLibrariesForSelfExtract) they extract to a temp dir + under their runtimes\\native\ subpath, which is NOT on the host's + native search path — and AppContext.BaseDirectory still points at the exe + dir, so FfmpegRuntime.ResolveNativeDir finds nothing and every av_* call + throws NotSupportedException. Loose files next to the exe also keep the + LGPL DLLs user-replaceable. --> runtimes\win-x64\native\%(Filename)%(Extension) PreserveNewest + true false runtimes\linux-x64\native\%(Filename)%(Extension) PreserveNewest + true false runtimes\osx-arm64\native\%(Filename)%(Extension) PreserveNewest + true false runtimes\osx-x64\native\%(Filename)%(Extension) PreserveNewest + true false diff --git a/src/OpenIPC.Viewer.Video/Pipeline/FfmpegRuntime.cs b/src/OpenIPC.Viewer.Video/Pipeline/FfmpegRuntime.cs index c651d60..cf99323 100644 --- a/src/OpenIPC.Viewer.Video/Pipeline/FfmpegRuntime.cs +++ b/src/OpenIPC.Viewer.Video/Pipeline/FfmpegRuntime.cs @@ -9,11 +9,21 @@ namespace OpenIPC.Viewer.Video.Pipeline; internal static class FfmpegRuntime { private static int _initialized; + private static Exception? _initFailure; public static void EnsureInitialized() { if (Interlocked.CompareExchange(ref _initialized, 1, 0) != 0) + { + // Replay a failed first init instead of returning success — otherwise + // every later session walks straight into uninitialized bindings and + // dies mid-stream with a bare "Specified method is not supported" + // (what the field logs showed: one descriptive crash, then an endless + // reconnect loop of cryptic av_dict_set failures). + if (_initFailure is not null) + throw _initFailure; return; + } if (OperatingSystem.IsAndroid()) { @@ -54,13 +64,15 @@ public static void EnsureInitialized() // message is preserved. var (rid, _) = RuntimeIds.Current(); var probe = string.IsNullOrEmpty(nativeDir) ? "(loader path)" : nativeDir; - throw new FfmpegNativeLibsMissingException( + _initFailure = new FfmpegNativeLibsMissingException( $"FFmpeg native libraries failed to load for runtime '{rid ?? "unknown"}'. " + $"Probed: {probe}. " + $"Android: ensure runtimes/android-{{arm64,x64}}/native/*.so are populated " + $"(run tools/build-ffmpeg-android.sh via WSL or pull .so from a CI APK artifact). " + - $"Desktop: tools/fetch-ffmpeg.ps1 (Windows) or apt/brew. " + + $"Desktop: keep the runtimes/ folder from the release archive next to the exe, " + + $"or tools/fetch-ffmpeg.ps1 (Windows) / apt/brew. " + $"Underlying: {DescribeChain(ex)}", ex); + throw _initFailure; } } @@ -70,6 +82,11 @@ private static bool IsNativeLoadFailure(Exception ex) { if (cur is DllNotFoundException) return true; if (cur is BadImageFormatException) return true; + // FFmpeg.AutoGen's DynamicallyLoadedBindings doesn't throw on a + // failed library load — it leaves the function vector pointing at a + // stub that throws NotSupportedException on first call. Within this + // init scope that's always a load failure, not a real capability gap. + if (cur is NotSupportedException) return true; } return ex is TypeInitializationException; }