Skip to content

feat(media): read Wisenet WAVE / Nx Witness exports (.nov) - #1463

Open
Ugoyo wants to merge 7 commits into
KnifeLemon:mainfrom
Ugoyo:claude/wisenet-nov
Open

Ugoyo wants to merge 7 commits into
KnifeLemon:mainfrom
Ugoyo:claude/wisenet-nov

Conversation

@Ugoyo

@Ugoyo Ugoyo commented Nov 13, 2026

Copy link
Copy Markdown
Collaborator

What does this change?

Adds .nov as a read-only video format. .nov is the export of Wisenet WAVE (Hanwha's OEM of Network Optix Nx Witness, the VMS used with Wisenet cameras) and of Nx Witness itself. A .nov is a layout file. Network Optix's open-source client (nx_open, MPL-2.0: vms/libs/nx_vms_common/src/nx/core/layout/layout_file_info.h, layout_storage_resource.cpp) writes it as a series of named streams: a NUL-terminated UTF-8 name, then the data. An index records where each stream starts:

  • version 2: after the streams, with a 20-byte footer (magic 0xdea5489c3f44ca16);
  • version 1: before the streams.

Each camera's recording is a Matroska stream named <camera id>_<start ms>.mkv. Media/NovExport.cs reads the index and gives FFmpeg the largest recording through the subfile protocol, so nothing is copied. It then goes to every video and audio target.

  • Password-protected exports are encrypted (AES-256 keyed by the password). They are refused with their own message (error.nov_password), which says to export again without a password. An export with no recording is refused with error.nov_no_video. Both keys are in all 22 i18n files.
  • Defensive reading: a damaged index throws an InvalidDataException with a clear message. The entry count is checked against the file's size before anything is allocated (version 2 must fill exactly the room between header and footer; version 1 allows at most 256 entries). Every stream must lie within the data, and names are read up to 1024 bytes.
  • Limit: an export of several cameras, or of one camera with gaps (several chunks), converts only the largest recording.
  • .exe exports (the client's player with the same streams appended) are not touched. .exe stays a Windows program (read for its icon, [Format]: .exe #1188) and is never run.
  • Settings and grade: settings schema 87 teaches existing video donuts .nov; custom claims stay put. It may need renumbering if another migration lands first. .nov is graded Specialist, in the video-pro field next to .dav; the FormatFieldsTests counts are updated.

No code was copied from nx_open: the reader is written from the structures its header documents, so nothing is added to THIRD-PARTY-NOTICES.md.

Type of change

  • Bug fix
  • New feature or improvement
  • New format or engine
  • Translation
  • Documentation
  • Build, CI or dependencies

How was it tested?

  • dotnet format --verify-no-changes (and dotnet build -c Release)
  • dotnet test: new tests pass; LocalizationTests pass. On Linux with the system's FFmpeg 6.1. The remaining failures fail on main too (Core tests that need a non-root user, 5 MediaTests that need codecs missing from this FFmpeg).
  • Tried it in the app: not tried.

New tests:

  • NovFormatTests (Core): .nov is detected as read-only video on the video donut; schema 86 → 87 adds it once.
  • NovExportTests (no FFmpeg): streams found by name with their data in both versions; protected exports refused (both versions); the largest .mkv is given as a subfile input; an export without a recording is refused; damaged indexes (not an export, cut short, wrong entry count, offset past the data, version 1 count over 256) throw InvalidDataException.
  • MediaTests.Nov_exports_are_read_but_never_written and A_wisenet_wave_export_converts_its_largest_recording_to_mp4 (versions 1 and 2): NovFile writes an export the way the client does, with a 1 s and a 2 s Matroska recording among layout streams. The 2 s one converts to MP4 (50 frames of 160x120 H.264, AAC, ~2 s).

No real sample was used. I found no public .nov sample. The test files follow the client's source code. A short unprotected export from Wisenet WAVE would be the best confirmation, if anyone has one.

Screenshots (UI changes)

None, no UI changes (two new error messages).

Checklist

  • New UI strings are in all src/Filee.App/Assets/i18n/*.json files
  • New third-party code or binaries are listed in THIRD-PARTY-NOTICES.md (none added)
  • The PR does one thing (unrelated changes go in a separate PR)

🤖 Generated with Claude Code

Wisenet WAVE (Hanwha's OEM of Nx Witness) exports a layout file: named
streams and an index of where each starts, at the end of the file
(version 2) or at its start (version 1), as Network Optix's open source
client (nx_open, MPL-2.0) writes them. Each camera's recording is a
Matroska stream inside; Filee reads the index and gives FFmpeg the
largest recording through the subfile protocol, without copying it.
Password-protected exports are encrypted and refused with their own
message, as are exports without a recording; a damaged index throws
InvalidDataException. Settings schema 87 teaches existing video donuts
.nov.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPxh5pgy6HfVPDtxbszvh3

@KnifeLemon KnifeLemon left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @Ugoyo. The index reader is careful: the entry count is checked against the file size before anything is
allocated, every offset is range-checked, and the structures match layout_file_info.h upstream. Reading the
recording in place through subfile is a nice touch.

One thing needs fixing before merge, plus a small one:

  1. Other video-donut actions still hand FFmpeg the raw .nov. NovExport.InputFor is only applied inside
    FfmpegConverter.ConvertAsync, after the Live Photo branch. These go around it and fail with FFmpeg's "Invalid data":

    • Caption (caption-video, on the video donut's front): WhisperConverter runs -i step.InputPath.
    • Live Photo / Motion Photo of a clip: step.LivePhoto returns to LivePhotoWriter before the rewrite.
    • Merge videos: JoinAsync probes Path.GetFullPath(inputPaths[i]).
    • The Cut a clip editor: VideoFrames and ClipPreview probe and preview the raw path.

    Could you resolve the input in one shared helper (path + format id → FFmpeg input) and call it from those places?
    Then every slot that reaches .nov works the same way. #1462 (Hikvision) is merged now and changed the same
    var input = ... line, so please rebase on main and let the helper cover its header rewrite too.

  2. Say when recordings are left out. For a multi-camera export, or one camera with gaps, only the largest .mkv is
    converted and the user isn't told. A step.Notice (new key in all 22 language files) when more than one recording
    is present would cover it.

Small things, no action needed:

  • The FFmpeg-backed tests (A_wisenet_wave_export_converts_its_largest_recording_to_mp4) are skipped on every CI job,
    so CI covers the parser only. That's fine, just noting it.
  • Schema 87 has just gone to #1444; we'll renumber yours at merge time.

Ugoyo and others added 3 commits November 13, 2026 12:55
Hikvision SD-card recordings (KnifeLemon#1462) and .nov exports now share the line
where FfmpegConverter picks FFmpeg's input: a .nov goes to
NovExport.InputFor with the plain path, every other file to
HikvisionStream.InputFor. Schema 87 went to the Arduino sketches on main,
so .nov takes 88.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPxh5pgy6HfVPDtxbszvh3
Review of KnifeLemon#1463: only FfmpegConverter.ConvertAsync rewrote the input, so
Caption, Live / Motion Photo, Merge videos, the Cut a clip editor and
size estimates still handed FFmpeg the raw .nov (and the header-less
Hikvision recording). MediaInput.For(path, format) now gives each of
them the same input: a .nov's recording through subfile, a Hikvision
SD-card recording with mu-law sound through concat, any other file as
it is. Merge videos' concat list allows those two protocols, which the
concat demuxer refuses by default. Only video files are looked into for
the Hikvision header.

A .nov export with more than one recording now says that only the
largest was converted (notice.nov_recordings_left_out, in all 22
languages).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPxh5pgy6HfVPDtxbszvh3
From the review of KnifeLemon#1462: HikvisionStream read the first 256 KB of every
FFmpeg input through a FileStream. It now reads with RandomAccess, and
only the first four bytes unless they are a pack header, so an MP4 or a
Matroska file costs one small read. The shared header of the Cut a clip
editor is written once, under another name first, so a reader never sees
half of it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPxh5pgy6HfVPDtxbszvh3
@Ugoyo

Ugoyo commented Nov 13, 2026 •

Copy link
Copy Markdown
Collaborator Author

Done, thanks for the review.

New Media/MediaInput.cs: one For(path, format) gives FFmpeg the input for every slot: a .nov’s recording through subfile, a header-less Hikvision recording with mu-law sound through concat (now looked for in video files only), anything else as is. Convert, Caption (Whisper), Live / Motion Photo, Merge videos, the Cut a clip editor (VideoFrames, ClipPreview) and CompressionEstimator go through it. Merge videos’ concat list adds -protocol_whitelist file,crypto,data,subfile,concat, since the concat demuxer refuses both protocols by default.
A .nov with more than one recording sets notice.nov_recordings_left_out (all 22 files). It isn’t shown for Merge videos, which has no notice channel.
New MediaInputTests: the inputs, the notice, the merge list keeping protocol inputs, and FFmpeg-backed checks: the editor’s probe and a frame from a .nov, merging .nov exports (copied) and .nov + SD-card (encoded), a Live Photo from a .nov, the notice on a job. Caption wasn’t run (no Whisper model here).
I merged main instead of rebasing, to keep the reviewed history; the migration is already v88.

Also folded in the notes from #1462: the Hikvision rewrite goes through the same MediaInput helper, so a header-less mu-law recording now works in Caption, Live / Motion Photo, Merge videos and the Cut a clip editor too. HikvisionStream reads with RandomAccess, only 4 bytes of a file that doesn’t start with a pack header, and only for video formats. The editor’s shared header file is written once, atomically.

Schema 88 went to the NuGet / APK containers (KnifeLemon#1464, KnifeLemon#1465), so .nov
takes 89; the specialist count is 124 with them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPxh5pgy6HfVPDtxbszvh3
@Ugoyo
Ugoyo requested a review from KnifeLemon November 13, 2026 18:04

@KnifeLemon KnifeLemon left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스키마 89: #1461(G-code), #1458(Gerber), #1463 셋이 모두 89를 써요. AppSettings.cs와 FormatFieldsTests(Specialist
124)는 세 PR이 같은 줄을 같은 값으로 바꿔서 서로 병합해도 텍스트 충돌이 안 나요. 두 번째부터는 숫자(90, 91)와
Specialist 개수(125, 126)를 직접 올려야 해요. 안 그러면 FormatFieldsTests가 깨지고, from < 89 블록이 두 개 생겨요.
UserDataStore.cs는 실제로 충돌해요(#1463 vs #1461, #1463 vs #1458 모두).

  • N1을 작성자에게 맡길지, 병합 때 직접 한 줄 고칠지.

5. 리뷰 본문 초안 (English)


Thanks, @Ugoyo, this is exactly what I hoped for. MediaInput.For is now the one place every slot asks, and I checked
each one from the last round: Convert, Caption (WhisperConverter), Live / Motion Photo (LivePhotoWriter), Merge
videos (per clip, with the -protocol_whitelist on the concat list), the Cut a clip editor (VideoFrames,
ClipPreview) and CompressionEstimator. The left-out recordings notice reads well too, and folding the #1462
follow-ups in (video formats only, a 4-byte read with RandomAccess, the staged header write) was a nice extra.

One small change before merge:

  1. The shared header folder should be Filee's work folder, not the temp folder. MediaInput.SharedFolder is
    Path.Combine(Path.GetTempPath(), "filee-media"). On Linux that's /tmp/filee-media, which another user can create
    first; WriteHeader then reuses that folder and an existing hikvision-imkh.bin, and the folder's owner can swap the
    file before FFmpeg reads it. That's the case WorkFolders (in Filee.Core.Platform) exists for: on Linux and macOS
    it gives a per-user folder with mode 700. Path.Combine(WorkFolders.Root, "media") fixes it (and
    ClipPreview.ReadLoudnessAsync could pass its workFolder). The docs line about "Filee's folder in the temp folder"
    would follow.

A nit, take it or leave it:

  • The new ko.json strings use 합니다체 ("…있습니다", "없습니다", "변환했습니다"), while the file uses 해요체. For example:
    "…암호화되어 있어요. … 다시 내보내 주세요", "…카메라 녹화가 없어요", "…있어요. 가장 큰 녹화만 변환했어요".

The schema number: #1456 has taken 89 meanwhile, and we'll renumber yours when merging, so no need to change it.

Review of KnifeLemon#1463: the folder used when an action has no work folder of
its own was /tmp/filee-media, which another user can create first and
then swap the header before FFmpeg reads it. It is now a "media" folder
in WorkFolders.Root, private to the user on Linux and macOS. The Cut a
clip editor's loudness reading passes its own work folder. The Korean
strings use the file's 해요체.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPxh5pgy6HfVPDtxbszvh3
@Ugoyo

Ugoyo commented Nov 13, 2026

Copy link
Copy Markdown
Collaborator Author

Done: when a slot has no work folder of its own, the header now goes to Path.Combine(WorkFolders.Root, "media") instead of /tmp/filee-media, and ClipPreview.ReadLoudnessAsync passes its workFolder. The docs line follows, and MediaInputTests checks the new place. The Korean strings now use 해요체 as suggested. Schema left at 89 for you to renumber, thanks!

Schema 89 went to the KuGou lyrics (KnifeLemon#1296), so .nov takes 90. The Cut a
clip editor's probe keeps main's sound-only summary (KnifeLemon#1430) and reads
through MediaInput.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPxh5pgy6HfVPDtxbszvh3
@Ugoyo
Ugoyo requested a review from KnifeLemon November 13, 2026 18:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants