Class MediaPlayer
- All Implemented Interfaces:
AutoCloseable
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
- The UI thread: every configuration method,
start(),pause(),resume(),stop(),restart(),seek(long, VideoStreamSource.SeekMode),close()andtakePicture(). Asserted, because the clock is not thread-safe and a picture handed to two threads is released twice. - The decode thread:
decodeStep()and nothing else. This player starts one of its own unless told not to; the stream is read from there and never from a thread that is drawing. - Any thread:
state(),isEnded(),failure(),decodedFrames()andbufferedPictures(). They are reads of volatile or locked state, for a status line or a log.positionMicros(),hasAudio(),audio()andunderruns()are the UI thread's, because they read the clock or state only that thread writes.
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:
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic enumWhere a player is in its life.static enumWhat one pass of the decode loop did: what a host driving it needs to know to pace itself. -
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final intPictures held ahead of the one being shown, when nothing says otherwise. -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionaudio()intbooleancanSeek()booleanclock()The clock timing the pictures.voidclose()Stops everything and releases every picture this player is holding.longOne pass of the decode loop: at most one picture read from the stream and handed to the ring.failure()booleanhasAudio()booleanisEnded()booleanbooleanvoidpause()Freezes the picture and the sound, keeping both positions.longvoidrestart()Rewinds to the first picture and plays from there:stop(), thenVideoStreamSource.reset(), thenstart(), which is also how it clears a failed state.voidresume()Resumes from wherepause()froze it.intvoidseek(long micros) Asseek(long, VideoStreamSource.SeekMode)landing exactly.voidseek(long micros, VideoStreamSource.SeekMode mode) Moves tomicrosand carries on from there: the pictures, the soundtrack and the timeline together.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, andclose()closes it if playback never started.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.setLooping(boolean newLooping) Whether reaching the end rewinds and plays again (default false).setOwnsDecodeThread(boolean owns) Whether this player starts a decode thread of its own (the default) or leavesdecodeStep()to the caller, for a host that owns its threads, or a test that wants decoding to happen exactly when it says so.setRingCapacity(int pictures) How many decoded pictures may be held ahead of the one being shown, at least 1.voidstart()Starts decoding, starts the audio track if there is one, and begins timing.state()voidstop()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 picture whose moment has come, or null to keep showing the current one.toString()longvideo()
-
Field Details
-
DEFAULT_RING_CAPACITY
public static final int DEFAULT_RING_CAPACITYPictures held ahead of the one being shown, when nothing says otherwise.- See Also:
-
-
Constructor Details
-
MediaPlayer
- Parameters:
video- the stream to play, which stays the caller's to close, after this player is closed and not before- Throws:
NullPointerException- ifvideois null
-
-
Method Details
-
setAudio
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, andclose()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, soPlayOptions.loop()is overridden bysetLooping(boolean).- Throws:
IllegalStateException- if this player has already been started
-
setRingCapacity
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- ifpicturesis below 1IllegalStateException- 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
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
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 everytakePicture(). -
setOwnsDecodeThread
Whether this player starts a decode thread of its own (the default) or leavesdecodeStep()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
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 wherepause()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(), thenVideoStreamSource.reset(), thenstart(), which is also how it clears a failed state. Silent afterwards, for the reasonstop()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, whichVideoStreamSource.canReset()answers in advanceIllegalStateException- if this player is closed
-
seek
Moves tomicrosand 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()reportsmicrosin 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 negativemode- how close to land, and therefore what this costs.VideoStreamSource.SeekMode.KEYFRAMEwhile a control is being dragged andVideoStreamSource.SeekMode.EXACTwhen 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, whichVideoStreamSource.canSeek()answers in advanceIllegalArgumentException- ifmicrosis negativeIllegalStateException- if this player is closed
-
seek
public void seek(long micros) Asseek(long, VideoStreamSource.SeekMode)landing exactly. -
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.
VideoStreamSourcepromises 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:
closein interfaceAutoCloseable
-
takePicture
The picture whose moment has come, or null to keep showing the current one. The caller owns what it is given and mustreleaseit 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
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 bothMediaPlayer.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
- 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
- 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
-
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
- Returns:
- the handle to the sounding audio track, or
Playback.NONEwhen 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
-