Package limn.video

Class MediaPlayer

java.lang.Object
limn.video.MediaPlayer
All Implemented Interfaces:
AutoCloseable

public final class MediaPlayer extends Object implements AutoCloseable
Plays a video stream: a decode thread ahead of a small ring of pictures, a VideoClock timing them, and an optional audio track that becomes the clock's master. What a VideoStreamSource lacks to be watchable (somewhere to decode that is not the thread drawing, somewhere to keep a picture that is not yet due, and something to be in time with) is exactly this.

 VideoStreamSource stream = Videos.open(file);       // the application's decoder, and its stream
 MediaPlayer player = new MediaPlayer(stream);
 player.setAudio(track, PlayOptions.DEFAULTS.withBus(AudioBus.MUSIC));
 player.start();
 // ... and when the application is finished:
 player.close();
 stream.close();                                     // in this order, and only in this order
 

Who closes what

The video stream is the caller's, opened by the caller and closed by the caller. This player never closes it. That is the same rule a view follows, for the same reason: a stream may outlive a player, be shown in two places, or be handed to a second player afterwards. What the player does promise is that close() returns only once its decode thread has stopped touching the stream, so closing it after that is safe and closing it before is a decode against a torn down decoder.

The audio track is this player's from setAudio(limn.sound.AudioStreamSource, limn.sound.PlayOptions) onwards, and is closed exactly once: by the audio engine when playback ends or is stopped, or by close() when playback never started. There is no path on which the caller should close it and no path on which it leaks. That asymmetry with video is not a preference: handing a source to an audio engine transfers it by contract, including on every failure path, so a player that also closed it would be closing it twice.

Threads

One decode thread per player, started at start() and joined at close(). A shared pool was rejected: VideoStreamSource.readFrame() may block for a whole decode and promises no bound, so a pool of any size lets one stalled stream stop the others, and a page of thumbnails that never appear is a failure nobody can see the cause of. The honest cost is stated instead: twelve videos on a page are twelve threads, and the expensive part of that is the twelve concurrent decodes, not the twelve threads.

Pictures

The player holds at most ringCapacity() decoded pictures. It allocates none of them: the pool belongs to the stream that produced them, so the ring holds borrowed references and every one of them is released exactly once: when it is shown and handed on, when the clock drops it, or when the player is stopped or closed.

Full: the decode thread stops reading and waits. It does not block forever on a condition it might miss; it waits with a bound, so a lost signal costs latency rather than a player that never plays again, and every consumed picture signals it awake. Holding pictures the consumer has not asked for would also starve the stream's own pool, which needs a free slot to decode into: the effective read-ahead is therefore the smaller of this ring and the stream's spare slots, and a stream reporting VideoStreamSource.Read.PENDING because everything is held is answered the same way as one that is simply not ready.

Empty: takePicture() returns null and the caller keeps showing what it has. The screen never goes blank for want of a picture, and each dry spell is counted once by underruns().

Because the ring can name the picture behind the candidate, this player can drop, unlike a view holding a single picture. A player that has fallen behind skips pictures whose moment has already passed rather than showing them late.

Sound, and being in time with it

With an audio track that sounds, its position becomes the clock's master through AudioMasterClock and the pictures follow it. With no track at all (which is every stream a pure-Java decoder produces, so it is the common case and not the corner), no master is installed and the clock free-runs on the wall clock at the stream's own rate.

A track that does not sound is treated as no master rather than as an error: a machine with no audio device yields a handle whose position is a constant zero, which this player detects at admission and declines to follow, so the video plays at the right rate rather than not at all.

A master is dropped the moment the pictures can no longer follow it, and the wall clock carries on from the position already reached, without a step. Two things cause it: a handle that stops advancing (a device that went away, a track that ended before the pictures did), which the clock's own stall detection catches; and a handle that moves somewhere the pictures are not, which in practice means a looping track wrapping to its start. Neither is recoverable by holding pictures and waiting, which is what a clock left following them would do.

Seeking

seek(long, VideoStreamSource.SeekMode) moves the pictures, the soundtrack and the timeline together, and is a request rather than a completed operation: the ring is released and the clock is moved before it returns, and the stream is repositioned by the decode thread, which may be inside a whole decode at that moment. It is cheap enough to drive from a control being dragged, provided the caller drags in VideoStreamSource.SeekMode.KEYFRAME and lands in VideoStreamSource.SeekMode.EXACT.

