Skip to content

Add ndr.reader.video: a reader for mp4/avi/mov video - #140

Merged
stevevanhooser merged 6 commits into
mainfrom
claude/native-video-reader
Oct 20, 2026
Merged

stevevanhooser merged 6 commits into
mainfrom
claude/native-video-reader

Conversation

@jesshaley

@jesshaley jesshaley commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

This PR adds ndr.reader.video, an NDR reader for .mp4, .avi and .mov files. The video reader type is defined by the file format, not by a library. It reads the frames of a video container (MPEG-4, AVI, QuickTime) and the time stored for each frame, so any language can implement it. This MATLAB implementation uses MATLAB's built-in VideoReader and needs no toolbox. A Python implementation can use any library that reads the same containers and their frame timestamps (e.g. PyAV).

Until now the only NDR reader that could open video was ndr.reader.imagestack, which depends on NANSEN. With this reader, NDI can read video through ndi.daq.reader.image.ndr('video') without extra toolboxes. The NDI V2 Haley import records 'video' as its cameras' reader.

  • New reader +ndr/+reader/video.m (subclass of ndr.reader.base). It has the same frame API as imagestack/tiffstack: numframes, framesize, dimensionorder, datatype, frametimes, readframes (including SelectC/SelectZ), epochclock, t0_t1, getchannelsepoch and metadata.
    • Dimensions: frames are [Y X C Z T] in 'YXCZT' order, with Z=1. RGB video has C=3 and greyscale video has C=1. Both C and the datatype are read from the first decoded frame.
    • Epochs: an epoch is one video file. Directories are expanded and non-video companion files (such as epochprobemap.ndi) are ignored. Anything other than exactly one video file is an error.
    • Frame times are each frame's presentation time: the timestamp the container stores for the frame, in seconds from the start of the file, on the dev_local_time clock. They are read, not computed from the frame rate. In MATLAB, frametimes decodes the file once and records VideoReader's CurrentTime before each readFrame, then caches the result per file for the session. This matters on real data: a one-hour lab video (10,796 frames) drifts up to 1.33 s from (k-1)/FrameRate.
    • t0_t1 reads only a few frames (ndr.reader.video.endtimes). It reads the first frame's time before decoding frame 1. For the last frame it seeks a few frame periods before Duration and decodes to the end, widening the margin if the seek overshoots. It falls back to the full scan only when the file reports no usable FrameRate or Duration. NDI asks every epoch for t0_t1 when it lists a system's epochs, so a full decode there would decode every video of the system. On the one-hour video this takes 0.25 s, against 10.2 s for the full scan, with identical start and end times.
    • numframes uses NumFrames, and counts frames by decoding only when the container reports no count. readframes reads with read(v, idx), one call per run of consecutive indices.
    • metadata: israster=false; only frame_period = 1/FrameRate is filled in.
  • Registration: added to resource/ndr_reader_types.json as video, mp4, avi and mov. These are format names only, so the stored reader type means the same thing in MATLAB and Python.
  • Merged with main, including smartspim: prefer SmartSPIM's stitched All_Channels volume #142. The merge was clean.

Tests

tools/tests/+ndr/+unittest/+reader/TestVideo.m writes synthetic videos: 12-frame 24x32 at 4 fps, plus a 60-frame one. RGB video is written as MPEG-4 where the platform supports it and as Motion JPEG AVI otherwise; greyscale is written as lossless Grayscale AVI. The tests cover:

  • the known_readers listing, the size, dimension order and datatype;
  • frame times and their spacing, and t0_t1;
  • random access with indices in mixed order, SelectC, and greyscale content round-tripping exactly;
  • companion files being ignored, and the errors for zero or several video files and bad indices;
  • metadata;
  • t0_t1 on a file neither cache has seen, which must agree with the full scan, for both RGB and greyscale;
  • endtimes on a 60-frame video, which reaches the end by seeking and must agree with the full scan.

On real data, on macOS with the Haley lab's StreamPix .mp4 files:

  • a one-hour behaviour video and a 74-frame lawn clip both give identical start and end times from the seek and from the full scan;
  • NumFrames equals the number of frames decoded;
  • read through NDI, the videos give 117 and 118 frames of 1024x1024x3 uint8.

