Video

scalacv.Video
object 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 the Ops extensions over frame.mat: those allocate their own destination and never alias the receiver, so frame.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.toList still 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 Object
trait Matchable
class Any
Self type
Video.type

Members list

Value members

Concrete methods

def frames[A](capture: VideoCapture, attemptsPerFrame: Int = ...)(f: (Iterator[BorrowedMat]) => A): A

Runs f over the capture's frames, borrowing a single decode buffer for the whole traversal.

Runs f over the capture's frames, borrowing a single decode buffer for the whole traversal.

Each element is a BorrowedMat: a liveness-checked view over that one buffer, valid from the next() that produced it until the iterator is next asked for anything — hasNext included, since asking decodes over the same memory — and spent for good when this method returns, on the exception path too. Sequential access to an already-spent view throws IllegalStateException before JNI. Do not race an access with advancing or closing the source: the check does not lock the buffer. The raw .mat result is checked only at extraction. Read the view, record it or run Ops operations through frame.mat (those allocate their own output and never alias the receiver), and frame.clone() the ones you need to keep — or use framesCopied, which does that for you.

capture is borrowed too: it is neither released nor rewound, so calling frames again resumes from wherever the previous traversal stopped. That is what makes partial consumption — _.take(10) — behave the way it reads.

Value parameters

attemptsPerFrame

how many consecutive failed read() calls end the stream. 1 — the default — is right for a file, where the first false is end-of-file. A live camera can drop a frame without the stream being over, and a small value (2–5) rides that out. It is a bound, not a retry-forever: read blocks in native code with no timeout of its own (see CaptureOptions), so an unbounded loop would turn a dead camera into a hung thread that also spins.

Attributes

Throws
CvError.NativeCall

if OpenCV fails while decoding. End-of-stream is not an error and does not throw.

Source
Video.scala
def framesCopied[A](capture: VideoCapture, attemptsPerFrame: Int = ...)(f: (Iterator[Managed[Mat]]) => A): A

As frames, but each frame is cloned into a caller-owned Managed.

As frames, but each frame is cloned into a caller-owned Managed.

The copy is what makes the frame keepable: it has its own pixel buffer, so it stays valid after the iterator moves on and after this method returns. The price is one allocation and one full-frame copy per frame, which is why it is not the default.

The clone happens as you pull, not up front — frames you never reach are never copied. Everything you do pull is yours to release after a successful callback; Using.Manager or a .use per frame is the way to not forget. Acquisition is transactional: if traversal, the callback or source cleanup throws, every still-owned clone (and its managed successors) is released before the error propagates. Successful callbacks still transfer all outstanding owners, even those omitted from the return value.

val firstThree = Video.framesCopied(c)(_.take(3).toVector)
try firstThree.foreach(m => process(m.get))
finally firstThree.foreach(_.release())

Attributes

Source
Video.scala
def info(capture: VideoCapture): CaptureInfo

What the backend claims about capture. Every field is advisory — see CaptureInfo.

What the backend claims about capture. Every field is advisory — see CaptureInfo.

Attributes

Source
Video.scala
def open(index: Int): Either[CvError, Managed[VideoCapture]]

Opens a camera by device index, with CaptureOptions.Default.

Opens a camera by device index, with CaptureOptions.Default.

Attributes

Source
Video.scala
def open(index: Int, options: CaptureOptions): Either[CvError, Managed[VideoCapture]]

Opens a camera by device index.

Opens a camera by device index.

Attributes

Returns

Left if the device does not exist, is in use, or no backend can drive it — isOpened is checked, so a capture that cannot deliver frames is an error here and never a silently empty stream. The returned Managed is caller-owned; prefer .use.

Source
Video.scala
def open(source: String): Either[CvError, Managed[VideoCapture]]

Opens a file, URL or device path, with CaptureOptions.Default.

Opens a file, URL or device path, with CaptureOptions.Default.

Attributes

Source
Video.scala
def open(source: String, options: CaptureOptions): Either[CvError, Managed[VideoCapture]]

Opens a file, URL or device path.

Opens a file, URL or device path.

Value parameters

source

whatever the backend understands: a filesystem path, an rtsp:// or http:// URL, a frame_%04d.png sequence pattern, a GStreamer pipeline. OpenCV resolves it itself and knows nothing about classpath resources.

Attributes

Returns

Left if the source cannot be opened — a missing file, an unreadable container, no backend for the protocol. isOpened is checked, so this is never a silently empty stream. The returned Managed is caller-owned; prefer .use.

Source
Video.scala