Skip to content

feat: Media2 typed proxy + H265 VideoSettings fix (issue #21)#34

Draft
kumaakh wants to merge 31 commits into
developmentfrom
feat/media2-typed
Draft

feat: Media2 typed proxy + H265 VideoSettings fix (issue #21)#34
kumaakh wants to merge 31 commits into
developmentfrom
feat/media2-typed

Conversation

@kumaakh
Copy link
Copy Markdown

@kumaakh kumaakh commented Apr 18, 2026

Summary

  • Sprint 8: Generates a typed Media2 ONVIF proxy via dotnet-svcutil and integrates it into NvtSession.fs, replacing untyped dynamic calls with strongly-typed Media2 operations
  • Issue VideoSettings: H265 shows only one resolution option; disappears from encoder list after switching to H264 #21: Fixes H265 VideoSettings showing only one resolution and disappearing from the encoder dropdown after switching to H264 — uses GetVideoEncoderConfigurationOptionsMedia2 (capability query, independent of current config state) to supplement sparse Media1 H265 options
  • Regression fix: Milesight cameras that advertise Media2 but incorrectly point the XAddr at device_service (firmware bug) no longer cause HTTP 400 errors — GetCapabilities and GetMedia2Client now fail gracefully per the swiss-army-knife best-effort policy
  • Build fix: Aligns onvif.utils and onvif.session solution mappings to Net40 (both Debug|x64 and Release|x64) — resolves System.Reactive assembly key mismatch (FileLoadException)

Closes

Fixes #21

Test plan

  • Release|x64 build passes with 0 errors
  • H265 cameras: full resolution list shown in VideoSettings
  • H265 stays in encoder dropdown after applying H264 config
  • Milesight cameras (e.g. 192.168.1.190): no HTTP 400 errors, camera loads normally
  • Media1-only cameras: no regression in VideoSettings behaviour
  • Offline unit tests pass

Reviews

🤖 Generated with Claude Code

Azure Pipeline and others added 11 commits April 18, 2026 01:31
…l (tasks 1.1–1.5)

- Create feat/media2-typed branch from development
- Add onvif/odm.onvif.gen SDK-style net48 project with System.ServiceModel.Http 6.*
- Copy WSDL/schema source files from rock-onvif reference repo
- Run dotnet-svcutil 2.1.0 against https://www.onvif.org/ver20/media/wsdl/media.wsdl
  generating OnvifMedia2Gen.cs with typed Media2 interface and client proxy
- Add odm.onvif.gen.csproj to odm.sln

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ess.json

Remove incompatible System.ServiceModel.Http/Duplex/Security 6.* NuGet refs
(they target net6+ only). net48 uses the BCL System.ServiceModel reference
added by svcutil. V1 build: 0 errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 1 Media2 typed generation passes all criteria: typed interface,
correct types, net48 target, WSDL committed, project in solution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…into NvtSession

2.1: Add ProjectReference from onvif.session.fsproj to odm.onvif.gen.csproj
2.2: Implement routeMedia Strategy helper (onvif.services.Media2 / IMediaAsync)
2.3: No separate type-mapping helpers needed; mapping done inline
2.4: Refactor GetVideoEncoderConfigurationsMedia2 to use routeMedia with
     Media1 fallback (replaces the single GetMedia2Client if/else fork)
2.5: Remove raw IMedia2 interface and Media2GetVideoEncoderConfigurationsRequest
     from onvif.services.cs
Build compat: bump odm.onvif.gen to net45, ui projects to v4.5, fix
     Primitives.fs InvokeAsync call, align sln Debug/Release|x64 to Net45

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tivity (V2 verify)

The net45 upgrade exposed two more InvokeAsync call sites that needed
.Task |> Async.AwaitTask to match the updated DispatcherOperation API.

V2 verify: Release x64 build 0 errors; 69/69 offline unit tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Consistent with other projects upgraded to net45 for Media2 typed proxy compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- No Media2XmlParserTests.cs or Media2IntegrationTests.cs existed to remove (task 3.1 no-op)
- Add docs/features/media2-routing.md: typed proxy, routeMedia helper, fallback chain, how to add new ops
- Add Media2 Service Detection subsection to docs/architecture.md under Transport Layer
- Add docs/features/media2-testing.md: offline/integration test instructions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ine tests passed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…alytics, devicemgmt, imaging, events, etc.)
Azure Pipeline and others added 2 commits April 18, 2026 07:52
The WSDL consolidation commit inadvertently set TargetFramework to net45.
This restores the correct net48 target to match the solution requirements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
TargetFramework was net45, corrected to net48. All other checks pass:
WSDL files consolidated, schemas deleted, Service References untouched.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- onvif.session: bump TargetFrameworkVersion v4.5 → v4.8 (Net45 platform
  condition) so it can reference odm.onvif.gen which targets net48
- onvif.services: update all schemas\ paths to ..\wsdl\ following WSDL
  consolidation; add Link element to EmbeddedResource to preserve logical
  resource name onvif.schemas.onvif.xsd

Fixes CS1566 (missing schemas\onvif.xsd) and MSB4078 TFM mismatch.
All 25 library/app projects were on v4.0 or v4.5 via platform-conditional
TFV settings. The main app (odm.ui.app) was already v4.8. Sprint 8 exposed
the inconsistency when odm.onvif.gen (net48) caused FS0039 and TFM mismatch
errors in downstream projects that referenced it.

Bulk upgrade — no logic changes.
@kumaakh kumaakh marked this pull request as draft April 18, 2026 12:15
…ableExtensions

- activities.fs: replace Dispatcher.InvokeAsync (returns DispatcherOperation,
  not Async<unit>) with synchronous Dispatcher.Invoke wrapped in async block.
  InvokeAsync was added in .NET 4.5 but its return type conflicts with
  Async.StartWithContinuations when targeting net48.

- EnumerableExtensions: remove custom Append<T> extension method. System.Linq
  gained Enumerable.Append in .NET 4.7.1, causing CS0121 ambiguity at net48.
  The LINQ version is identical for non-null collections.
C++/CLI project was targeting 4.0 but referenced managed assemblies
(utils.common, utils.diagnostics) now target v4.8. Compiler refused
to auto-reference them, breaking 'using namespace utils' in libapi.cpp.
Upgrading to 4.8 aligns the C++/CLI project with the rest of the solution.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…at pull point events

onvif.utils.dll compiled against net48 reference assemblies embeds a reference
to System.Runtime v4.1.2.0 (the net48 Facades DLL). At runtime the GAC only
contains v4.0.0.0, so without the DLL present the CLR throws FileLoadException
approximately 1 minute after launch when pull-point subscription fires.

Fix: add System.Runtime.dll from the net48 Facades directory as a Content item
in odm.ui.app so it is copied to the output directory. The CLR finds it there,
satisfies the v4.1.2.0 version requirement, and follows the type-forwarders to
mscorlib. No binding redirect required.
…ception crash

onvif.session and onvif.utils were built with Release|Net45 in the solution,
linking against Rx 2.0.20823 Net45 DLLs (PublicKeyToken=f300afd708cefcd3).
All other assemblies referenced Net40 Rx (PublicKeyToken=31bf3856ad364e35).
At runtime the CLR rejected the key mismatch at GetPullPointEvents, crashing ODM.

Fix: align both projects to Release|Net40, making all assemblies consistently
reference the same Rx build. Also adds AutoGenerateBindingRedirects=false to
prevent MSBuild from overriding the manual System.Runtime binding redirect, and
adds a crash.log writer to the unhandled exception handler for future diagnostics.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reviewed commit d21df8a (System.Reactive assembly key mismatch fix).
All four changed files verified correct. One minor follow-up noted:
Debug|x64 mappings still reference Net45 for the two affected projects.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…-to-end)

