Package limn.video

Class Videos

java.lang.Object
limn.video.Videos

public final class Videos extends Object
Video facade, over the installed 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 Details

    • installDecoder

      public static void installDecoder(VideoDecoder decoder)
      Installs decoder at 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 - if decoder is null
    • uninstallDecoder

      public static void uninstallDecoder(VideoDecoder decoder)
      Removes decoder. No-op when it was never installed, and a no-op for null rather than a failure; the asymmetry with installDecoder(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

      public static List<VideoDecoder> 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.isDecoderInstalled and SvgIcon.isRasterizerInstalled are, and deliberately not isAvailable: Sounds.isAvailable asks 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

      public static boolean canOpen(Path file)

      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 of open(java.nio.file.Path), for choosing a poster or disabling a control. A true here does not promise that opening succeeds.
      Throws:
      NullPointerException - if file is null
    • open

      public static VideoStreamSource open(Path file)
      Opens file with 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 here
      UnsupportedOperationException - if decoders are installed and none claims the input; the message names every decoder asked, in order
      NullPointerException - if file is null
    • openAsync

      public static Work<VideoStreamSource> openAsync(Path file)
      The same open as open(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 at onFailure on 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 until start(): the probe runs inside the body, not at this call, which matters because a decoder's supports is 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 onDiscarded that closes the source, so a job cancelled (or a deliverIf that 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 own onDiscarded takes 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 onProgress handler that never hears anything is the normal case here and not a sign that the open is stuck.

      Throws:
      NullPointerException - if file is null
      IllegalStateException - if no backend is running
    • warmUpAsync

      public static Work<Boolean> 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 in Async is an unstarted description, and one rule that holds across all of them is worth more than the start() 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, and canOpen is 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 onSuccess receives is isDecoderInstalled() (the same answer, and the same shape, as Sounds.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(), and onSuccess then receives whether any decoder is installed to play with
      Throws:
      IllegalStateException - if no backend is running