Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,31 @@ struct basis_decoder {
* codec + released pre-seek frames; the render leg holds
* until it matches so it neither anchors to nor deletes a
* post-seek frame the producer has already enqueued */
int64_t vPrerollCutUs; /* video-submit thread only: after a seek, decoded frames
* short of this are the keyframe run-up to the target —
* reference-only, released unrendered. Set from
* seekTargetUs at the seek flush, cleared by the first
* frame at or past it. */
int vAwaitKey; /* video-submit thread only: set at the seek flush, cleared
* by the first OUTPUT of the first keyframe submitted after
* it (matched by PTS via vAwaitKeyPts). Output before that
* is post-flush mid-GOP input decoded against stale
* reference memory (Adreno emits it as a visible pre-seek
* flash; the HLS path can still hand over a pre-seek tail
* AU the engine's seek_taken gate doesn't cover) — released
* unrendered, and it must not end the preroll run-up.
* Clearing at the keyframe's SUBMISSION is not enough: a
* tail AU queued just before it can still emit its garbage
* frame afterwards, and with a pre-seek PTS past the target
* that frame would both show and end the run-up. */
int64_t vAwaitKeyPts; /* video-submit thread only: PTS of that keyframe;
* INT64_MIN until it is submitted */
int vAwaitDrained; /* video-submit thread only: outputs drained since the
* seek flush; bounds the wait so a dropped or
* re-stamped keyframe output — or a run whose post-seek
* AUs are never flagged as keyframes, which would
* otherwise never latch vAwaitKeyPts — can't hold the
* gate (and video) shut until the next seek */

/* debug counters */
long dbg_render, dbg_nodue, dbg_acqfail, dbg_drop, dbg_lagms;
Expand Down Expand Up @@ -319,6 +344,14 @@ static void on_image(void* ctx, AImageReader* reader) {

int64_t ts_ns = 0;
AImage_getTimestamp(img, &ts_ns); /* MediaCodec propagates the input PTS (ns) */
/* Seek-generation tag (sub-microsecond digits, written at release):
* a mismatch means this frame crossed a seek flush in flight — showing
* it would anchor the present clock at the pre-seek position. */
{
int tag = (int)(((ts_ns % 1000) + 1000) % 1000);
int gen = ((__atomic_load_n(&d->seekGen, __ATOMIC_ACQUIRE) % 1000) + 1000) % 1000;
if (tag != gen) { AImage_delete(img); continue; }
}
int64_t pts = ts_ns / 1000;
int32_t aw = 0, ah = 0;
AImage_getWidth(img, &aw); AImage_getHeight(img, &ah);
Expand Down Expand Up @@ -419,15 +452,57 @@ static int ensure_reader(basis_decoder_t* d, int w, int h) {

/* ---- output draining (push decoded frames to the Surface) --------------- */

static void drain_video_output(basis_decoder_t* d) {
if (!d->vcodec) return;
/* Returns 1 once the codec has emitted its end-of-stream output (only after
* basis_decoder_notify_end_of_stream queued the EOS input); 0 otherwise. */
static int drain_video_output(basis_decoder_t* d) {
if (!d->vcodec) return 0;
int eos = 0;
for (;;) {
AMediaCodecBufferInfo info;
ssize_t oi = AMediaCodec_dequeueOutputBuffer(d->vcodec, &info, 0);
if (oi >= 0) {
if (info.flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM) eos = 1;
d->lastPtsUs = info.presentationTimeUs;
/* render=true pushes the frame onto the AImageReader Surface */
AMediaCodec_releaseOutputBuffer(d->vcodec, oi, info.size != 0);
/* render=true pushes the frame onto the AImageReader Surface. Post-seek
* preroll (keyframe run-up short of the target) is decoded so later
* frames have their references but released unrendered; output is
* display-order, so the first frame at or past the target ends the
* run-up for good — but only once the post-flush keyframe's own
* output has emerged: output before that is post-flush mid-GOP
* garbage whose PTS may sit past the target, and it must neither
* show nor end the run-up. */
int render = info.size != 0;
/* The PTS match is the designed clear (the keyframe's own output; the
* cut below gates it). The drain bound is a backstop, set well past any
* plausible garbage-tail length: past it the cut still suppresses the
* run-up, so the worst case is one stale frame — degraded, not wedged. */
if (d->vAwaitKey &&
((d->vAwaitKeyPts != INT64_MIN && info.presentationTimeUs == d->vAwaitKeyPts) ||
++d->vAwaitDrained > 16))
d->vAwaitKey = 0;
if (d->vAwaitKey) {
render = 0;
} else if (d->vPrerollCutUs != INT64_MIN) {
if (info.presentationTimeUs < d->vPrerollCutUs) render = 0;
else d->vPrerollCutUs = INT64_MIN;
}
if (render) {
/* Tag the frame with the seek generation in the sub-microsecond
* digits of the surface timestamp (the PTS rides in whole
* microseconds, so on_image's ts/1000 is untouched). A frame
* still in flight through the AImageReader listener when a seek
* flushes carries the old tag and dies at on_image instead of
* presenting its pre-seek PTS and mis-anchoring the clock.
* The tag is this thread's videoSeekGen, which advances only in
* the seek-flush block: a pre-seek frame drained after the seek
* posts but before the flush runs still carries the old tag,
* where the live seekGen would stamp it as post-seek content. */
int64_t tag = (int64_t)(((d->videoSeekGen % 1000) + 1000) % 1000);
AMediaCodec_releaseOutputBufferAtTime(d->vcodec, oi,
info.presentationTimeUs * 1000 + tag);
} else {
AMediaCodec_releaseOutputBuffer(d->vcodec, oi, 0);
}
} else if (oi == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) {
AMediaFormat* f = AMediaCodec_getOutputFormat(d->vcodec);
int32_t w = 0, h = 0;
Expand All @@ -451,6 +526,42 @@ static void drain_video_output(basis_decoder_t* d) {
break; /* try again later / no buffer */
}
}
return eos;
}

void basis_decoder_notify_end_of_stream(basis_decoder_t* d) {
if (!d || !d->vcodec) return;
/* Caller is the video-submit (demux) thread, which owns vcodec — same
* ownership as submit_video. MediaCodec flushes asynchronously after the
* EOS input, so pump the output side (bounded) until the EOS-flagged
* buffer emerges rather than trusting a single drain pass.
* TRY_AGAIN_LATER from dequeueInputBuffer is routine while the codec is
* busy, so retry to a short deadline (draining outputs frees input slots).
* If the EOS input still can't be queued, the retained tail stays in the
* codec, presentation_pending never sees it, and the core's drain-wait
* ends on its idle cap — degraded, not wedged. */
ssize_t ii = -1;
for (int i = 0; i < 50 && ii < 0; ++i) { /* ~500ms deadline */
ii = AMediaCodec_dequeueInputBuffer(d->vcodec, 10000);
if (ii < 0) drain_video_output(d);
}
if (ii < 0) return;
if (AMediaCodec_queueInputBuffer(d->vcodec, ii, 0, 0, 0,
AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM) != AMEDIA_OK) return;
for (int i = 0; i < 100; ++i) { /* ~1s cap */
if (drain_video_output(d)) return;
usleep(10000);
}
}

int basis_decoder_presentation_pending(basis_decoder_t* d) {
if (!d) return 0;
int pending = 0;
pthread_mutex_lock(&d->vm);
for (int i = 0; i < VRING; ++i) if (d->vimg[i]) { pending = 1; break; }
pthread_mutex_unlock(&d->vm);
if (!pending) pending = ring_fill_ms(&d->pcm) > 0;
return pending;
}

static void drain_audio_output(basis_decoder_t* d) {
Expand Down Expand Up @@ -554,6 +665,10 @@ static void* url_worker(void* arg) {
basis_engine_set_state(d->engine, BASIS_MEDIA_STATE_PLAYING);
while (basis_engine_is_running(d->engine)) {
if (basis_engine_is_paused(d->engine)) { usleep(10000); continue; }
/* No seek support on this path (no seekTo, no flush), but the drain's
* frame tag reads videoSeekGen — keep it tracking seekGen so a seek
* request can't leave every subsequent frame tagged stale. */
d->videoSeekGen = __atomic_load_n(&d->seekGen, __ATOMIC_ACQUIRE);

int track = AMediaExtractor_getSampleTrackIndex(d->extractor);
if (track == d->video_track && d->vcodec) feed_extractor_sample(d, d->vcodec, track);
Expand Down Expand Up @@ -608,6 +723,10 @@ basis_decoder_t* basis_decoder_create(basis_media_engine_t* engine) {
pthread_mutex_init(&d->vm, NULL);
d->lastPresentedPts = INT64_MIN;
d->presentedPosUs = -1;
d->vPrerollCutUs = INT64_MIN;
d->vAwaitKey = 0;
d->vAwaitKeyPts = INT64_MIN;
d->vAwaitDrained = 0;
d->prevWritePts = INT64_MIN;
d->audClockOffsetUs = INT64_MIN;
d->bufferUs = 120000;
Expand Down Expand Up @@ -888,7 +1007,6 @@ int basis_decoder_set_audio_format(basis_decoder_t* d, basis_codec_t codec,
}

int basis_decoder_submit_video(basis_decoder_t* d, const uint8_t* annexb, int len, int64_t pts_us, int key) {
(void)key;
if (!d || !d->vcodec || !annexb || len <= 0) return -1;
/* First video AU after a seek: flush the codec and release the pre-seek frames
* in the ring so they can't present ahead of the post-seek content. Demux thread
Expand All @@ -898,14 +1016,23 @@ int basis_decoder_submit_video(basis_decoder_t* d, const uint8_t* annexb, int le
if (svg != d->videoSeekGen) {
d->videoSeekGen = svg;
AMediaCodec_flush(d->vcodec);
d->vPrerollCutUs = __atomic_load_n(&d->seekTargetUs, __ATOMIC_ACQUIRE);
d->vAwaitKey = 1;
d->vAwaitKeyPts = INT64_MIN;
d->vAwaitDrained = 0;
pthread_mutex_lock(&d->vm);
for (int i = 0; i < VRING; ++i) if (d->vimg[i]) { AImage_delete(d->vimg[i]); d->vimg[i] = NULL; }
pthread_mutex_unlock(&d->vm);
/* Frames released to the Surface before the flush can still be in flight
* through the AImageReader listener and would land after the clear
* above; on_image drops them by their seek-generation timestamp tag,
* so no quiescence wait is needed here. */
/* Publish the generation so the render leg knows the pre-seek frames are gone
* and it can re-anchor. Releasing frames on this (owning) thread only stops
* the render leg from deleting post-seek frames drain_video_output re-enqueues. */
__atomic_store_n(&d->videoSeekAck, svg, __ATOMIC_RELEASE);
}
if (key && d->vAwaitKey && d->vAwaitKeyPts == INT64_MIN) d->vAwaitKeyPts = pts_us;
int rc = -1;
ssize_t ii = AMediaCodec_dequeueInputBuffer(d->vcodec, 2000);
if (ii >= 0) {
Expand Down Expand Up @@ -1268,6 +1395,15 @@ void basis_decoder_seek(basis_decoder_t* d, int64_t target_us) {
* mutex, safe from this (caller) thread; the codec reset stays on the submit
* thread where the decoder is owned. */
ring_flush(&d->pcm);
/* Invalidate the audio serve clock: it re-derives from presents, and until the
* first post-seek frame presents it still describes the pre-seek timeline. On a
* backward seek that stale (higher) clock reads freshly banked post-target audio
* as long-stale and the serve trims it away — eating the first second of audio
* after video resumes. INT64_MIN is the serve's hold state (a stream with video
* holds audio until the clock exists), so post-seek audio banks through the
* settle and releases in sync with the first presented frame. Audio-only stays
* ungated: its offset never leaves INT64_MIN in the first place. */
__atomic_store_n(&d->audClockOffsetUs, INT64_MIN, __ATOMIC_RELAXED);
/* Latch the target before bumping the generation so any leg that sees the new
* generation reads the matching target. */
__atomic_store_n(&d->seekTargetUs, target_us, __ATOMIC_RELEASE);
Expand Down
82 changes: 73 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 @@ -426,7 +438,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 +460,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 +1100,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
Loading
Loading