diff --git a/Basis/Packages/com.basis.mediaplayer/Native~/android/basis_android_decode.c b/Basis/Packages/com.basis.mediaplayer/Native~/android/basis_android_decode.c index a2f626e96..caa04e8cc 100644 --- a/Basis/Packages/com.basis.mediaplayer/Native~/android/basis_android_decode.c +++ b/Basis/Packages/com.basis.mediaplayer/Native~/android/basis_android_decode.c @@ -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), @@ -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; }; @@ -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) { @@ -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; @@ -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); } @@ -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); diff --git a/Basis/Packages/com.basis.mediaplayer/Native~/android/basis_android_vk.c b/Basis/Packages/com.basis.mediaplayer/Native~/android/basis_android_vk.c index 31a443918..3798286b0 100644 --- a/Basis/Packages/com.basis.mediaplayer/Native~/android/basis_android_vk.c +++ b/Basis/Packages/com.basis.mediaplayer/Native~/android/basis_android_vk.c @@ -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; @@ -277,7 +296,7 @@ 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 */ @@ -285,7 +304,7 @@ static int ensure_format_objects(basis_vk_present* v, uint64_t externalFormat, 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; diff --git a/Basis/Packages/com.basis.mediaplayer/Native~/basis_media_core.c b/Basis/Packages/com.basis.mediaplayer/Native~/basis_media_core.c index 79ffaf561..2985f5a01 100644 --- a/Basis/Packages/com.basis.mediaplayer/Native~/basis_media_core.c +++ b/Basis/Packages/com.basis.mediaplayer/Native~/basis_media_core.c @@ -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); diff --git a/Basis/Packages/com.basis.mediaplayer/Native~/basis_media_internal.h b/Basis/Packages/com.basis.mediaplayer/Native~/basis_media_internal.h index 22b96fe1c..edb1744a1 100644 --- a/Basis/Packages/com.basis.mediaplayer/Native~/basis_media_internal.h +++ b/Basis/Packages/com.basis.mediaplayer/Native~/basis_media_internal.h @@ -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); diff --git a/Basis/Packages/com.basis.mediaplayer/Native~/protocol/basis_hls.c b/Basis/Packages/com.basis.mediaplayer/Native~/protocol/basis_hls.c index fb188f667..8c7f4807f 100644 --- a/Basis/Packages/com.basis.mediaplayer/Native~/protocol/basis_hls.c +++ b/Basis/Packages/com.basis.mediaplayer/Native~/protocol/basis_hls.c @@ -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 { @@ -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; } diff --git a/Basis/Packages/com.basis.mediaplayer/Native~/windows/basis_win_decode.cpp b/Basis/Packages/com.basis.mediaplayer/Native~/windows/basis_win_decode.cpp index d35bdd4d6..ba9acfd64 100644 --- a/Basis/Packages/com.basis.mediaplayer/Native~/windows/basis_win_decode.cpp +++ b/Basis/Packages/com.basis.mediaplayer/Native~/windows/basis_win_decode.cpp @@ -198,6 +198,18 @@ struct PcmRing { LeaveCriticalSection(&cs); return got; } + + /* Drop everything buffered. Used on a seek so pre-seek chunks can neither + * gate the ring (a backward seek leaves front chunks whose PTS is ahead of + * the target, which block the newer post-seek audio queued behind them) nor + * play out ahead of the post-seek audio that replaces them. */ + void flush() { + EnterCriticalSection(&cs); + head = 0; tail = 0; + chead = 0; ccount = 0; + playedUs = INT64_MIN; + LeaveCriticalSection(&cs); + } }; /* ---- decoder ------------------------------------------------------------ */ @@ -346,6 +358,25 @@ struct basis_decoder { * target forward so samples released now come due exactly when they reach * the speaker. */ volatile LONG audLatencyUs = 60000; + + /* Seek notification. basis_decoder_seek bumps seekGen (+ latches the target) + * on the caller thread. Each consumer leg keeps its own last-seen copy and, + * when it differs, flushes its stale buffers and re-anchors on ITS OWN thread: + * the audio-submit (demux) thread flushes the PCM ring + the MF/Opus decoder; + * the video-submit (demux) thread flushes the video MFT (drops its reorder + * buffer so retained pre-seek frames can't repopulate the ring) and clears the + * frame ring — it owns vdec and writes the ring; the render thread re-anchors + * the present clock and also clears the ring so a stale frame can't present in + * the window before the next video AU arrives. Nothing is touched across threads. */ + volatile LONG seekGen = 0; + volatile LONG64 seekTargetUs = 0; + LONG audioSeekGen = 0; /* audio-submit (demux) thread only */ + LONG videoSeekGen = 0; /* video-submit (demux) thread only */ + LONG renderSeekGen = 0; /* render thread only */ + volatile LONG videoSeekAck = 0; /* demux publishes seekGen here once it has flushed + * vdec + dropped pre-seek frames; the render leg + * holds until it matches so it neither anchors to a + * stale frame nor races the producer's ring clear */ }; /* ---- D3D / MF helpers --------------------------------------------------- */ @@ -973,6 +1004,7 @@ static void drain_video(basis_decoder* d) { * process share one resolved table. A missing library or symbol degrades to * muted audio (the format is rejected), never a crash. */ #define OPUS_SET_GAIN_REQUEST 4034 +#define OPUS_RESET_STATE 4028 typedef struct OpusDecoder OpusDecoder; typedef struct OpusMSDecoder OpusMSDecoder; struct opus_api { @@ -1461,6 +1493,22 @@ extern "C" int basis_decoder_submit_video(basis_decoder_t* d, const uint8_t* ann /* Bound len here, before the AV1 configOBU concatenation below adds to it — so * the total can't overflow int or drive an oversized allocation. */ if (!d || !d->vdec || !annexb || len <= 0 || len > BASIS_MAX_INPUT_SAMPLE) return -1; + /* First video AU after a seek: flush the MFT so its reorder buffer can't emit + * retained pre-seek frames into the ring, and drop the frames already in the + * ring. Demux thread owns vdec and writes the ring (drain_video below), so both + * are safe here; ring slots are aligned int64, cleared the same lock-free way + * they're written. */ + LONG svg = InterlockedCompareExchange(&d->seekGen, 0, 0); + if (svg != d->videoSeekGen) { + d->videoSeekGen = svg; + d->vdec->ProcessMessage(MFT_MESSAGE_COMMAND_FLUSH, 0); + for (int i = 0; i < basis_decoder::RING; ++i) d->ringPts[i] = INT64_MIN; + /* The ring is this (demux) thread's to clear — do it here only, then publish + * the generation so the render leg knows the pre-seek frames are gone. That + * keeps a single writer of the ring on seek and stops the render leg from + * clearing frames this thread may already have repopulated. */ + InterlockedExchange(&d->videoSeekAck, svg); + } IMFSample* s; bool carried_config = false; if (d->vConfigObusLen > 0) { @@ -1620,6 +1668,26 @@ static void submit_opus(basis_decoder* d, const uint8_t* data, int len, int64_t extern "C" 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 (BUG: multi-second post-seek silence), and flush + * the MF decoder so it doesn't overlap-add across the discontinuity. Runs on + * the demux thread, which is the only thread that touches `adec`. */ + LONG sg = InterlockedCompareExchange(&d->seekGen, 0, 0); + if (sg != d->audioSeekGen) { + d->audioSeekGen = sg; + d->pcm.flush(); + if (d->adec) d->adec->ProcessMessage(MFT_MESSAGE_COMMAND_FLUSH, 0); + /* Opus bypasses the MFT; reset its predictive/history state so the first + * post-seek packet doesn't decode against the pre-seek timeline. */ + if (d->opusDec) { + if (d->opusIsMS) { if (g_opus.ms_ctl) g_opus.ms_ctl((OpusMSDecoder*)d->opusDec, OPUS_RESET_STATE); } + else { if (g_opus.dec_ctl) g_opus.dec_ctl((OpusDecoder*)d->opusDec, OPUS_RESET_STATE); } + } + /* Seed the no-timestamp fallback from this AU so post-seek chunks land on + * the target timeline; 0 would put them at the start and the serve gate + * would trim or mis-time them. */ + d->aPtsFallback = pts_us; + } if (d->acodec == BASIS_CODEC_LPCM) { submit_lpcm(d, data, len, pts_us); return 0; } if (d->acodec == BASIS_CODEC_OPUS) { submit_opus(d, data, len, pts_us); return 0; } if (!d->adec) return -1; @@ -1678,6 +1746,33 @@ extern "C" int basis_decoder_render_update(basis_decoder_t* d) { LARGE_INTEGER nowq; QueryPerformanceCounter(&nowq); EnterCriticalSection(&d->presentLock); + /* First render after a seek: re-anchor the present clock to the first post-seek + * frame instead of the stale `newest` — without a re-anchor the clock stays + * clamped and freezes until a post-seek frame arrives (a ~18s video hang on a + * cold forward seek). The ring is cleared by the demux thread that owns it (see + * submit_video); this leg only re-anchors, then waits on that clear before + * proceeding so it never anchors to a stale frame or races the producer. */ + { + LONG sg = InterlockedCompareExchange(&d->seekGen, 0, 0); + if (sg != d->renderSeekGen) { + d->renderSeekGen = sg; + d->clockStarted = false; + d->primeStartQpc = 0; + d->lastPresentedPts = INT64_MIN; + d->videoBasePts = INT64_MIN; + /* Report the target now so get_position_us tracks before the first + * post-seek frame presents; render overwrites it once it does. */ + InterlockedExchange64(&d->presentedPosUs, InterlockedCompareExchange64(&d->seekTargetUs, 0, 0)); + } + /* Hold until the demux thread has flushed vdec and dropped the pre-seek + * frames. The prime/anchor path below then re-locks to the first post-seek + * frame the producer writes. */ + if (InterlockedCompareExchange(&d->videoSeekAck, 0, 0) != sg) { + LeaveCriticalSection(&d->presentLock); + return 0; + } + } + /* newest available PTS in the ring */ int64_t newest = INT64_MIN; for (int i = 0; i < basis_decoder::RING; ++i) if (d->ringPts[i] > newest) newest = d->ringPts[i]; @@ -1993,6 +2088,21 @@ extern "C" int basis_decoder_get_video_size(basis_decoder_t* d, int* w, int* h) if (w) *w = d->sharedW; if (h) *h = d->sharedH; return 0; } extern "C" int basis_decoder_get_frame_origin(basis_decoder_t* d) { return d ? (int)d->frameTopLeft : 0; } +extern "C" 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. pcm.flush() is cs-guarded, + * safe from this (caller) thread; the codec-state reset stays on the submit + * thread where the MFT/Opus decoder is owned. */ + d->pcm.flush(); + /* Latch target before bumping the generation so any leg that observes the new + * generation reads the matching target. presentedPosUs is set here too so the + * seek bar snaps to the target immediately, before a frame is presented. */ + InterlockedExchange64(&d->seekTargetUs, target_us); + InterlockedExchange64(&d->presentedPosUs, target_us); + InterlockedIncrement(&d->seekGen); +} + extern "C" int64_t basis_decoder_get_position_us(basis_decoder_t* d) { if (!d) return -1; /* Presentation position once a frame has shown; decode-side before that diff --git a/Basis/Packages/com.basis.mediaplayer/Plugins/Android/arm64-v8a/libbasis_media_native.so b/Basis/Packages/com.basis.mediaplayer/Plugins/Android/arm64-v8a/libbasis_media_native.so index 0fc2573c5..f8f84ec1f 100644 Binary files a/Basis/Packages/com.basis.mediaplayer/Plugins/Android/arm64-v8a/libbasis_media_native.so and b/Basis/Packages/com.basis.mediaplayer/Plugins/Android/arm64-v8a/libbasis_media_native.so differ diff --git a/Basis/Packages/com.basis.mediaplayer/Plugins/Windows/x86_64/basis_media_native.dll b/Basis/Packages/com.basis.mediaplayer/Plugins/Windows/x86_64/basis_media_native.dll index 2b3f7c621..63db1dce4 100644 Binary files a/Basis/Packages/com.basis.mediaplayer/Plugins/Windows/x86_64/basis_media_native.dll and b/Basis/Packages/com.basis.mediaplayer/Plugins/Windows/x86_64/basis_media_native.dll differ