Package limn.video

Class VideoClock

java.lang.Object
limn.video.VideoClock

public final class VideoClock extends Object
Decides, one candidate picture at a time, whether to show it, keep the current one, or throw it away: a player's whole timing policy, as arithmetic.

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 Classes
    Modifier and Type
    Class
    Description
    static enum 
    What the caller must do with the picture it just asked about.
    static interface 
    A media position source on the same timeline as the pictures' presentation times.
  • Field Summary

    Fields
    Modifier and Type
    Field
    Description
    static final long
    Half of a 60 Hz refresh interval, in microseconds: the half-width of the window in which a picture counts as due.
    static final long
    How 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 long
    Microseconds of wall time a master may report an unchanged position before it is declared stalled and the wall clock takes over.
    static final long
    Passed as the successor's presentation time when the caller has nothing queued behind the candidate.
  • Constructor Summary

    Constructors
    Constructor
    Description
    Drives from the system's monotonic clock until a master is installed.
     
  • Method Summary

    Modifier and Type
    Method
    Description
    decide(long ptsMicros)
    As deciding with no successor named, which therefore never returns a drop.
    decide(long ptsMicros, long nextPtsMicros)
     
    long
     
    long
     
    long
     
    boolean
     
    long
     
    long
     
    long
     
    void
    Forgets the timeline, so the next picture offered is shown and re-anchors it.
    void
    seekTo(long positionMicros)
    Declares that the timeline has been moved to positionMicros (the master's position included) so that the move is not mistaken for a master that has run away.
    void
    Installs the master position source, or null to free-run on the wall clock.
    void
    setPaused(boolean newPaused)
    Freezes the timeline.
    long
     

    Methods inherited from class java.lang.Object

    clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
  • Field Details

    • NO_PTS

      public static final long NO_PTS
      Passed 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_MICROS
      Half 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_MICROS
      Microseconds 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_MICROS
      How 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

      public VideoClock(LongSupplier nanoClock)
      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 - if nanoClock is null
  • Method Details

    • setMaster

      public void setMaster(VideoClock.MasterClock newMaster)
      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 to positionMicros (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_MICROS is counted as a jump, and a player watching jumpCount() 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() reports positionMicros exactly 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, and jumpCount() is deliberately not incremented, which is what a player distinguishes a seek from a runaway master by.

    • decide

      public VideoClock.Decision decide(long ptsMicros, long nextPtsMicros)
      Parameters:
      ptsMicros - presentation time of the candidate picture; must not be NO_PTS
      nextPtsMicros - presentation time of the picture queued behind it (or NO_PTS), the only thing that can license a drop
      Returns:
      what to do with the candidate
      Throws:
      IllegalArgumentException - if ptsMicros is NO_PTS, because a picture with no timestamp cannot be timed and arithmetic on the sentinel would silently produce a plausible wrong answer
    • decide

      public VideoClock.Decision decide(long ptsMicros)
      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 in decide(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() and seekTo(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