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
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,17 @@ static int ring_fill_ms(pcm_ring* r) {
return (int)((int64_t)frames * 1000 / srr);
}

/* Drop everything buffered — used on a seek so pre-seek chunks can neither gate
* the ring (front chunks ahead of the target block the post-seek audio behind
* them) nor play out ahead of the post-seek audio. Mirrors PcmRing::flush. */
static void ring_flush(pcm_ring* r) {
pthread_mutex_lock(&r->m);
r->head = 0; r->tail = 0;
r->chead = 0; r->ccount = 0;
r->playedUs = INT64_MIN;
pthread_mutex_unlock(&r->m);
}

/* ---- video frame ring --------------------------------------------------- */

/* Decoded frames are held as acquired AImages (each owning an AHardwareBuffer),
Expand Down Expand Up @@ -259,6 +270,22 @@ struct basis_decoder {
int bufferMode; /* 0 = fixed, 1 = dynamic */
int audioLatencyUs; /* managed sink's reported output latency; drives the video hold + audio lead */

/* Seek notification (mirrors basis_win_decode.cpp). basis_decoder_seek bumps
* seekGen (+ latches the target) on the caller thread. Each leg keeps its own
* last-seen copy and flushes on ITS OWN thread: the audio submit (demux) thread
* flushes the PCM ring + AAC codec; the video submit (demux) thread flushes the
* video codec + frame ring (it owns vcodec and writes vimg); the render thread
* re-anchors the present clock. seekGen/seekTargetUs are cross-thread (atomics). */
int seekGen; /* atomic */
int64_t seekTargetUs; /* atomic */
int audioSeekGen; /* audio-submit (demux) thread only */
int videoSeekGen; /* video-submit (demux) thread only */
int renderSeekGen; /* render thread only */
int videoSeekAck; /* atomic: demux publishes seekGen once it has flushed the
* 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 */