In NDI, TestHaleyReadVideo (VH-Lab/NDI-matlab#1021) reads a frame through daqsystem_load → readframes using this branch.

Test plan

  • Test code (run_tests.yml) green on 6dfeb51: 409 passed, 0 failed. The 3 incompletes are the CED sonpipe tests, which skip without that tool.
  • Test NDR Symmetry and Test .smrx green on 6dfeb51.
  • Re-run on 488417d (help text and the videoreader alias only; no code change).

After merge

NDI's test-import-v2.yml checks out this branch in a step marked temporary. That step can be removed once this PR is merged.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FxKLA8Tbf9cpvDe7fuVeKf


Generated by Claude Code

Reading video (.mp4/.avi/.mov) through NDR previously required NANSEN via
ndr.reader.imagestack. ndr.reader.video uses only MATLAB's built-in
VideoReader and implements the same frame API (numframes, framesize,
dimensionorder, datatype, frametimes, readframes, epochclock, t0_t1,
getchannelsepoch, metadata) with the YXCZT dimension model.

Frame times are VideoReader CurrentTime recorded before each readFrame
(correct for variable frame rate), cached per file. Registered in
ndr_reader_types.json as "video" (aliases videoreader, mp4, avi, mov).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FxKLA8Tbf9cpvDe7fuVeKf
Comment thread +ndr/+reader/video.m Fixed
Comment thread +ndr/+reader/video.m Fixed
Comment thread +ndr/+reader/video.m Fixed
Comment thread +ndr/+reader/video.m Fixed
Comment thread +ndr/+reader/video.m Fixed
Comment thread +ndr/+reader/video.m Fixed
Comment thread +ndr/+reader/video.m Fixed
Comment thread +ndr/+reader/video.m Fixed
Comment thread +ndr/+reader/video.m Fixed
Comment thread +ndr/+reader/video.m Fixed
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

412 tests  +14   409 ✅ +14   43s ⏱️ -4s
 52 suites + 1     3 💤 ± 0 
  1 files   ± 0     0 ❌ ± 0 

Results for commit 794a0e8. ± Comparison against base commit 966fe3c.

♻️ This comment has been updated with latest results.

@codecov

codecov Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.51685% with 24 lines in your changes missing coverage. Please review.
✅ Project coverage is 54.88%. Comparing base (966fe3c) to head (794a0e8).

Files with missing lines Patch % Lines
+ndr/+reader/video.m 86.51% 24 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #140      +/-   ##
==========================================
+ Coverage   54.26%   54.88%   +0.61%     
==========================================
  Files         163      164       +1     
  Lines        9420     9598     +178     
==========================================
+ Hits         5112     5268     +156     
- Misses       4308     4330      +22     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FxKLA8Tbf9cpvDe7fuVeKf
jesshaley pushed a commit to VH-Lab/NDI-matlab that referenced this pull request Oct 19, 2026
Builds TestHaleyRecordings' fixture, replaces camera 2's placeholder
2022-02-04_12-10-51_2.mp4 with a real 5-frame video of known content,
runs import_V2's write path for concentration_0001, then rebuilds camera 2
with daqsystem_load and checks numframes and readframes (size, class,
per-channel means within a codec tolerance). Fails, rather than filters,
when ndr.reader.video is absent.

test-import-v2.yml: a TEMPORARY step puts NDR-matlab's
claude/native-video-reader branch first on the path, until
VH-Lab/NDR-matlab#140 merges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FxKLA8Tbf9cpvDe7fuVeKf
t0_t1 went through frametimes, which decodes every frame once to measure
its time. NDI asks every epoch for t0_t1 when it lists a system's
epochs, so listing a camera decoded every video on it. t0_t1 now uses
ndr.reader.video.endtimes: the first frame's time is read before frame
1 is decoded, and the last by seeking a few frame periods before
Duration and decoding to the end (widening the margin if the seek lands
past the last frame; the full scan only if there is no usable FrameRate
or Duration). Both are measured as scanframetimes measures them
(CurrentTime before readFrame), so they agree with frametimes, and
variable-frame-rate files stay exact. frametimes still scans, only when
asked for every frame's time.

Tests: t0_t1 on a file neither cache has seen agrees with the full scan
taken after, for the RGB and greyscale videos, and a 60-frame video's
end is reached by seeking.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FxKLA8Tbf9cpvDe7fuVeKf
Comment thread +ndr/+reader/video.m Fixed
…unused

Code Analyzer flagged the argument after t0_t1 stopped passing it on to
frametimes (code scanning alert 4138).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FxKLA8Tbf9cpvDe7fuVeKf
@jesshaley
jesshaley marked this pull request as ready for review October 19, 2026 17:41
The repository's AGENTS.md asks for MATLAB arguments blocks (validation
and tab-completion); the reader had one, on readframes. Every method and
static helper now declares its inputs: the reader object, EPOCHSTREAMS as
text (char, string or a cell of char vectors), EPOCH_SELECT a positive
integer defaulting to 1, FRAMEIND a double index (range-checked as before,
so a bad index still raises ndr:reader:video:badframeind), a VideoReader
where one is taken. t0_t1 names its EPOCH_SELECT again (an ignored input
cannot be declared) and says it is unused. The two test helpers declare
theirs too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FxKLA8Tbf9cpvDe7fuVeKf
Comment thread +ndr/+reader/video.m Fixed
Comment thread +ndr/+reader/video.m Fixed
Comment thread +ndr/+reader/video.m Fixed
Comment thread +ndr/+reader/video.m Fixed
Comment thread +ndr/+reader/video.m Fixed
Comment thread +ndr/+reader/video.m Fixed
Comment thread +ndr/+reader/video.m Fixed
Comment thread +ndr/+reader/video.m Fixed
Comment thread +ndr/+reader/video.m Fixed
Comment thread +ndr/+reader/video.m Fixed
Code Analyzer (code scanning, 15 alerts on 1929281) flags an input declared
in an arguments block and not used after it. Each is the object, the
epoch's files or EPOCH_SELECT on a method the reader interface fixes the
signature of (an epoch is one file, so EPOCH_SELECT is never needed), so
each declaration carries %#ok<INUSA>, as DID-matlab's
resolveDatasetEntities does.

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

