Class VideoClock
Pure logic: no threads, no input, and no clock of its own. The wall clock is supplied, so a test drives hours of playback with no real time passing and a player drives it from an audio position instead.
Timeline. Presentation times and the master's position must be on the same timeline. A producer emitting times on a container's native base paired with a master reporting zero-based positions is out by the whole base and no threshold rescues that. With no master any base works, because the timeline is anchored to the first picture this clock is asked about.
Presentation times must be non-decreasing across successive calls. That is not enforced: a producer emitting them out of order will have them shown out of order. Reordering here would hide a producer's bug inside the timing policy, which is the last place anyone would look for it.
Memoryless. Each call is judged only on the position reported at that instant. This does not smooth a noisy master; a master whose reported position jitters by more than the earliness window produces a visibly irregular cadence. Smooth the master, not the policy.
Not thread-safe. Call it from one thread. It touches no shared state and needs no runtime, which is what lets it be tested with nothing installed.
Allocation-free after construction. No field is a collection or a boxed value, every decision is an interned constant, and deciding creates nothing, so a 60-per-second loop produces no garbage at all.
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic enumWhat the caller must do with the picture it just asked about.static interfaceA media position source on the same timeline as the pictures' presentation times. -
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final longHalf of a 60 Hz refresh interval, in microseconds: the half-width of the window in which a picture counts as due.static final longHow far a master's position may move away from the wall time that elapsed (in either direction, since a scrub backwards is as much a seek as one forwards) before the move is read as a seek rather than an irregularity.static final longMicroseconds of wall time a master may report an unchanged position before it is declared stalled and the wall clock takes over.static final longPassed as the successor's presentation time when the caller has nothing queued behind the candidate. -
Constructor Summary
ConstructorsConstructorDescriptionDrives from the system's monotonic clock until a master is installed.VideoClock(LongSupplier nanoClock) -
Method Summary
Modifier and TypeMethodDescriptiondecide(long ptsMicros) As deciding with no successor named, which therefore never returns a drop.decide(long ptsMicros, long nextPtsMicros) longlonglongbooleanlonglonglongvoidreset()Forgets the timeline, so the next picture offered is shown and re-anchors it.voidseekTo(long positionMicros) Declares that the timeline has been moved topositionMicros(the master's position included) so that the move is not mistaken for a master that has run away.voidsetMaster(VideoClock.MasterClock newMaster) Installs the master position source, or null to free-run on the wall clock.voidsetPaused(boolean newPaused) Freezes the timeline.long
-
Field Details
-
NO_PTS
public static final long NO_PTSPassed as the successor's presentation time when the caller has nothing queued behind the candidate. Equal to the unknown-timestamp sentinel a frame reports, so a successor whose own time is unknown correctly licenses no drop.- See Also:
-
EARLY_MICROS
public static final long EARLY_MICROSHalf of a 60 Hz refresh interval, in microseconds: the half-width of the window in which a picture counts as due. A picture arriving up to this early is shown now rather than a tick late; at higher refresh rates that is at most one refresh early, which is not visible. It is also well inside the skew at which a viewer can detect audio and video disagreeing, so no value within a few milliseconds of it would look different.- See Also:
-
MASTER_STALL_MICROS
public static final long MASTER_STALL_MICROSMicroseconds of wall time a master may report an unchanged position before it is declared stalled and the wall clock takes over. A live audio position advances by at least one sample between reads, and two display ticks are hundreds of samples apart, so an unchanged reading means nothing advanced. Long enough that no healthy device is declared dead by a scheduling hiccup, short enough that a viewer sees at most a brief hitch.- See Also:
-
MASTER_JUMP_MICROS
public static final long MASTER_JUMP_MICROSHow far a master's position may move away from the wall time that elapsed (in either direction, since a scrub backwards is as much a seek as one forwards) before the move is read as a seek rather than an irregularity. Above audio buffering granularity and above any plausible stall of the calling loop; below the smallest step a transport control makes.- See Also:
-
-
Constructor Details
-
VideoClock
public VideoClock()Drives from the system's monotonic clock until a master is installed. -
VideoClock
- Parameters:
nanoClock- a monotonic nanosecond source, read exactly once per decision: the seam that lets a test run a whole stream instantly by handing over a counter- Throws:
NullPointerException- ifnanoClockis null
-
-
Method Details
-
setMaster
Installs the master position source, or null to free-run on the wall clock. Reads the new master's position once, here, which arms the stall timer immediately: a master that is inert on arrival (a handle from a machine with no audio device, whose position is a constant zero) is then detected while buffering rather than partway into the picture. Removing a master hands the timeline to the wall clock from the position reached so far, without a step.While paused the wall references are dated at the instant the pause began rather than now, so resuming moves them forward by the paused span exactly once. Dating them at the live clock instead would have the resume add a span that had already elapsed before the master arrived, and the first reading afterwards would be scored against it and counted as a seek.
-
setPaused
public void setPaused(boolean newPaused) Freezes the timeline. Mandatory around a pause: a paused master reports a frozen position, which is indistinguishable from a stalled device, and without this the wall clock would take over and run the video straight through the pause. While paused every decision is a hold, the master is not read at all, and stall detection is suspended; on resuming, every wall reference moves forward by the paused span, so the pause costs no media time.Reads the wall clock once on each transition, and the master once when a pause begins. Setting the state it already has does nothing at all.
-
reset
public void reset()Forgets the timeline, so the next picture offered is shown and re-anchors it. For a change of stream, or a loop, after the caller has flushed its own queue. The cumulative counters survive: starting over is not a reason to lose a session's history. -
seekTo
public void seekTo(long positionMicros) Declares that the timeline has been moved topositionMicros(the master's position included) so that the move is not mistaken for a master that has run away.Without this a seek is indistinguishable from the thing this clock is built to survive: a position that suddenly differs from the wall time elapsed by more than
MASTER_JUMP_MICROSis counted as a jump, and a player watchingjumpCount()drops its master on the strength of it. Every scrub would then cost the video its audio, once and permanently.The position is taken as fact rather than polled. The master is deliberately not read here: an audio engine repositions asynchronously, so reading it at the instant of the request captures the position being left, and the move to the new one would be scored as a jump a few milliseconds later, the exact failure this exists to prevent. The caller is therefore responsible for having moved the master there, or for having moved it at all; a caller that seeks the pictures and leaves a soundtrack where it was has told this clock something untrue and will see it as a jump on the next decision.
Afterwards the next picture offered is shown whatever its timestamp, which re-anchors the timeline on it, and
positionMicros()reportspositionMicrosexactly until then; it does not free-run forward while the pictures are still being found. Works while paused, where it moves the frozen position; the pause is not lifted. The cumulative counters survive, andjumpCount()is deliberately not incremented, which is what a player distinguishes a seek from a runaway master by. -
decide
- Parameters:
ptsMicros- presentation time of the candidate picture; must not beNO_PTSnextPtsMicros- presentation time of the picture queued behind it (orNO_PTS), the only thing that can license a drop- Returns:
- what to do with the candidate
- Throws:
IllegalArgumentException- ifptsMicrosisNO_PTS, because a picture with no timestamp cannot be timed and arithmetic on the sentinel would silently produce a plausible wrong answer
-
decide
As deciding with no successor named, which therefore never returns a drop. -
positionMicros
public long positionMicros()- Returns:
- the current media position in microseconds. Before the first decision, the position a
seekTo(long)moved to, or failing that the installed master's position, or 0 when none is installed: nothing has anchored the timeline yet, so a stream whose presentation times start at a non-zero container base reads 0 until its first picture is judged. While paused, the position the timeline was frozen at. Otherwise the master's, or the wall-driven fallback's while the master is stalled or absent. It reads only the master before the first decision, nothing at all while paused, and otherwise the wall clock plus the master when one is installed and running; it changes nothing either way, because stall and jump detection happen only indecide(long, long).
-
isMasterStalled
public boolean isMasterStalled()- Returns:
- whether the master has reported an unchanged position for longer than
MASTER_STALL_MICROS; goes false again by itself at the first decision whose reading has moved
-
driftMicros
public long driftMicros()- Returns:
- presentation time minus position at the last picture shown, in microseconds; positive
means the picture ran ahead of the clock. It stays within one frame interval while
the policy is tracking, and grows without bound when the master's rate is not the
stream's. A detected master jump clears it, as do
reset()andseekTo(long), so a player watching drift to decide whether to resync reads 0 right after a seek until the next picture is shown; the jump counter, not this, is what says a master moved on its own.
-
presentedFrames
public long presentedFrames()- Returns:
- pictures shown since construction; a reset does not clear it
-
droppedFrames
public long droppedFrames()- Returns:
- pictures dropped unshown since construction
-
heldFrames
public long heldFrames()- Returns:
- decisions to keep the current picture since construction
-
jumpCount
public long jumpCount()- Returns:
- times a master reading differed from the wall time elapsed since the previous reading
that moved, in either direction, by more than
MASTER_JUMP_MICROS; a scrub backwards counts exactly as a seek forwards does
-
stallCount
public long stallCount()- Returns:
- times the master was declared stalled
-