/* debug counters */
long dbg_render, dbg_nodue, dbg_acqfail, dbg_drop, dbg_lagms;
};
Expand Down Expand Up @@ -860,6 +887,22 @@ 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
* owns vcodec and writes vimg (drain_video_output below), so both are safe here;
* the frame release mirrors the shutdown path (AImage_delete under vm). */
int svg = __atomic_load_n(&d->seekGen, __ATOMIC_ACQUIRE);
if (svg != d->videoSeekGen) {
d->videoSeekGen = svg;
AMediaCodec_flush(d->vcodec);
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);
/* 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);
}
int rc = -1;
ssize_t ii = AMediaCodec_dequeueInputBuffer(d->vcodec, 2000);
if (ii >= 0) {
Expand Down Expand Up @@ -933,6 +976,15 @@ static void submit_lpcm(basis_decoder_t* d, const uint8_t* p, int len, int64_t p

int basis_decoder_submit_audio(basis_decoder_t* d, const uint8_t* data, int len, int64_t pts_us) {
if (!d || !data || len <= 0) return -1;
/* First audio AU after a seek: drop the stale pre-seek ring so this post-seek
* audio serves immediately, and flush the AAC codec so it doesn't overlap-add
* across the discontinuity. Demux thread, the only thread that touches acodec. */
int sg = __atomic_load_n(&d->seekGen, __ATOMIC_ACQUIRE);
if (sg != d->audioSeekGen) {
d->audioSeekGen = sg;
ring_flush(&d->pcm);
if (d->acodec) AMediaCodec_flush(d->acodec);
}
if (d->ac == BASIS_CODEC_LPCM) { submit_lpcm(d, data, len, pts_us); return 0; }
if (!d->acodec) return -1;
int rc = -1;
Expand Down Expand Up @@ -1143,7 +1195,29 @@ static void present_select(basis_decoder_t* d) {
int basis_decoder_render_update(basis_decoder_t* d) {
if (!d || !d->vk) return -1;
if (basis_engine_is_paused(d->engine)) return 0;
present_select(d);
/* First render after a seek: reset the present clock so present_select re-locks
* it to the first post-seek frame instead of staying clamped to the stale decode
* edge (a video/clock freeze on a cold forward seek). Render-thread-owned clock;
* the frame ring is cleared by the demux thread that owns it (submit_video), and
* this leg waits on that clear (videoSeekAck) before selecting a frame. */
int rsg = __atomic_load_n(&d->seekGen, __ATOMIC_ACQUIRE);
if (rsg != d->renderSeekGen) {
d->renderSeekGen = rsg;
d->clockStarted = 0;
d->primeStartUs = 0;
d->lastPresentedPts = INT64_MIN;
d->mediaStartUs = 0;
__atomic_store_n(&d->presentedPosUs,
__atomic_load_n(&d->seekTargetUs, __ATOMIC_ACQUIRE), __ATOMIC_RELAXED);
}
/* Hold frame selection until the demux thread has flushed the codec and released
* the pre-seek frames. Selecting before then would show a stale frame, and
* releasing them here would race the producer and could delete post-seek frames
* it has already enqueued (notably when seeking while paused). Keep rendering so
* the last frame stays up during the hold. */
if (__atomic_load_n(&d->videoSeekAck, __ATOMIC_ACQUIRE) == rsg) {
present_select(d);
}
return basis_vk_render_update(d->vk);
}
void basis_decoder_render_release(basis_decoder_t* d) { if (d && d->vk) basis_vk_release(d->vk); }
Expand All @@ -1170,6 +1244,21 @@ int basis_decoder_get_video_size(basis_decoder_t* d, int* w, int* h) {
int basis_decoder_get_frame_origin(basis_decoder_t* d) { (void)d; return 0; }
/* Presentation position once a frame has shown; decode edge before that
* (start-up, audio-only) so early consumers still see the clock move. */
void basis_decoder_seek(basis_decoder_t* d, int64_t target_us) {
if (!d) return;
/* Drop any pre-seek PCM still queued so the audio callback stops serving it
* immediately rather than up to the next audio AU. ring_flush takes the pcm
* mutex, safe from this (caller) thread; the codec reset stays on the submit
* thread where the decoder is owned. */
ring_flush(&d->pcm);
/* Latch the target before bumping the generation so any leg that sees the new
* generation reads the matching target. presentedPosUs is set here so the seek
* bar snaps to the target immediately, before a post-seek frame presents. */
__atomic_store_n(&d->seekTargetUs, target_us, __ATOMIC_RELEASE);
__atomic_store_n(&d->presentedPosUs, target_us, __ATOMIC_RELAXED);
__atomic_add_fetch(&d->seekGen, 1, __ATOMIC_RELEASE);
}

int64_t basis_decoder_get_position_us(basis_decoder_t* d) {
if (!d) return -1;
int64_t presented = __atomic_load_n(&d->presentedPosUs, __ATOMIC_RELAXED);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,25 @@ static int ensure_format_objects(basis_vk_present* v, uint64_t externalFormat,
/* Y'CbCr conversion described by the AHB external format. */
VkExternalFormatANDROID extFmt = { VK_STRUCTURE_TYPE_EXTERNAL_FORMAT_ANDROID };
extFmt.externalFormat = externalFormat;
/* Linear filtering is only valid when the format advertises the matching
* feature bit, and each filter has its own bit: chroma reconstruction needs
* YCBCR_CONVERSION_LINEAR_FILTER, the ordinary sampler mag/min needs
* SAMPLED_IMAGE_FILTER_LINEAR, and the two filters may only differ when
* SEPARATE_RECONSTRUCTION_FILTER is present. Qualcomm's UBWC external formats
* (VP9/AV1 on Adreno) frequently advertise none of these, and forcing LINEAR
* there is undefined behaviour. Pick each filter from its own bit; when separate
* reconstruction is absent, force them equal (downgrading chroma to nearest if
* the sampler cannot do linear). */
VkFormatFeatureFlags features = fmt->formatFeatures;
int separate = (features & VK_FORMAT_FEATURE_SAMPLED_IMAGE_YCBCR_CONVERSION_SEPARATE_RECONSTRUCTION_FILTER_BIT) != 0;
int linearSample = (features & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT) != 0;
VkFilter yf = (features & VK_FORMAT_FEATURE_SAMPLED_IMAGE_YCBCR_CONVERSION_LINEAR_FILTER_BIT)
? VK_FILTER_LINEAR : VK_FILTER_NEAREST;
VkFilter samplerFilter = linearSample ? VK_FILTER_LINEAR : VK_FILTER_NEAREST;
if (!separate) {
if (yf == VK_FILTER_LINEAR && !linearSample) yf = VK_FILTER_NEAREST;
samplerFilter = yf;
}
VkSamplerYcbcrConversionCreateInfo cy = { VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO };
cy.pNext = &extFmt;
cy.format = VK_FORMAT_UNDEFINED;
Expand All @@ -277,15 +296,15 @@ static int ensure_format_objects(basis_vk_present* v, uint64_t externalFormat,
cy.components = fmt->samplerYcbcrConversionComponents;
cy.xChromaOffset = fmt->suggestedXChromaOffset;
cy.yChromaOffset = fmt->suggestedYChromaOffset;
cy.chromaFilter = VK_FILTER_LINEAR;
cy.chromaFilter = yf;
if (vkCreateSamplerYcbcrConversion(v->device, &cy, NULL, &v->ycbcr) != VK_SUCCESS) return -1;

/* immutable sampler with the conversion attached */
VkSamplerYcbcrConversionInfo cvInfo = { VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_INFO };
cvInfo.conversion = v->ycbcr;
VkSamplerCreateInfo sci = { VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO };
sci.pNext = &cvInfo;
sci.magFilter = VK_FILTER_LINEAR; sci.minFilter = VK_FILTER_LINEAR;
sci.magFilter = samplerFilter; sci.minFilter = samplerFilter;
sci.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
sci.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sci.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1359,6 +1359,11 @@ BASIS_API int BASIS_CALL basis_media_seek_us(basis_media_engine_t* e, int64_t ta
* that boundary before the generation is visible. */
e->seek_target_us = target_us;
e->seek_seq++;
/* Notify the decoder to drop its pre-seek audio/video buffers and re-anchor
* the present clock to the target (each leg does it on its own thread). The
* demuxer only repositions the byte source; without this the decoder keeps
* serving stale buffers — post-seek audio silence and a frozen video clock. */
if (e->decoder) basis_decoder_seek(e->decoder, target_us);
void* hls = e->active_hls;
int rc = hls ? basis_hls_request_seek(hls, target_us / 1000) : 0;
mutex_unlock(&e->lock);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,12 @@ int basis_decoder_try_open_url(basis_decoder_t* dec, const char* url);
int basis_decoder_render_update(basis_decoder_t* dec);
void basis_decoder_render_release(basis_decoder_t* dec);

/* Seek notification (any thread; typically the caller of basis_media_seek_us).
* Sets a target + bumps a generation the decoder's consumer legs observe; each
* leg flushes its own stale buffers and re-anchors the clock ON ITS OWN THREAD,
* so nothing is flushed across threads. Idempotent per generation. */
void basis_decoder_seek(basis_decoder_t* dec, int64_t target_us);

/* Accessors mirrored by the public ABI (any thread unless noted). */
void* basis_decoder_get_texture(basis_decoder_t* dec, int* out_w, int* out_h);
uint64_t basis_decoder_get_frame_counter(basis_decoder_t* dec);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@ static void hls_mutex_unlock(hls_mutex_t* m) { pthread_mutex_unlock(m); }
#define HLS_MAX_PLAYLIST (1 << 20) /* 1 MiB playlist cap */
#define HLS_MAX_EMPTY_RELOADS 8 /* consecutive no-new-media reloads before giving up */
#define HLS_LIVE_MARGIN_SEGMENTS 3 /* playout buffer kept behind the live edge for plain (non-LL) HLS */
#define HLS_RING_CAP (4 * 1024 * 1024) /* read-ahead byte buffer (~5 s of 1080p HD) */
/* Read-ahead byte buffer (~5 s of 1080p HD). Kept small on purpose: a larger
* read-ahead makes the VOD producer reach the end of the playlist further ahead
* of playout, which widens the window where a seek is rejected outright (the
* producer_done path). One heap allocation per HLS stream. */
#define HLS_RING_CAP (4 * 1024 * 1024)

/* (msn, part) media position. part == -1 means a whole segment. */
typedef struct {
Expand Down Expand Up @@ -785,11 +789,13 @@ void* basis_hls_open(const char* url, const basis_http_provider_t* http,
/* Start the read-ahead producer so segments buffer ahead of playout. */
h->ring_cap = HLS_RING_CAP;
h->ring = (uint8_t*)malloc((size_t)h->ring_cap);
if (!h->ring) { free(h); return NULL; }
if (!h->ring) { free(h->vod_uri); free(h->vod_dur_ms); free(h); return NULL; }
hls_mutex_init(&h->lock);
if (!hls_thread_start(h)) {
hls_mutex_destroy(&h->lock);
free(h->ring);
free(h->vod_uri);
free(h->vod_dur_ms);
free(h);
return NULL;
}
Expand Down
Loading
Loading