Copy link
Copy Markdown
Contributor

This raises an interesting issue. The videos would be unplayable on python if we make this mean "the MATLAB VideoReader". Maybe we want to make it an 'mp4', 'avi', and 'mov' reader generically, so that that both Matlab and python can play it? On the Matlab side, VideoReader can be the implementation, and on the python side, it can use whatever libraries are appropriate. How does that sound?

@jesshaley

jesshaley commented Oct 20, 2026 via email

Copy link
Copy Markdown
Collaborator Author

@stevevanhooser

Copy link
Copy Markdown
Contributor

OK I didn't look carefully, I just read the header. So great, sounds like it is fine, thanks! I'll merge

@stevevanhooser
stevevanhooser merged commit 6fa70d0 into main Oct 20, 2026
8 checks passed
@stevevanhooser
stevevanhooser deleted the claude/native-video-reader branch October 20, 2026 13:56
@jesshaley jesshaley changed the title Add ndr.reader.video: native video reader using only MATLAB's VideoReader Add ndr.reader.video: a reader for mp4/avi/mov video Oct 20, 2026

Copy link
Copy Markdown
Collaborator Author

Agreed, and the code already works that way: the reader type stored in the data is video (class ndr.reader.video), and nothing in a document depends on VideoReader. What tied it to MATLAB was the wording, plus one alias. Fixed in 488417d:

  • Title and help text now say the video type is defined by the file format (mp4/avi/mov). VideoReader is how MATLAB implements it; Python can use whatever reads the same containers (e.g. PyAV).
  • The videoreader alias is removed from ndr_reader_types.json. The type is now registered as video, mp4, avi and mov. Nothing in NDI used the alias.
  • Frame times are defined independently of MATLAB: each frame's presentation time, the timestamp the container stores for it, in seconds from the start of the file. In MATLAB that is VideoReader's CurrentTime read just before each frame is decoded. A Python reader that returns the same timestamps gives the same times.

No change to how frames are read; the tests are unchanged.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Correction to my comment above: this PR was merged at 794a0e8 a minute before 488417d was pushed, so the wording change and the removal of the videoreader alias are not in main. They are in #143.


Generated by Claude Code

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.

4 participants