Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
840d7fb
feat(media2): Phase 1 — generate typed Media2 proxy via dotnet-svcuti…
Apr 18, 2026
a6873fb
fix(media2): use built-in System.ServiceModel for net48; update progr…
Apr 18, 2026
6b77a6b
review: V1 verdict — APPROVED
Apr 18, 2026
4a364b5
feat(media2): Phase 2 (tasks 2.1-2.5) — integrate typed Media2 proxy …
Apr 18, 2026
bce08f6
fix(build): fix InvokeAsync compat in SaveFileActivity and OpenFileAc…
Apr 18, 2026
158360a
fix(build): upgrade odm.onvif.extensions to TargetFrameworkVersion v4.5
Apr 18, 2026
9b0f6eb
review: V2 verdict — APPROVED
Apr 18, 2026
723f4fd
feat(media2): Phase 3 (tasks 3.1-3.4) — docs and test cleanup
Apr 18, 2026
ee33350
chore: block V3 — MSBuild permission pattern mismatch
Apr 18, 2026
efa2135
chore(V3): mark V3 completed — Release x64 build 0 errors, 69/69 offl…
Apr 18, 2026
00f8485
review: V3 verdict — APPROVED
Apr 18, 2026
46643cd
ci: restore odm.onvif.gen before solution build (fix NETSDK1004)
Apr 18, 2026
c88ff31
refactor: consolidate WSDLs to onvif/wsdl/ — remove duplicate schemas…
Apr 18, 2026
45620fb
refactor: restore full WSDL set to onvif/wsdl/ (all services: ptz, an…
Apr 18, 2026
f7f1ec5
fix: correct TargetFramework from net45 to net48 in odm.onvif.gen.csproj
Apr 18, 2026
71babae
review: WSDL consolidation structural check — APPROVED with fix
Apr 18, 2026
2661967
fix: CI build errors — net48 TFV for onvif.session + fix schema paths
Apr 18, 2026
d7967a9
chore: bump all projects to TargetFrameworkVersion v4.8
Apr 18, 2026
1e8408a
fix: resolve net48 upgrade compile errors in activities.fs and Enumer…
Apr 18, 2026
da28698
fix: upgrade odm.player.net TargetFrameworkVersion to 4.8
Apr 18, 2026
6094d8c
fix: ship System.Runtime 4.1.2.0 facade to resolve FileLoadException …
Apr 18, 2026
d21df8a
fix: resolve System.Reactive assembly key mismatch causing FileLoadEx…
Apr 18, 2026
09fbdb3
review: crash fix post-V3 addendum — APPROVED
Apr 18, 2026
0aea94b
review: full branch review — CHANGES NEEDED (issue #21 not solved end…
Apr 18, 2026
0e2c320
fix: implement issue #21 — GetVideoEncoderConfigurationOptionsMedia2 …
Apr 18, 2026
9b8556d
review: issue #21 H265 fix — APPROVED
Apr 18, 2026
75813f5
fix: tolerate HTTP 400 from device_service on Milesight cameras
Apr 18, 2026
452b63d
review: Milesight regression fix 75813f5 — APPROVED
Apr 18, 2026
940fd29
test: load Milesight integration test credentials from credentials.da…
Apr 18, 2026
198f8f9
test: harden MilesightRegressionTests against GetProfiles HTTP 400
Apr 18, 2026
c343723
test: add diagnostic endpoint dump test + annotate feedback.md with d…
Apr 18, 2026
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
3 changes: 3 additions & 0 deletions .github/workflows/odm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ jobs:

Write-Host "Version: $verInfo MSI: $verMsi ProductCode: {$newGuid}"

- name: Restore odm.onvif.gen
run: dotnet restore onvif\odm.onvif.gen\odm.onvif.gen.csproj

- name: Build application
run: msbuild ${{ env.Solution_Name }} /p:Configuration=Release /p:Platform=x64 /p:DeployExtension=false /m /nologo /v:minimal

Expand Down
224 changes: 224 additions & 0 deletions PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
# Sprint 8 — Media2 Typed Generation

## Goal

Replace the LINQ-to-XML Media2 hack with properly generated WCF types from the ONVIF Media2 WSDL
via `dotnet-svcutil`. Remove `Media2XmlParser`, raw `IMedia2`, and 8 per-operation if/else forks.
Introduce `routeMedia` Strategy helper and typed Media2 proxy.

Branch: `feat/media2-typed` from `development`

---

## Phase 1 — Generate typed Media2 proxy

**1.1** Create `feat/media2-typed` branch from `development`:
```
git -C C:\akhil\git\ONVIF-Device-Manager fetch origin
git -C C:\akhil\git\ONVIF-Device-Manager checkout -b feat/media2-typed origin/development
```

**1.2** Create project directory structure:
```
onvif/odm.onvif.gen/
wsdl/
odm.onvif.gen.csproj
```
`odm.onvif.gen.csproj` — SDK-style, targeting `net48`, referencing `System.ServiceModel`:
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<AssemblyName>odm.onvif.gen</AssemblyName>
<RootNamespace>onvif.services</RootNamespace>
<Nullable>disable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.ServiceModel.Http" Version="6.*" />
<PackageReference Include="System.ServiceModel.Duplex" Version="6.*" />
<PackageReference Include="System.ServiceModel.Security" Version="6.*" />
</ItemGroup>
</Project>
```

**1.3** Copy source files from `C:\akhil\alca\rock-onvif\onvif_gen\src\`:
- `wsdl\media2.wsdl` → `onvif/odm.onvif.gen/wsdl/media2.wsdl`
- `wsdl\media.wsdl` → `onvif/odm.onvif.gen/wsdl/media.wsdl`
- `wsdl\onvif.wsdl` → `onvif/odm.onvif.gen/wsdl/onvif.wsdl`
- `wsdl\catalog.xml` → `onvif/odm.onvif.gen/wsdl/catalog.xml`
- `schema\b-2.xsd` → `onvif/odm.onvif.gen/wsdl/b-2.xsd`
- `schema\bf-2.xsd` → `onvif/odm.onvif.gen/wsdl/bf-2.xsd`
- `schema\r-2.xsd` → `onvif/odm.onvif.gen/wsdl/r-2.xsd`
- `schema\t-1.xsd` → `onvif/odm.onvif.gen/wsdl/t-1.xsd`

**1.4** Install `dotnet-svcutil` if not present and run to generate `OnvifMedia2Gen.cs`:
```
cd C:\akhil\git\ONVIF-Device-Manager\onvif\odm.onvif.gen
dotnet tool install --global dotnet-svcutil # if not installed
dotnet-svcutil ^
-n "*,onvif.services" ^
-n "http://www.onvif.org/ver10/schema,onvif.services" ^
-n "http://www.onvif.org/ver20/media/wsdl,onvif.services" ^
--serializer XmlSerializer ^
-o OnvifMedia2Gen.cs ^
wsdl/media2.wsdl
```
Internet access required (WSDL stubs import live ONVIF.org schemas).

If svcutil reports type conflicts with existing `onvif.services` types, re-run with:
```
--reference C:\akhil\git\ONVIF-Device-Manager\onvif\onvif.services\onvif.services.csproj
```
This tells svcutil to reuse types already in `onvif.services` instead of regenerating them.

Verify the generated `OnvifMedia2Gen.cs` contains:
- `interface IMedia2` with typed return types (NOT `System.ServiceModel.Channels.Message`)
- `class Media2Client : System.ServiceModel.ClientBase<IMedia2>`
- `class MediaProfile` with `Configurations: ConfigurationSet`
- `class ConfigurationSet` with `VideoEncoder: VideoEncoder2Configuration`
- `class VideoEncoder2Configuration` with `Encoding` as `string`

If svcutil generates duplicate type definitions that conflict with `onvif.types.cs` /
`Reference.cs` (e.g. `Profile`, `VideoEncoderConfiguration`), remove the duplicates from
`OnvifMedia2Gen.cs`. Keep only types that are new: the `IMedia2` interface, client proxy,
`MediaProfile`, `ConfigurationSet`, `VideoEncoder2Configuration`, and any Media2-specific types.

**1.5** Add `odm.onvif.gen.csproj` to `odm.sln`:
Use `dotnet sln C:\akhil\git\ONVIF-Device-Manager\odm.sln add C:\akhil\git\ONVIF-Device-Manager\onvif\odm.onvif.gen\odm.onvif.gen.csproj`

**V1 — VERIFY 1**
- Build: `msbuild C:\akhil\git\ONVIF-Device-Manager\odm.sln /p:Configuration=Release /p:Platform=x64 /v:minimal`
- Must succeed with 0 errors.
- Confirm `OnvifMedia2Gen.cs` is committed.
- Push: `git -C C:\akhil\git\ONVIF-Device-Manager push origin feat/media2-typed`
- **STOP** — report to PM.

---

## Phase 2 — Integrate typed IMedia2 into NvtSession.fs

**2.1** Add project reference from `onvif.session.fsproj` to `odm.onvif.gen.csproj`:
In `onvif/onvif.session/onvif.session.fsproj`, add:
```xml
<ProjectReference Include="..\odm.onvif.gen\odm.onvif.gen.csproj" />
```
(Remove any existing `ProjectReference` to the old IMedia2 if one exists.)

**2.2** Implement `routeMedia` Strategy helper in `NvtSession.fs`.
Find where `GetMedia2Client` is defined (around line 1218) and add `routeMedia` nearby:
```fsharp
/// Routes a Media operation: try Media2 first (with HTTP fallback), then fall back to Media1.
let routeMedia
(media2Work: IMedia2 -> Async<'T>)
(media1Work: IMediaAsync -> Async<'T>) : Async<'T> = async {
let! m2 = GetMedia2Client()
if m2 |> NotNull then
try return! withMedia2HttpFallback m2 media2Work
with _ ->
let! m1 = GetMediaClient()
if m1 |> NotNull then return! withMedia1HttpFallback m1 media1Work
else return raise (System.InvalidOperationException("No Media service available"))
else
let! m1 = GetMediaClient()
if m1 |> NotNull then return! withMedia1HttpFallback m1 media1Work
else return raise (System.InvalidOperationException("No Media service available"))
}
```
Replace `IMedia2` in the type annotation with the fully-qualified type from `onvif.services`
if needed (e.g., `onvif.services.IMedia2`).

**2.3** Add type mapping helpers (only if needed — check if svcutil reused existing types):
```fsharp
/// Map a generated MediaProfile to the existing Profile type used by INvtSession.
let private mapMedia2Profile (p: onvif.services.MediaProfile) : Profile =
Profile(token = p.token, name = p.Name, ...)
// Fill in all Profile fields from ConfigurationSet p.Configurations

/// Map VideoEncoder2Configuration (Encoding: string) to VideoEncoderConfiguration (VideoEncoding enum).
let private mapVideoEncoder2Config (c: onvif.services.VideoEncoder2Configuration) : VideoEncoderConfiguration =
let encoding =
match c.Encoding with
| "H265" | "H.265" | "HEVC" -> VideoEncoding.h265
| "H264" | "H.264" -> VideoEncoding.H264
| "MPEG4" -> VideoEncoding.MPEG4
| _ -> VideoEncoding.JPEG
VideoEncoderConfiguration(encoding = encoding, ...)
```
Only add mappers that are actually needed. If svcutil reused `Profile` and
`VideoEncoderConfiguration` directly, no mapping needed.

**2.4** Replace the 8 per-operation if/else forks with `routeMedia` calls.
For each operation in `NvtSession.fs` that currently has:
```fsharp
let! media2 = GetMedia2Client()
if media2 |> NotNull then
// Media2 path
...
else
// Media1 path
...
```
Replace with:
```fsharp
return! routeMedia
(fun m2 -> async { ... (* typed Media2 call *) })
(fun m1 -> ... (* Media1 call as before *))
```
Operations to migrate:
- `GetProfiles`
- `GetStreamUri`
- `GetVideoEncoderConfigurations`
- `GetVideoEncoderConfigurationOptions`
- `SetVideoEncoderConfiguration`
- `GetAudioEncoderConfigurations`
- `SetAudioEncoderConfiguration`
- Any other operations that had the if/else fork in the feat/media2-support branch.

**2.5** Remove raw-XML approach code from `onvif/onvif.services/onvif.services.cs`:
- Delete the `interface IMedia2` block (the raw Message version — NOT the generated one).
- Delete all `Media2GetXxxRequest` and `Media2GetXxxResponse` classes.
- Delete `class Media2EncoderOptions`.
- Delete `static class Media2XmlParser` (~250 lines of LINQ-to-XML parsing).
- Remove any `using` statements that are now unused.

**V2 — VERIFY 2**
- Build Release x64. Must succeed with 0 errors.
- Run offline unit tests:
`dotnet build C:\akhil\git\ONVIF-Device-Manager\odm\odm.tests\odm.tests.csproj -v quiet`
`vstest.console.exe ... --TestCaseFilter:"TestCategory!=Integration"`
All previously-passing tests (54) must still pass.
- Push: `git -C C:\akhil\git\ONVIF-Device-Manager push origin feat/media2-typed`
- **STOP** — report to PM.

---

## Phase 3 — Tests and docs

**3.1** Update test projects:
- **Delete** `odm/odm.tests/Media2XmlParserTests.cs` — tests the removed parser.
- **Review** `odm/odm.tests/Media2IntegrationTests.cs`:
- Remove any tests that tested internal parsing behaviour of `Media2XmlParser`.
- Keep behavioral integration tests (those that test `INvtSession` operations against a real camera).
- Update any references to removed types (`Media2EncoderOptions`, raw `IMedia2`).

**3.2** Rewrite `docs/features/media2-routing.md`:
- Describe the typed approach: generated `IMedia2`, `routeMedia` helper, fallback chain.
- Explain how to add a new Media2 operation in future (call the typed method — no parser needed).
- Remove references to `Media2XmlParser` and raw Message approach.

**3.3** Update `docs/architecture.md`:
- Add a "Media2 Service Detection" subsection under the Transport Layer section.
- Cover: memoized `GetMedia2Client`, null-return on non-Media2 cameras, routeMedia helper,
zero overhead for Media1-only cameras.

**3.4** Update `docs/features/media2-testing.md`:
- Remove references to `Media2XmlParser`.
- Update test file locations if any changed.
- Keep integration test instructions (ODM_TEST_HOST etc.) intact.

**V3 — VERIFY 3**
- Build Release x64. Must succeed.
- Run offline unit tests. All must pass.
- Confirm `Media2XmlParserTests.cs` is deleted.
- Push: `git -C C:\akhil\git\ONVIF-Device-Manager push origin feat/media2-typed`
- **STOP** — report to PM.
18 changes: 18 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,24 @@ Key helpers in `SslStreamHelpers` module:
- `parseResponse` — splits HTTP response into status, headers, body
- `stripDoctype` — removes XML DOCTYPE declarations before WCF message parsing

### Media2 Service Detection

ODM supports both Media (ver10) and Media2 (ver20). Detection happens once per session:

- `GetResolvedEndpoints()` calls `GetServices()` and maps each namespace URL to an XAddr.
- `ServiceEndpointMap.HasMedia2` is `true` when `http://www.onvif.org/ver20/media/wsdl` is present.
- `GetMedia2Client()` is memoized: it returns `null` for Media1-only cameras (no retry overhead).

The `routeMedia` helper (defined just below `GetMedia2Client`) routes each operation:

```
GetMedia2Client() → non-null → typed Media2 call
↘ exception → GetMediaClient() → Media1 call
→ null ──────────────────→ GetMediaClient() → Media1 call
```

The generated proxy (`onvif/odm.onvif.gen/OnvifMedia2Gen.cs`) provides the typed `Media2` interface and `Media2Client`. See `docs/features/media2-routing.md` for how to add new operations.

### `FixUrl` — host/port fixup

`FixUrl` (NvtSession.fs) corrects RTSP stream URIs where the camera's self-reported host/port does not match the network address used to connect. It handles:
Expand Down
70 changes: 70 additions & 0 deletions docs/features/media2-routing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Media2 Routing

## Overview

ODM supports both the ONVIF Media (ver10) and Media2 (ver20) services via a typed WCF proxy generated from the ONVIF Media2 WSDL. The `routeMedia` helper encapsulates the fallback chain so each operation stays concise.

## Generated proxy

`onvif/odm.onvif.gen/OnvifMedia2Gen.cs` is auto-generated by `dotnet-svcutil` from `wsdl/media2.wsdl`. It provides:

- `interface Media2` — typed contract with strongly-typed request/response messages
- `class Media2Client` — `ClientBase<Media2>` WCF proxy
- `class MediaProfile`, `ConfigurationSet`, `VideoEncoder2Configuration` — Media2-specific data types

Do not edit this file by hand. Re-run svcutil if the WSDL changes.

## Service detection

`GetMedia2Client()` (in `NvtSession.fs`) is memoized per session. It calls `GetResolvedEndpoints()` and returns `null` if the camera does not advertise Media2 in its `GetServices()` response. There is no network overhead for Media1-only cameras beyond the single `GetServices()` call that is already required.

## `routeMedia` helper

```fsharp
let routeMedia
(media2Work: onvif.services.Media2 -> Async<'T>)
(media1Work: IMediaAsync -> Async<'T>) : Async<'T>
```

**Fallback chain:**

1. Obtain the memoized `Media2Client`.
2. If non-null, call `media2Work`. On success, return result.
3. On exception, fall back to `GetMediaClient()` and call `media1Work`.
4. If `Media2Client` is null from the start, skip straight to step 3.
5. If neither client is available, raise `InvalidOperationException("No Media service available")`.

## Adding a new Media2 operation

1. Find the typed method on the generated `Media2` interface in `OnvifMedia2Gen.cs`.
2. Add the operation to `INvtSession` (if it needs to be callable from activities).
3. Implement via `routeMedia` in `NvtSession.fs`:

```fsharp
member this.GetSomeProfiles() = async {
return! routeMedia
(fun m2 -> async {
let req = new onvif.services.GetProfilesRequest()
let! resp = Async.AwaitTask(m2.GetProfilesAsync(req))
return resp.Profiles
})
(fun m1 -> m1.GetProfiles())
}
```

No XML parsing, no `Message` objects, no manual namespace handling — the generated proxy handles all serialization.

## Encoding mapping

`VideoEncoder2Configuration.Encoding` is a plain `string` in Media2 (vs. the `VideoEncoding` enum in Media1). Map it when bridging to the existing type system:

```fsharp
let enc =
match (c.Encoding |> SuppressNull "").ToUpperInvariant() with
| "H265" -> VideoEncoding.h265
| "JPEG" -> VideoEncoding.jpeg
| "MPEG4" -> VideoEncoding.mpeg4
| _ -> VideoEncoding.h264
```

Add new codec strings here as cameras are encountered.
39 changes: 39 additions & 0 deletions docs/features/media2-testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Media2 Testing

## Test files

| File | Category | Description |
|------|----------|-------------|
| `odm/odm.tests/Media2IntegrationTests.cs` | Integration | Behavioral tests against a real camera using `INvtSession.GetVideoEncoderConfigurationsMedia2` |

Integration tests require the `ODM_TEST_HOST` environment variable. They are skipped in CI via `Assert.Inconclusive` when that variable is absent.

## Running offline tests

```
dotnet build odm\odm.tests\odm.tests.csproj -v quiet
vstest.console.exe odm\odm.tests\bin\Debug\net48\odm.tests.dll --TestCaseFilter:"TestCategory!=Integration"
```

No camera required.

## Running integration tests

Set `ODM_TEST_HOST` to the camera's IP address (and optionally `ODM_TEST_USER` / `ODM_TEST_PASS`), then run all tests without the filter:

```
set ODM_TEST_HOST=192.168.1.100
set ODM_TEST_USER=admin
set ODM_TEST_PASS=password
vstest.console.exe odm\odm.tests\bin\Debug\net48\odm.tests.dll
```

## What to test

- `GetVideoEncoderConfigurationsMedia2` returns a non-empty array on a Media2-capable camera.
- Encoding strings (`H264`, `H265`, `MPEG4`, `JPEG`) round-trip correctly to `VideoEncoding` enum values.
- A Media1-only camera (no `Media2XAddr` in `GetServices()`) still returns results via the Media1 fallback.

## Adding tests for new Media2 operations

Add integration test methods to `Media2IntegrationTests.cs` using the `[TestCategory("Integration")]` attribute. Test the `INvtSession` surface — not the internal proxy types — so tests stay valid regardless of the underlying WCF implementation.
Loading
Loading