See Also:
  • Field Details

    • DEFAULT_RING_CAPACITY

      public static final int DEFAULT_RING_CAPACITY
      Pictures held ahead of the one being shown, when nothing says otherwise.
      See Also:
  • Constructor Details

    • MediaPlayer

      public MediaPlayer(VideoStreamSource video)
      Parameters:
      video - the stream to play, which stays the caller's to close, after this player is closed and not before
      Throws:
      NullPointerException - if video is null
  • Method Details

    • setAudio

      public MediaPlayer setAudio(AudioStreamSource newAudio, PlayOptions options)
      Gives this player the video's audio track, which it owns from here on: the audio engine closes it when playback ends or is stopped, and close() closes it if playback never started. The caller must not close it and must not stream it itself.

      Null removes a track that has not been started yet, closing it.

      Parameters:
      options - gain, bus and priority for the track. Looping is this player's to decide, so PlayOptions.loop() is overridden by setLooping(boolean).
      Throws:
      IllegalStateException - if this player has already been started
    • setRingCapacity

      public MediaPlayer setRingCapacity(int pictures)
      How many decoded pictures may be held ahead of the one being shown, at least 1. Larger rides out a slower decoder and a busier machine; smaller costs the stream's pool fewer held slots and shortens the pause a stop has to drain. Beyond a handful it buys nothing a stream's own pool can supply.
      Throws:
      IllegalArgumentException - if pictures is below 1
      IllegalStateException - if this player has already been started
    • ringCapacity

      public int ringCapacity()
      Returns:
      how many decoded pictures may be held ahead of the one being shown
    • setClock

      public MediaPlayer setClock(VideoClock newClock)
      Replaces the clock that times the pictures, for a test driving a whole stream with no real time passing, or a host with a timeline of its own. A master installed on it is replaced when this player starts an audio track that sounds.
      Throws:
      IllegalStateException - if this player has already been started
    • clock

      public VideoClock clock()
      The clock timing the pictures. Touch it only from the UI thread: it is not thread-safe, and this player reads it from there on every takePicture().
    • setOwnsDecodeThread

      public MediaPlayer setOwnsDecodeThread(boolean owns)
      Whether this player starts a decode thread of its own (the default) or leaves decodeStep() to the caller, for a host that owns its threads, or a test that wants decoding to happen exactly when it says so.
      Throws:
      IllegalStateException - if this player has already been started
    • setLooping

      public MediaPlayer setLooping(boolean newLooping)
      Whether reaching the end rewinds and plays again (default false). Ignored by a stream that cannot be rewound. May be changed while playing.

      With an audio track, the master governs the first pass only. At the first loop the pictures' timeline restarts and the track's does not (or does, at its own length, which is not the video's), and one clock cannot be on two timelines at once. The player therefore drops the master there and paces the remaining passes on the wall clock, which keeps them at the right rate instead of holding until the track catches up or racing to catch it. Aligning a loop across both tracks needs a seek, which this does not have.

    • isLooping

      public boolean isLooping()
      Returns:
      whether the end rewinds rather than stops
    • start

      public void start()
      Starts decoding, starts the audio track if there is one, and begins timing. Resumes instead when paused, and does nothing at all when already playing, ended or failed; restart() is what rewinds one of those.

      Starting the audio track is where its ownership passes to the engine, so it happens once and only once however many times this is called.

      Throws:
      IllegalStateException - if this player is closed
    • pause

      public void pause()
      Freezes the picture and the sound, keeping both positions. Decoding continues until the ring is full, so resuming shows the next picture immediately rather than after a decode.

      The clock is told, which is not optional: a paused audio track reports a frozen position, which is indistinguishable from a device that has died, and a clock not told would hand the timeline to the wall clock and run the video straight through the pause.

      Does nothing unless playing.

    • resume

      public void resume()
      Resumes from where pause() froze it. Does nothing unless paused.
    • stop

      public void stop()
      Ends this playback: the decode thread is stopped and joined, every held picture is released, the clock is reset and the sound is stopped. The video stream is left open, wherever it had reached, and is not rewound.

      Terminal for the audio track. Stopping a stream hands it to the engine to close, and a track cannot be re-opened by something that never opened it, so a player started again after this plays silently. pause() is the resumable one. Idempotent, and harmless on a closed player.

    • restart

      public void restart()
      Rewinds to the first picture and plays from there: stop(), then VideoStreamSource.reset(), then start(), which is also how it clears a failed state. Silent afterwards, for the reason stop() gives: the audio track went with the stop, and a video rewound under a track that was not would be showing the first pictures against the wrong sound.
      Throws:
      UnsupportedOperationException - if the stream cannot be rewound, which VideoStreamSource.canReset() answers in advance
      IllegalStateException - if this player is closed
    • seek

      public void seek(long micros, VideoStreamSource.SeekMode mode)
      Moves to micros and carries on from there: the pictures, the soundtrack and the timeline together.

      Returns as soon as the request is placed, which is not the same as the picture having changed. What has happened by the time it returns: every picture this player was holding has been released, the clock has been moved to micros, and the soundtrack has thrown away what it had queued on the device and reports the new position. What has not happened is the decode: the stream is repositioned by this player's decode thread, at the top of its next pass, which is the only thread allowed to touch it and may be inside a whole decode right now. takePicture() keeps answering null until the first picture from there arrives.

      positionMicros() reports micros in between, exactly, rather than creeping forward while the pictures are being found. A transport control reading it therefore shows where it was told to go and not where the buffering has got to.

      While paused this hands over exactly one picture and then holds again, because a viewer dragging a paused video is asking to see where they have landed. The pause is not lifted.

      Clears an ended state (seeking backwards out of the end is how a viewer replays a part) and clears a failure, since a decode that threw at one position says nothing about another. Looping is unaffected, and a seek is not a loop: the pass the pictures are on does not change, so a soundtrack that is still mastering keeps mastering.

      The audio track is repositioned only if it can be. A soundtrack from the same container as the pictures moves with them; a track from somewhere else, or one on an engine that cannot discard what it has queued, keeps playing where it was, and because that leaves it on a timeline the pictures are not on, this player drops it as its master rather than following it somewhere the pictures will never be. canSeekAudio() answers in advance.

      Parameters:
      micros - where to move to, in microseconds on the pictures' own timeline; not negative
      mode - how close to land, and therefore what this costs. VideoStreamSource.SeekMode.KEYFRAME while a control is being dragged and VideoStreamSource.SeekMode.EXACT when it is let go is what makes a scrub bar cheap while it moves and right when it stops.
      Throws:
      UnsupportedOperationException - if the stream cannot be seeked, which VideoStreamSource.canSeek() answers in advance
      IllegalArgumentException - if micros is negative
      IllegalStateException - if this player is closed
    • seek

      public void seek(long micros)
    • canSeek

      public boolean canSeek()
      Returns:
      whether seek(long, VideoStreamSource.SeekMode) works: the stream's answer, which is the one that decides whether a transport control is offered at all
    • canSeekAudio

      public boolean canSeekAudio()
      Returns:
      whether a seek also moves the soundtrack. False with no track, with a track no device would take, and with one the engine cannot discard queued audio for, in which case a seek still moves the pictures and stops timing them by a soundtrack that is now somewhere else. UI thread, because it reads the playback handle.
    • close

      public void close()
      Stops everything and releases every picture this player is holding. Blocks until the decode thread has finished the read it is in, which is the whole point of it: only then can the caller close the video stream without tearing a decoder down underneath a decode. A stream that never returns from a read therefore hangs this call, and that is a defect in the stream rather than something to paper over with a timeout that would make the promise a guess.

      The decode thread is not interrupted. VideoStreamSource promises nothing about interruption, and a decoder woken out of a read could leave its input at a position it cannot describe.

      The audio track is closed here if it never reached the engine; if it did, the engine closed it. The video stream is not closed. Idempotent.

      Specified by:
      close in interface AutoCloseable
    • takePicture

      public VideoFrame takePicture()
      The picture whose moment has come, or null to keep showing the current one. The caller owns what it is given and must release it exactly once.

      Returns null while paused, before the first start, after the end, after a failure, and whenever nothing in the ring is due yet. Pictures whose moment has already passed are dropped and released here rather than handed over late, which a caller holding a single picture could not do for itself.

      The one exception to the pause is a seek: a player seeked while paused hands over the first picture from the new position and then holds again, so that a paused scrub shows where it landed.

      UI thread. Cheap enough for every frame: it reads the master at most once and allocates nothing.

    • decodeStep

      public MediaPlayer.Step decodeStep()
      One pass of the decode loop: at most one picture read from the stream and handed to the ring. Never blocks on the ring and never spins: a full ring and a stream with nothing ready are both MediaPlayer.Step.IDLE, which the caller answers by waiting a moment.

      This player's own decode thread calls it in a loop. A host that owns its threads calls it itself, from anywhere that is not drawing: a read may take a whole decode.

      Returns:
      what this pass did
    • video

      public VideoStreamSource video()
      Returns:
      the stream being played, never null: for a consumer that needs its declared size or frame rate before a picture exists, which every metadata accessor answers from any thread. Still the caller's to close, and only after this player is closed.
    • state

      public MediaPlayer.State state()
      Returns:
      where this player is in its life; readable from any thread
    • isEnded

      public boolean isEnded()
      Returns:
      whether the stream ended and every decoded picture has been handed out. The last one stays on screen; nothing more is decoded or timed.
    • failure

      public RuntimeException failure()
      Returns:
      the exception a decode threw, or null. The player stops reading the stream and hands out nothing more; restart() or stop() clears it.
    • hasAudio

      public boolean hasAudio()
      Returns:
      whether an audio track is sounding, or is waiting for start() to sound
    • isFollowingAudio

      public boolean isFollowingAudio()
      Returns:
      whether the pictures are currently timed by the audio track rather than by the wall clock. False before start(), with no track, with a track no device would take, and after a track has stopped advancing or moved off the pictures' timeline, which is what a status line needs to say why a video is pacing the way it is
    • audio

      public Playback audio()
      Returns:
      the handle to the sounding audio track, or Playback.NONE when there is none: because there was no track, because no device would take it, or because playback has been stopped
    • positionMicros

      public long positionMicros()
      Returns:
      the media position in microseconds: the audio track's while it is mastering, and the wall-clock timeline's otherwise
    • decodedFrames

      public long decodedFrames()
      Returns:
      decoded pictures handed to the ring since construction, dropped ones included
    • underruns

      public long underruns()
      Returns:
      how many times a picture was asked for and the ring was empty while playing, counted once per dry spell rather than once per ask, so it reads as "the decoder fell behind this many times" rather than as a frame count
    • bufferedPictures

      public int bufferedPictures()
      Returns:
      decoded pictures held right now, at most ringCapacity()
    • toString

      public String toString()
      Overrides:
      toString in class Object