Class Videos
VideoDecoders.
The application installs them, not the backend, and that is the one thing to know
before anything here works: starting a backend supplies an image decoder and an audio engine,
and supplies no video decoder at all. Nothing can be opened until something calls
installDecoder(limn.video.VideoDecoder). That is deliberate rather than an omission: an image decoder is a
platform capability every backend has, while a video decoder is a dependency with a size and a
licence, so which ones a process carries is the application's decision.
Videos.installDecoder(new Y4mDecoder()); // pure Java, no native
Videos.installDecoder(new FfmpegVideoDecoder()); // if that module is on the classpath
Unlike the image and audio facades this holds an ordered list: decoders differ by input rather than by platform, so several coexist and each declines what it cannot take.
try (VideoStreamSource source = Videos.open(Path.of("clip.y4m"))) {
while (source.readFrame() == VideoStreamSource.Read.FRAME) {
VideoFrame frame = source.frame();
// ... use it ...
frame.release();
}
}
Opening throws rather than degrading to a silent no-op, which is the opposite of the audio
facade and deliberately so: a feedback sound that does not play is invisible, whereas a video
that does not open is a blank rectangle where content was asked for, and a caller that is not
told has no way to show a poster or an error instead. There is also nothing coherent to return:
a stand-in source would have to invent a width, a height and a layout. Ask first with
isDecoderInstalled() and canOpen(java.nio.file.Path) rather than catching.
The registry is a concurrent list and asking it something (isDecoderInstalled(),
installedDecoders(), canOpen(java.nio.file.Path)) is safe from any thread, including a decode
thread. Opening is where the thread starts to matter.
open(java.nio.file.Path) blocks for as long as the decoder that claims the input takes, which for
a real container is a header read, a probe of every stream in it, an index, a decoder, and on the
first call a native library: far longer than a frame, and a visible freeze when the caller is the
UI thread. openAsync(java.nio.file.Path) does the same work on the worker pool and hands the source back on
the UI thread, closing it for you if the request was withdrawn before it landed. And because
canOpen(java.nio.file.Path) has to stay synchronous (a control asking whether to enable itself cannot wait),
warmUpAsync() is how an application pays a decoder's first-call cost somewhere other
than the first probe.
The asynchronous forms need a running backend, since their callbacks land on the UI thread; everything else here works in a process that never started one.
The registry is process-wide and outlives any one test class. A test that installs a decoder
removes it again in a cleanup block; uninstallAllDecoders() clears the list for a test
that would rather start from empty.
-
Method Summary
Modifier and TypeMethodDescriptionstatic booleanSynchronous and cheap by contract, on any thread: each decoder's claim is an extension comparison and at most a few bytes read, because this is what a control asks before it decides whether to enable itself and it has to be answerable inside a frame.static voidinstallDecoder(VideoDecoder decoder) Installsdecoderat the end of the probe order.static List<VideoDecoder> static booleanA lock-free peek at the registry and nothing more: no probe, no library, no device.static VideoStreamSourceOpensfilewith the first installed decoder that claims it.static Work<VideoStreamSource> The same open asopen(java.nio.file.Path), on the worker pool, delivered on the UI thread, and returned unstarted, so the caller attaches its handlers first:static voidRemoves every decoder: backend shutdown, and test cleanup.static voiduninstallDecoder(VideoDecoder decoder) Removesdecoder.Prepares every installed decoder on the worker pool (a native library linked, a payload extracted) and is returned unstarted.
-
Method Details
-
installDecoder
Installsdecoderat the end of the probe order. Decoders are asked in the order they were installed, so an application that wants one to win installs it first, or uninstalls what it means to override. Installing an already installed decoder is a no-op that leaves the order untouched, so running startup twice cannot reshuffle priorities.- Throws:
NullPointerException- ifdecoderis null
-
uninstallDecoder
Removesdecoder. No-op when it was never installed, and a no-op for null rather than a failure; the asymmetry withinstallDecoder(limn.video.VideoDecoder)is deliberate, so a cleanup block can uninstall whatever it may or may not have installed without a null check of its own. -
uninstallAllDecoders
public static void uninstallAllDecoders()Removes every decoder: backend shutdown, and test cleanup. -
installedDecoders
- Returns:
- an immutable snapshot of the installed decoders, in the order they are asked
-
isDecoderInstalled
public static boolean isDecoderInstalled()A lock-free peek at the registry and nothing more: no probe, no library, no device.Named for what it answers, the way
Images.isDecoderInstalledandSvgIcon.isRasterizerInstalledare, and deliberately notisAvailable:Sounds.isAvailableasks a different and far more expensive question (engine installed and device answering, whose first call may open the audio hardware), and one name spanning a field read and a device open is a cost a caller cannot infer.- Returns:
- whether the application has installed any decoder at all; a running backend does not imply one, because a backend supplies none
-
canOpen
Synchronous and cheap by contract, on any thread: each decoder's claim is an extension comparison and at most a few bytes read, because this is what a control asks before it decides whether to enable itself and it has to be answerable inside a frame. The one cost it cannot promise away is a decoder's own first-call preparation: a native library links once, and whoever asks first pays for it.
warmUpAsync()is how that is moved off this call.- Returns:
- whether some installed decoder claims
file: the non-throwing form ofopen(java.nio.file.Path), for choosing a poster or disabling a control. A true here does not promise that opening succeeds. - Throws:
NullPointerException- iffileis null
-
open
Opensfilewith the first installed decoder that claims it. The caller owns the returned source and closes it. If that decoder then fails, the failure propagates and no later decoder is tried: the one that accepted the input is the one that knows what is wrong with it, and replacing that with a generic message would be the worst diagnostic available.Runs on the calling thread and takes as long as that decoder takes: for a container, far longer than a frame, and longer again on a cold or network volume or on the call that first links a native library. On the UI thread that is a freeze the length of the open, so use
openAsync(java.nio.file.Path)anywhere but setup code.- Throws:
IllegalStateException- if no decoder is installed at all, or if the decoder that claimed the input returned nothing without having been cancelled, which it cannot have been hereUnsupportedOperationException- if decoders are installed and none claims the input; the message names every decoder asked, in orderNullPointerException- iffileis null
-
openAsync
The same open asopen(java.nio.file.Path), on the worker pool, delivered on the UI thread, and returned unstarted, so the caller attaches its handlers first:open = Videos.openAsync(file) .onSuccess(source -> player.play(source)) .onFailure(error -> status.setText(error.getMessage())) .deliverIf(this::isAttached) .start();Both of
open(java.nio.file.Path)'s failures arrive atonFailureon the UI thread instead of being thrown here (no decoder installed at all, and decoders installed of which none claims the input, the second still naming every decoder asked in order), as does whatever the decoder that accepted the input threw. Nothing at all happens untilstart(): the probe runs inside the body, not at this call, which matters because a decoder'ssupportsis cheap only after that decoder has been prepared, and the first one may link a native library.A withdrawn open closes what it opened. The returned description already carries an
onDiscardedthat closes the source, so a job cancelled (or adeliverIfthat answers false) while the container was being opened does not leak it: the source is closed on a worker instead of being delivered. A caller replacing its ownonDiscardedtakes that job over. Cancelling does not stop the open, because nothing interrupts a decoder inside a native read; it stops the delivery, and disposes of the result.Progress is whatever the chosen decoder chooses to report, which for most opens is nothing at all; an
onProgresshandler that never hears anything is the normal case here and not a sign that the open is stuck.- Throws:
NullPointerException- iffileis nullIllegalStateException- if no backend is running
-
warmUpAsync
Prepares every installed decoder on the worker pool (a native library linked, a payload extracted) and is returned unstarted. An application that wants it starts it once, after installing its decoders:Videos.installDecoder(new SomeDecoder()); Videos.warmUpAsync().start();Like
openAsync(java.nio.file.Path), this is a description and not a running job: calling it and dropping the result warms nothing at all. It keeps that shape rather than starting itself, even though nothing here waits for a result, because every form in this toolkit whose name ends inAsyncis an unstarted description, and one rule that holds across all of them is worth more than thestart()it would save; a facade that started itself would be the exception a reader has to remember, at the one call site where forgetting is silent.It buys nothing but the thread the cost is paid on, and that is the point: the first
canOpen(java.nio.file.Path)would otherwise pay it, andcanOpenis synchronous by contract because a control deciding whether to enable itself cannot wait for a frame. Skipping this is not an error and changes no answer this class gives; it changes only when, and on which thread, a decoder's one-off preparation happens.A decoder that fails to prepare itself is left in exactly the state it would have been in had nobody warmed it, and the next decoder is warmed anyway; that is why this delivers no failure of its own. Progress runs from 0 to 1 across the installed decoders, in probe order, for an application that shows a splash. Only the decoders installed when the body starts are warmed; one installed afterwards warms itself at its first call, or takes another
warmUpAsync.What
onSuccessreceives isisDecoderInstalled()(the same answer, and the same shape, asSounds.warmUpAsync), so a control can be gated off either of them the same way:Videos.warmUpAsync().onSuccess(playable::setEnabled).start(). A cancelled warm-up delivers nothing at all, as every cancelled job does.- Returns:
- the unstarted work; nothing is prepared until
start(), andonSuccessthen receives whether any decoder is installed to play with - Throws:
IllegalStateException- if no backend is running
-