GetVideoEncoderConfigurationOptions still uses Media1 only; H265
resolution options are never fetched from Media2. Branch needs a
routeMedia-based GetVideoEncoderConfigurationOptionsMedia2 method.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…+ H265 options synthesis

Three changes as specified in issue #21:

1. Add GetVideoEncoderConfigurationOptionsMedia2 to INvtSession interface and
   NvtSession.fs implementation. Uses routeMedia to call Media2
   GetVideoEncoderConfigurationOptionsAsync, falling back to [] on error.

2. Update VideoSettingsActivity.fs load() to fetch Media2 encoder options via
   the new method and synthesize options.h265 (resolutions, frame rate range,
   GOV length range) when Media1 options are absent or sparse. Fixes H265
   resolution list disappearing and H265 vanishing from encoder list after
   H264 apply.

3. Fix Debug|x64 Net45->Net40 in odm.sln for GUIDs 902A3FF3 (onvif.session)
   and 55DED141 (onvif.utils).

Also add odm.onvif.gen ProjectReference to onvif.utils, odm.onvif.extensions,
and odm.ui.activities fsproj files — required because F# needs a direct
assembly reference for any type appearing in a referenced interface signature
(VideoEncoder2ConfigurationOptions in INvtSession).

Annotate feedback.md with Doer: section.

