Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

90 changes: 81 additions & 9 deletions Basis/Packages/com.basis.mediaplayer/Native~/basis_media_core.c
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,18 @@ static void sink_video_format(void* user, basis_codec_t codec, const uint8_t* ed
static void sink_video_au(void* user, const uint8_t* au, int len, int64_t pts, int64_t dts, int key) {
basis_media_engine_t* e = (basis_media_engine_t*)user;
if (!e->running) return;
/* Drop video a demuxer emits after a seek is posted but before the main leg
* takes it (the same read-buffer-granularity window the audio drop gate
* covers). These tail AUs are mid-GOP leftovers that post-date the decoder's
* seek flush: they can't decode correctly without their references, some
* hardware decoders emit them anyway against stale reference memory (a
* flash of the pre-seek picture), and — because their PTS can sit past the
* seek target — the first of them would end the decoder's preroll run-up
* early and let the whole run-up render. Video always rides the main leg
* (a split source's audio leg never submits video). HLS is excluded for
* the same reason as audio: it repositions at the BASIS_READ_REPOSITION
* boundary and seek_taken is not its signal. */
if (!e->active_hls && e->seek_seq != e->seek_taken_main) return;
/* Pace on the decode timestamp: gating on pts would sleep out a composition
* offset the decoder still needs the AU inside of, and starve the other
* track's earlier samples queued behind this one on the demux thread. */
Expand Down Expand Up @@ -395,6 +407,14 @@ static void sink_audio_frame(void* user, const uint8_t* data, int len, int64_t p
if (e->url_audio[0] || !e->video_format_seen)
pace_gate(e, pts); /* paced mode: hold until ~real time; no-op otherwise */
if (!e->running) return;
/* Re-check the pre-seek drop after the pace hold: pace_gate parks this thread
* for up to the pace lead, so a seek posted while this frame slept would
* otherwise let it through with its pre-seek PTS. Submitted, it would trigger
* the decoder's seek flush early — the post-flush timeline re-anchor would
* measure against its stale PTS — and it would sit in the flushed ring as a
* stale front chunk. */
taken = e->url_audio[0] ? e->seek_taken_audio : e->seek_taken_main;
if (!e->active_hls && e->seek_seq != taken) return;
e->audio_frame_count++;
mutex_lock(&e->submit_lock);
basis_decoder_submit_audio(e->decoder, data, len, pts);
Expand Down Expand Up @@ -426,7 +446,14 @@ static void sink_transport(void* user, const char* t) {
e->transport[sizeof(e->transport) - 1] = 0;
mutex_unlock(&e->lock);
}
static void sink_eos(void* user) { basis_engine_set_state((basis_media_engine_t*)user, BASIS_MEDIA_STATE_ENDED); }
/* Live end-of-stream ends now. A paced (VOD) source's delivery runs ahead of
* presentation — the pace lead, the audio serve cushion, and any post-seek
* settle skew are all still banked when the demuxer finishes — so its ENDED
* is raised by demux_body after the presentation drain, not here. */
static void sink_eos(void* user) {
basis_media_engine_t* e = (basis_media_engine_t*)user;
if (!e->paced) basis_engine_set_state(e, BASIS_MEDIA_STATE_ENDED);
}
static void sink_duration(void* user, int64_t us) { basis_media_engine_t* e = (basis_media_engine_t*)user; if (us > 0) e->duration_us = us; }
/* A raised error is fatal to the current demux run: the reconnect loop already
* treats an error state as non-retryable, so stopping here makes the protocol
Expand All @@ -441,12 +468,20 @@ static int take_seek_common(basis_media_engine_t* e, volatile long* taken, int64
mutex_lock(&e->lock);
long seq = e->seek_seq;
int64_t us = e->seek_target_us;
/* Re-anchor delivery pacing: only post-seek samples flow on this leg from
* here, and against the old anchor they'd read as far-future (a forward
* seek stalls the demux thread for the jump distance) or as late (a
* backward seek floods through unpaced and fast-forwards back). The next
* paced sample re-establishes base/wall from its own timestamp. */
if (*taken != seq) e->pace_started = 0;
/* Re-anchor delivery pacing at the seek TARGET, not at whatever sample
* arrives next. A container seek repositions to the sync point at or
* before the target — with a sparse-keyframe file that can be tens of
* seconds of run-up — and anchoring on the first delivered sample would
* pace that whole preroll at 1x (a silent, position-pinned crawl to the
* target). Against a target anchor the preroll reads as late and flows at
* decode speed while everything from the target onwards paces at 1x. The
* decoders drop decoded video frames short of the target (they exist only
* as references), so the preroll is never shown. */
if (*taken != seq) {
e->pace_wall0_us = now_us();
e->pace_base_pts = us;
e->pace_started = 1;
}
mutex_unlock(&e->lock);
if (*taken == seq) return 0;
*taken = seq;
Expand Down Expand Up @@ -1073,8 +1108,45 @@ static void demux_body(basis_media_engine_t* e) {
/* Paced (VOD) sources are finite and play once: a clean run end is EOF, not
* a live drop to reconnect through. Looping would replay from PTS 0 while the
* paced clock is at the old edge — every frame would read "behind" the clock
* and flood in ungated (fast-forward). Stop instead. */
if (e->paced) { basis_engine_set_state(e, BASIS_MEDIA_STATE_ENDED); break; }
* and flood in ungated (fast-forward). Stop instead — but let presentation
* drain first: delivery runs ahead by the pace lead plus the audio serve
* cushion (and any post-seek settle skew), so several seconds can still be
* banked when the demuxer finishes. ENDED fires once the reported position
* has stopped advancing for a beat; paused time doesn't count as idle. */
if (e->paced) {
/* Flush the video decoder's reorder tail into the ring first —
* nothing else ever tells it the stream is over, and what it
* retains is the end of the file (seconds, at low frame rates). */
if (e->decoder) {
mutex_lock(&e->submit_lock);
basis_decoder_notify_end_of_stream(e->decoder);
mutex_unlock(&e->submit_lock);
}
/* Presentation is drained when the decoder holds nothing more to
* show or serve AND the reported position has settled — a stall
* alone is not enough, since a variable-frame-rate tail can hold
* the position flat for seconds with frames still queued. The
* absolute cap is the escape hatch for a consumer that never
* presents (a headless probe) or a wedged renderer. Paused time
* counts toward neither clock. */
int64_t last_pos = -1;
int idle_ms = 0, waited_ms = 0;
while (e->running) {
if (e->paused) { idle_ms = 0; sleep_interruptible(e, 50); continue; }
int pending = e->decoder ? basis_decoder_presentation_pending(e->decoder) : 0;
int64_t pos = e->decoder ? basis_decoder_get_position_us(e->decoder) : -1;
if (pos != last_pos) { last_pos = pos; idle_ms = 0; }
else idle_ms += 50;
if (!pending && idle_ms >= 700) break;
waited_ms += 50;
if (waited_ms >= 10000) break;
sleep_interruptible(e, 50);
}
/* A stop/close that cleared `running` mid-drain must not read as the
* content finishing: ENDED reaches OnEnded consumers (playlists). */
if (e->running) basis_engine_set_state(e, BASIS_MEDIA_STATE_ENDED);
break;
}

long au_after = e->video_au_count + e->audio_frame_count;
long delta = au_after - au_before;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,18 @@ int basis_decoder_get_video_size(basis_decoder_t* dec, int* out_w, int* out
* video processor can't mirror on this GPU; Vulkan always normalizes to 0. */
int basis_decoder_get_frame_origin(basis_decoder_t* dec);
int64_t basis_decoder_get_position_us(basis_decoder_t* dec);
/* Delivery has ended: flush the video decoder's reorder tail into the frame
* ring (nothing else ever tells it the stream is over, and the retained
* frames are the last seconds of the file — large at low frame rates).
* Video only: the audio decoder's latency is a single frame and a split
* source's audio leg may still be submitting. Call on the video-submit
* (demux) thread under the engine's submit lock. */
void basis_decoder_notify_end_of_stream(basis_decoder_t* dec);
/* Non-zero while decoded frames are still banked for presentation or decoded
* audio is still unserved — the end-of-stream drain waits on this rather than
* inferring quiescence from a position stall (which a variable-frame-rate
* tail can legitimately hold flat for seconds). */
int basis_decoder_presentation_pending(basis_decoder_t* dec);
int basis_decoder_get_audio_format(basis_decoder_t* dec, int* out_rate, int* out_channels);
int basis_decoder_read_audio(basis_decoder_t* dec, float* out, int max_floats); /* audio thread */
int basis_decoder_get_debug(basis_decoder_t* dec, char* buf, int size); /* diagnostics */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ uint64_t basis_decoder_get_frame_counter(basis_decoder_t* d) { (void)d; return 0
int basis_decoder_get_video_size(basis_decoder_t* d, int* w, int* h) { (void)d;(void)w;(void)h; return -1; }
int basis_decoder_get_frame_origin(basis_decoder_t* d) { (void)d; return 0; }
int64_t basis_decoder_get_position_us(basis_decoder_t* d) { (void)d; return -1; }
void basis_decoder_notify_end_of_stream(basis_decoder_t* d) { (void)d; }
int basis_decoder_presentation_pending(basis_decoder_t* d) { (void)d; return 0; }
int basis_decoder_get_audio_format(basis_decoder_t* d, int* r, int* c) { (void)d;(void)r;(void)c; return -1; }
int basis_decoder_read_audio(basis_decoder_t* d, float* o, int m) { (void)d;(void)o;(void)m; return 0; }
int basis_decoder_get_debug(basis_decoder_t* d, char* buf, int size) { (void)d; if (buf && size > 0) buf[0] = 0; return 0; }
Expand Down
61 changes: 42 additions & 19 deletions Basis/Packages/com.basis.mediaplayer/Native~/protocol/basis_hls.c
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,16 @@ typedef struct basis_hls {
hls_thread_t thread;
int thread_started;
volatile int stop;
volatile int producer_done;
volatile int producer_done; /* the producer thread has actually exited (stop,
* policy block, reload exhaustion) — the only
* state that rejects a seek */
volatile int vod_idle; /* VOD fully fetched and the producer is parked,
* alive, waiting for a seek to revive it; with a
* drained ring and no seek in flight the reader
* reports end-of-stream. Set in the endlist
* branch and cleared in the seek flush, both
* under `lock`, so the reader can never see an
* idle mark alongside a fresh flush generation */

/* Delivery is paced by the engine (pace_gate, by AU timestamp), not here. */
} basis_hls_t;
Expand Down Expand Up @@ -595,8 +604,11 @@ static void hls_producer(basis_hls_t* h) {
/* Publish the flush for the snapshot generation under the same lock
* as the ring clear, so a consumer that acquires it sees an emptied
* ring and the matched generation together; its next serve is
* guaranteed post-seek. */
* guaranteed post-seek. Leaving idle in the same critical section
* keeps the reader from ever pairing an emptied ring with a stale
* end-of-stream verdict while the target segment is still fetching. */
h->flush_gen = seek_g;
h->vod_idle = 0;
hls_mutex_unlock(&h->lock);
}
if (!h->seg_ctx) {
Expand Down Expand Up @@ -624,16 +636,21 @@ static void hls_producer(basis_hls_t* h) {
}
h->empty_reloads = 0;
} else if (h->endlist_seen) {
/* VOD exhausted. Arbitrate against a late seek under the lock:
* either honour a pending request (loop back) or mark the
* producer done so basis_hls_request_seek rejects further
* seeks. The two can't interleave, so no request is lost and
* the reader never withholds on a flush that never comes. */
/* Unseekable VOD (fMP4: no TS segment list, request_seek
* rejects it) has nothing to park for — exit normally. */
if (h->vod_count == 0) break;
/* VOD exhausted — park, don't exit. A backward seek into the
* tail must still work for as long as the source is open, so
* the thread idles here and the top-of-loop seek take revives
* it. Arbitrate against a pending request under the lock so
* the two can't interleave: either loop back to honour it, or
* publish idle for the reader's end-of-stream verdict. */
hls_mutex_lock(&h->lock);
if (h->seek_pending) { hls_mutex_unlock(&h->lock); continue; }
h->producer_done = 1;
h->vod_idle = 1;
hls_mutex_unlock(&h->lock);
break; /* VOD / stream finished */
hls_sleep_ms(10);
continue;
} else {
int r = reload_and_enqueue(h);
if (r > 0) { h->empty_reloads = 0; }
Expand Down Expand Up @@ -822,8 +839,9 @@ int basis_hls_read(void* ctx, uint8_t* buf, int len) {
/* Seek in flight: the ring still holds pre-seek bytes until the producer
* flushes and requeues at the target. Withhold them, since handing them
* to the demuxer would let a stale AU re-anchor pacing to the old timeline.
* If the producer has already exited (VOD genuinely ended) without
* honouring the seek, stop waiting for a flush that will never come. */
* producer_done here means the thread actually exited (stop / policy
* block / reload exhaustion) — a parked VOD producer still honours the
* request — so only then stop waiting for a flush that will never come. */
if (h->seek_gen != h->flush_gen) {
int done = h->producer_done;
hls_mutex_unlock(&h->lock);
Expand All @@ -850,9 +868,13 @@ int basis_hls_read(void* ctx, uint8_t* buf, int len) {
hls_mutex_unlock(&h->lock);
return take;
}
int done = h->producer_done;
/* End of stream: the ring is drained and either the producer exited or a
* fully-fetched VOD is parked with no seek in flight (this branch is only
* reachable with the generations settled, so a pending seek can't race
* the verdict — request_seek bumps the generation under this lock). */
int done = h->producer_done || h->vod_idle;
hls_mutex_unlock(&h->lock);
if (done) return 0; /* producer finished and ring drained -> end of stream */
if (done) return 0;

hls_sleep_ms(2); /* ring empty: wait for the producer to buffer more */
}
Expand All @@ -876,12 +898,13 @@ int basis_hls_can_seek(void* ctx) {
int basis_hls_request_seek(void* ctx, long long target_ms) {
basis_hls_t* h = (basis_hls_t*)ctx;
if (!h || h->vod_count <= 0 || target_ms < 0) return -1; /* vod_count is fixed at open */
/* Accept the seek atomically with the producer_done check so a request can't
* be lost against the producer exiting on end-of-stream: the endlist path
* sets producer_done under this same lock while verifying no seek is pending,
* so exactly one of the two wins. Publish {target, generation, pending}
* together; the generation runs ahead of flush_gen until the producer
* finishes flushing, during which the consumer withholds the pre-seek ring. */
/* Reject only when the producer thread has actually exited — a fully-fetched
* VOD parks its producer instead, precisely so a seek into the tail (or after
* playout drained the ring) still repositions. Accept atomically with that
* check so a request can't be lost against a concurrent exit; publish
* {target, generation, pending} together, and the generation runs ahead of
* flush_gen until the producer finishes flushing, during which the consumer
* withholds the pre-seek ring. */
hls_mutex_lock(&h->lock);
if (h->producer_done) { hls_mutex_unlock(&h->lock); return -1; }
h->seek_target_ms = (long)target_ms;
Expand Down
Loading
Loading