Video
Video capture: opening a source, and walking its frames without leaking one per iteration.
==Why there is no frame LazyList==
The obvious shape for "the frames of a video" is a lazy sequence, and it is wrong here. LazyList memoises: once evaluated, a cell holds its head forever so that a second traversal is cheap. Applied to frames, that means every Mat the list has ever produced stays reachable — so either nothing is ever released (an unbounded native leak; a 1080p BGR frame is ~6 MB, so a minute at 30 fps is over 10 GB) or frames are released as they are consumed and the list is a field of dangling handles that the next traversal hands back as empty Mats. There is no version of the API where memoisation and per-frame release are both correct. Same argument, verbatim, for Stream, and for any Iterator combinator that retains what it has seen.
So the frame source here is an Iterator that owns exactly one Mat and decodes into it in place. It is created inside a scope, it is released when that scope ends, and it holds one frame's worth of native memory no matter how long the video is.
==The borrowing contract, enforced==
This is the one place in scalacv where a frame you are handed is not yours, and it is the exact opposite of the contract in Ops.scala. Each element is a BorrowedMat — a liveness-checked view over the iterator's single decode buffer, spent the moment the iterator advances or the frames block returns. Every access, including the BorrowedMat.mat escape hatch, throws IllegalStateException once the view is spent, so sequential retention of the view fails loudly. The raw Mat returned by .mat is checked only at extraction and remains unsafe to retain. Access must not overlap a pull or close on another thread/fiber: the liveness check is not a lock. Clone inside the loop before handing pixels to concurrent work.
- Do read the view, record it through
frame.mat, and run theOpsextensions overframe.mat: those allocate their own destination and never alias the receiver, soframe.mat.cvtColor(...)inside the loop is correct and yields a Mat you own. - Need to keep a frame?
frame.clone()inside the loop — the clone is caller-owned — or framesCopied, which does that per frame and hands you a Managed. it.toListstill compiles, but every element in the list is spent by the time you look at it: N spent views, not N frames.
Video.open("clip.mp4").map { capture =>
capture.use { c =>
Video.frames(c) { frames =>
frames.map(f => f.mat.cvtColor(ColorConversion.BgrToGray).use(_.findContours().size)).sum
}
}
}
==Exception mode==
VideoCapture.setExceptionMode(true) turns a silent false into a CvException carrying OpenCV's own message, and open uses it: a missing file becomes CvError.NativeCall quoting the path instead of a bare "it did not open". frames deliberately turns it off for the duration of the loop, because OpenCV reports end-of-file through the identical exception it uses for a broken stream — cap.cpp:533 error: (-2:Unspecified error) in function 'grab', measured on a clean five-frame file. With exception mode on there is no way to tell "the video ended" from "the camera was unplugged", so the loop would have to treat every real failure as a normal end. Off, read returning false ends the stream and a genuine decode error still surfaces as CvError.NativeCall.
Attributes
- Source
- Video.scala
- Graph
-
- Supertypes
-
class Objecttrait Matchableclass Any
- Self type
-
Video.type