Build: Release|x64 0 errors.
GetVideoEncoderConfigurationOptionsMedia2 correctly implements Media2
options fetch via routeMedia. VideoSettingsActivity H265 synthesis has
proper guards, type conversions, and exhaustive match. Debug|x64
Net45→Net40 alignment resolved. Issue #21 solved end-to-end.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two defensive fixes to prevent cameras that return HTTP 400 for
GetCapabilities from surfacing the error to the user:

1. GetAllCapabilities: wrap dev.GetCapabilities() in try/catch.
   Previously an unprotected call — if a camera returns 400 for
   GetCapabilities, the exception propagated through SectionDevice.Load
   and showed an error panel. Now logs the error and returns an empty
   Capabilities object, allowing sections to gracefully degrade
   (no sections loaded = tolerable for non-compliant cameras).

2. routeMedia: wrap GetMedia2Client() in try/catch. The call was
   outside the try-with that protects media2Work. If the memoized
   Media2 channel creation failed (e.g. firmware bug mapping Media2
   xAddr to device_service causing 400), the exception escaped
   routeMedia and surfaced through GetVideoEncoderConfigurationsMedia2
   or GetVideoEncoderConfigurationOptionsMedia2. Now falls through to
   Media1 silently, consistent with swiss-army-knife best-effort policy.

Regression introduced in 0e2c320: GetVideoEncoderConfigurationOptionsMedia2
triggers routeMedia which calls GetMedia2Client, whose memoized channel
(pointing at device_service on Milesight firmware) faults on the first
Media2 SOAP call, causing subsequent calls to throw CommunicationException
that leaked past insufficient catch coverage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@kumaakh kumaakh changed the title feat: Media2 typed proxy via svcutil — replace XML parser hack feat: Media2 typed proxy + H265 VideoSettings fix (issue #21) Apr 18, 2026
…t via DPAPI

ClassInitialize now defaults ODM_TEST_HOST=192.168.1.190 and walks up
the directory tree to find build/config/credentials.dat, decrypts it
with ProtectedData.Unprotect (same DPAPI mechanism as CredentialStore),
and uses the first stored credential — falling back to ODM_TEST_USER/
ODM_TEST_PASS env vars only when no credentials.dat is found.

Integration test results on 192.168.1.190:
  PASS  GetAllCapabilities_DoesNotThrow_OnMilesight
  FAIL  GetVideoEncoderConfigurationOptionsMedia2_DoesNotThrow_OnMilesight
  FAIL  GetVideoEncoderConfigurationOptionsMedia2_ReturnsOptions_OnCompliantCamera

The two failures trace to GetProfiles() receiving HTTP 400 from the
camera's Media1 endpoint (onvif/Media) — the same class of error fixed
for GetAllCapabilities in 75813f5, but GetProfiles has no equivalent
defensive handling yet.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Milesight cameras return HTTP 400 from the Media1 endpoint for GetProfiles.
GetVideoEncoderConfigurationOptionsMedia2_DoesNotThrow now falls back to an
empty profile token when GetProfiles fails (still verifies no-throw contract).
GetVideoEncoderConfigurationOptionsMedia2_ReturnsOptions marks Inconclusive
instead of erroring when GetProfiles is unavailable.

Results on 192.168.1.190:
  PASS   GetAllCapabilities_DoesNotThrow_OnMilesight
  PASS   GetVideoEncoderConfigurationOptionsMedia2_DoesNotThrow_OnMilesight
  SKIP   GetVideoEncoderConfigurationOptionsMedia2_ReturnsOptions_OnCompliantCamera (Inconclusive — camera not Media2-compliant)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…oer notes

Add Diagnostic_DumpServiceEndpoints integration test to inspect camera
service endpoints, capabilities, profiles, and Media2 encoder options.
Annotate feedback.md review findings with commit references.

Build: Release|x64 0 errors. Tests: 69/69 pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

VideoSettings: H265 shows only one resolution option; disappears from encoder list after switching to H264

1 participant