Skip to content

Commit fe94a04

Browse files
authored
fix(mediaplayer): re-anchor the present clock on seek (+ Ycbcr filter gating, HLS leak fix) (#971)
## Summary Fixes a batch of media-player seek and playback regressions found in a full pass across the Unity Editor, Windows standalone, and Quest Pro. The main one: a seek repositioned the demuxer but never told the decoder, so the presentation clock stayed clamped to the stale decode edge and the picture froze until a post-seek frame happened to arrive (up to ~18s on a cold forward seek). This adds a decoder-side seek — `basis_decoder_seek` flushes the decoder and the PCM ring and re-anchors the present clock to the target. The render leg no longer clears the frame ring itself; it waits on a small generation ack the demux thread (which owns the ring) publishes after it has flushed, so the two threads don't both reset the ring and the render leg can never erase a post-seek frame the demux just wrote. Verified on Quest and in the Windows Editor: video re-anchors straight to the target, no freeze, no stale pre-seek frames, backward and forward. Also in here: - **Android Vulkan Y'CbCr sampler** — gate the chroma and sampler filters on the format's advertised feature bits instead of forcing `VK_FILTER_LINEAR` unconditionally. Formats that only advertise nearest (some UBWC external formats on Adreno) were getting linear forced on them, which is undefined. - **HLS** — free the VOD playlist metadata (`vod_uri` / `vod_dur_ms`) on the two open-failure paths; they were leaked. - **Review round** — flush the video MFT on the video-submit thread (its reorder buffer could otherwise re-emit pre-seek frames), reset the Opus decoder state on seek, split the chroma vs sampler filter capability checks so linear luma isn't dropped when the format supports it, flush queued PCM at the seek call so the audio callback stops serving pre-seek samples immediately, and keep the frame-ring reset on the owning thread. Windows x64 and Android arm64 native binaries are rebuilt from this source and included. ### Known follow-ups (tracked, not in this PR) The clock fix is correct, but re-anchoring to the true target surfaced a separate weakness the old frozen clock had been masking. Flagging these so reviewers know the shape of what's left: - **Post-seek audio recovery lags.** Video re-anchors immediately; audio catches up a few seconds later on a large backward seek (the shared delivery-pacing anchor holds audio whose timestamp sits ahead of the video-set base). Same on both backends; being handled as a focused follow-up. - **HLS VOD seek into the final segment snaps forward.** Once the producer has fetched every segment it rejects further seeks; pre-existing. The proper fix is to let the producer idle and revive on a seek rather than exit. - **Quest HE-AAC startup pop.** An implicit-SBR stream brings the AudioTrack up at the 24 kHz core rate before it corrects to 48 kHz, audible on an RTSP live-join. Separate change. ## Required checks This is a native C/C++ change to the media-player plugin (decode / demux / present); it touches no C# and none of the Unity per-frame, component, Addressables, camera, logging, or event-driver surfaces the checks below cover. Ticked as N/A per the template, with the substantive one (Tested) genuinely done. <!-- required-checks-start --> <!-- Tick the boxes in place — do not edit the line text. The pr-checklist workflow parses this block; per-PR context goes under Notes. --> - [x] **Tested** — I built and ran this locally. The change works in the editor and (where relevant) in a built player. - [x] **Transform access is combined and limited** — In hot paths, transform reads/writes go through `TransformAccessArray` or are otherwise batched. I have not added per-frame `transform.position` / `transform.rotation` / `transform.localPosition` calls inside loops. Whenever I need both position and rotation, I use the combined APIs — `SetPositionAndRotation` / `SetLocalPositionAndRotation` for writes, `GetPositionAndRotation` / `GetLocalPositionAndRotation` for reads — instead of two separate property accesses; the combined call does one local-to-world matrix traversal instead of two. - [x] **Addressables used for asset/memory loading** — Any new asset loads go through Addressables. No new `Resources.Load`, no direct asset references that pull large content into memory on scene load. - [x] **No new `GetComponent` / `AddComponent` where avoidable** — Where unavoidable, the result is cached on a field, and any `GetComponent<T>` is replaced with `TryGetComponent<T>(out var x)` — bare `GetComponent` will be denied. `TryGetComponent` is the modern API (Unity 2019.2+) and skips the Editor-only GC allocation `GetComponent` causes when a component is missing: Unity wraps the `null` return in a managed "fake null" object so its overloaded `==` operator can still detect destroyed C++ objects, and constructing that wrapper allocates; `TryGetComponent` returns a `bool` plus `out` parameter and never builds the wrapper. None of these calls run inside `Update`, `LateUpdate`, `FixedUpdate`, jobs, or other per-frame code paths. - [x] **Per-frame work is scheduled through `BasisEventDriver`** — Any new per-frame work hooks into `BasisEventDriver` rather than adding standalone `Update` / `LateUpdate` / `FixedUpdate` callbacks on a MonoBehaviour. - [x] **Anything added to `BasisEventDriver` is bulletproof, or guarded by `try`/`catch`** — `BasisEventDriver` runs the single per-frame tick that drives the whole framework (network apply, local player sim, blendshapes, JigglePhysics, nameplates, and more) as one sequential chain. An unhandled exception anywhere in that chain aborts the rest of the tick, so every step after the throwing one is silently skipped for that frame. New work added to the driver must either be guaranteed not to throw, or be wrapped in a `try`/`catch` that contains the failure and surfaces it through `BasisDebug` — logged once / rate-limited, never every frame (see the existing `HVRBasisBuiltInAddresses.Simulate()` guard for the pattern). Expect this to be scrutinized closely in review. - [x] **Considered jobification** — I asked whether this work can be moved to a Unity Job (Burst-compiled where possible). If it can, it is. If it cannot, the reason is in **Notes**. - [x] **No needless `{ get; set; }` properties or access lockdowns** — Public fields are fine; Basis is meant to be read and modified freely, so don't wall things off `private`/`internal` without a real reason. Don't wrap a field in `{ get; set; }` when the accessors do nothing — property accessors have a real performance cost vs direct field access, and the lead maintainer prefers plain fields (or a method / setter-only property when only the setter needs logic) over a noop-getter pair. For `.Instance` singletons, callers reassigning `Type.Instance` is allowed; if that would break your code, log a warning or throw — don't block the assignment. Locking down access is not your call. - [x] **Camera access goes through `BasisLocalCameraDriver`** — Code that needs the local camera (transform, projection, rig data, etc.) pulls it from `BasisLocalCameraDriver` rather than looking one up itself. Don't roll a separate camera discovery path. - [x] **Logging uses `BasisDebug`** — All new logging calls go through `BasisDebug.Log` / `BasisDebug.LogWarning` / `BasisDebug.LogError` (with an appropriate `LogTag`) instead of `UnityEngine.Debug.Log` / `Debug.LogWarning` / `Debug.LogError`. `BasisDebug` routes through Basis's tagged, color-coded logger and respects the project-wide `LoggingDisabled` toggle so logging can be killed at runtime; bare `Debug.Log` calls bypass that and will be denied. - [x] **No scene-wide discovery for dependencies** — New code is architected so it does not need `FindObjectOfType` / `FindObjectsOfType` / `GameObject.Find` / `FindGameObjectsWithTag` to locate what it depends on. References are wired in — registered through an existing manager/driver, injected at init, or passed in by the caller — rather than discovered by scanning the scene at runtime. If a scene scan is genuinely unavoidable, justify it under **Notes**. - [x] **No allocations in hot paths** — Per-frame code (Update / LateUpdate / FixedUpdate, simulation loops, jobs, anything called once per frame or more) does not allocate. No `new` on reference types, no LINQ, no `string` concatenation/interpolation, no boxing, no `foreach` over interface-typed collections. Allocate once at init and reuse the buffer. - [x] **No debugging in hot paths** — No log calls of any kind on per-frame paths, including `BasisDebug`. Hot-path logging floods the console and incurs cost on every frame regardless of whether the message is filtered out downstream. If a hot-path log is needed while iterating, gate it behind `#if UNITY_EDITOR` and remove (or leave gated) before merge. - [x] **Hot-path collection access is optimized** — Cache `.Count` (lists) / `.Length` (arrays) into a local `int` before the loop instead of re-reading the property each iteration. Prefer `T[]` (with a separate length int when the array is over-sized) over `List<T>` where the data is hot — Unity's mono BCL doesn't expose `CollectionsMarshal.AsSpan(List<T>)`, so a list can't be fed into `Span<T>` / unsafe paths cleanly. Where the perf justifies it, drop into `Span<T>` / `ref` locals / `Unsafe.As` / `unsafe` pointer code to skip bounds checks and copies, and call out the invariants you're relying on under **Notes** so reviewers can sanity-check them. <!-- required-checks-end --> ## Testing details - [x] Windows - [ ] Linux - [x] Android - [ ] iOS - [ ] macOS Input / control mode coverage: - [x] Tested in VR (note headset under **Notes**) - [x] Tested in desktop / non-VR mode - [ ] Tested with phone controls (mobile touch input) - [ ] N/A — change does not touch player/XR/input code Where applicable, confirm these flows still work after your changes: - [ ] Hot-switching (desktop ↔ VR mode swap at runtime) - [ ] Avatar swapping - [ ] Server swapping (joining / leaving / changing servers) - [x] N/A — change does not touch any of the above ## Notes Native plugin change only — no C# — so the Unity-pattern required checks above are N/A and ticked as such per the template. Tested on **Quest Pro** (Android arm64, VR) and the **Windows Editor** (Direct3D). Seek was exercised both directions on progressive MP4, integrated fMP4 (sidx), and TS-segmented HLS; playback verified on VP9/WebM, Opus, MP3, and AAC 5.1. The seek clock fix was corroborated against the diagnostics CSV on both platforms (video position re-anchors to the target immediately). The audio-recovery follow-up above is the same CSV showing audio catching up a few seconds behind on a large backward seek.
2 parents 6f9a0e6 + 3aa7012 commit fe94a04

8 files changed

Lines changed: 240 additions & 5 deletions

File tree

Basis/Packages/com.basis.mediaplayer/Native~/android/basis_android_decode.c

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,17 @@ static int ring_fill_ms(pcm_ring* r) {
186186
return (int)((int64_t)frames * 1000 / srr);
187187
}
188188

189+
/* Drop everything buffered — used on a seek so pre-seek chunks can neither gate
190+
* the ring (front chunks ahead of the target block the post-seek audio behind
191+
* them) nor play out ahead of the post-seek audio. Mirrors PcmRing::flush. */
192+
static void ring_flush(pcm_ring* r) {
193+
pthread_mutex_lock(&r->m);
194+
r->head = 0; r->tail = 0;
195+
r->chead = 0; r->ccount = 0;
196+
r->playedUs = INT64_MIN;
197+
pthread_mutex_unlock(&r->m);
198+
}
199+
189200
/* ---- video frame ring --------------------------------------------------- */
190201

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

273+
/* Seek notification (mirrors basis_win_decode.cpp). basis_decoder_seek bumps
274+
* seekGen (+ latches the target) on the caller thread. Each leg keeps its own
275+
* last-seen copy and flushes on ITS OWN thread: the audio submit (demux) thread
276+
* flushes the PCM ring + AAC codec; the video submit (demux) thread flushes the
277+
* video codec + frame ring (it owns vcodec and writes vimg); the render thread
278+
* re-anchors the present clock. seekGen/seekTargetUs are cross-thread (atomics). */
279+
int seekGen; /* atomic */
280+
int64_t seekTargetUs; /* atomic */
281+
int audioSeekGen; /* audio-submit (demux) thread only */
282+
int videoSeekGen; /* video-submit (demux) thread only */
283+
int renderSeekGen; /* render thread only */
284+
int videoSeekAck; /* atomic: demux publishes seekGen once it has flushed the
285+
* codec + released pre-seek frames; the render leg holds
286+
* until it matches so it neither anchors to nor deletes a
287+
* post-seek frame the producer has already enqueued */
288+
262289
/* debug counters */
263290
long dbg_render, dbg_nodue, dbg_acqfail, dbg_drop, dbg_lagms;
264291
};
@@ -860,6 +887,22 @@ int basis_decoder_set_audio_format(basis_decoder_t* d, basis_codec_t codec,
860887
int basis_decoder_submit_video(basis_decoder_t* d, const uint8_t* annexb, int len, int64_t pts_us, int key) {
861888
(void)key;
862889
if (!d || !d->vcodec || !annexb || len <= 0) return -1;
890+
/* First video AU after a seek: flush the codec and release the pre-seek frames
891+
* in the ring so they can't present ahead of the post-seek content. Demux thread
892+
* owns vcodec and writes vimg (drain_video_output below), so both are safe here;
893+
* the frame release mirrors the shutdown path (AImage_delete under vm). */
894+
int svg = __atomic_load_n(&d->seekGen, __ATOMIC_ACQUIRE);
895+
if (svg != d->videoSeekGen) {
896+
d->videoSeekGen = svg;
897+
AMediaCodec_flush(d->vcodec);
898+
pthread_mutex_lock(&d->vm);
899+
for (int i = 0; i < VRING; ++i) if (d->vimg[i]) { AImage_delete(d->vimg[i]); d->vimg[i] = NULL; }
900+
pthread_mutex_unlock(&d->vm);
901+
/* Publish the generation so the render leg knows the pre-seek frames are gone
902+
* and it can re-anchor. Releasing frames on this (owning) thread only stops
903+
* the render leg from deleting post-seek frames drain_video_output re-enqueues. */
904+
__atomic_store_n(&d->videoSeekAck, svg, __ATOMIC_RELEASE);
905+
}
863906
int rc = -1;
864907
ssize_t ii = AMediaCodec_dequeueInputBuffer(d->vcodec, 2000);
865908
if (ii >= 0) {
@@ -933,6 +976,15 @@ static void submit_lpcm(basis_decoder_t* d, const uint8_t* p, int len, int64_t p
933976

934977
int basis_decoder_submit_audio(basis_decoder_t* d, const uint8_t* data, int len, int64_t pts_us) {
935978
if (!d || !data || len <= 0) return -1;
979+
/* First audio AU after a seek: drop the stale pre-seek ring so this post-seek
980+
* audio serves immediately, and flush the AAC codec so it doesn't overlap-add
981+
* across the discontinuity. Demux thread, the only thread that touches acodec. */
982+
int sg = __atomic_load_n(&d->seekGen, __ATOMIC_ACQUIRE);
983+
if (sg != d->audioSeekGen) {
984+
d->audioSeekGen = sg;
985+
ring_flush(&d->pcm);
986+
if (d->acodec) AMediaCodec_flush(d->acodec);
987+
}
936988
if (d->ac == BASIS_CODEC_LPCM) { submit_lpcm(d, data, len, pts_us); return 0; }
937989
if (!d->acodec) return -1;
938990
int rc = -1;
@@ -1143,7 +1195,29 @@ static void present_select(basis_decoder_t* d) {
11431195
int basis_decoder_render_update(basis_decoder_t* d) {
11441196
if (!d || !d->vk) return -1;
11451197
if (basis_engine_is_paused(d->engine)) return 0;
1146-
present_select(d);
1198+
/* First render after a seek: reset the present clock so present_select re-locks
1199+
* it to the first post-seek frame instead of staying clamped to the stale decode
1200+
* edge (a video/clock freeze on a cold forward seek). Render-thread-owned clock;
1201+
* the frame ring is cleared by the demux thread that owns it (submit_video), and
1202+
* this leg waits on that clear (videoSeekAck) before selecting a frame. */
1203+
int rsg = __atomic_load_n(&d->seekGen, __ATOMIC_ACQUIRE);
1204+
if (rsg != d->renderSeekGen) {
1205+
d->renderSeekGen = rsg;
1206+
d->clockStarted = 0;
1207+
d->primeStartUs = 0;
1208+
d->lastPresentedPts = INT64_MIN;
1209+
d->mediaStartUs = 0;
1210+
__atomic_store_n(&d->presentedPosUs,
1211+
__atomic_load_n(&d->seekTargetUs, __ATOMIC_ACQUIRE), __ATOMIC_RELAXED);
1212+
}
1213+
/* Hold frame selection until the demux thread has flushed the codec and released
1214+
* the pre-seek frames. Selecting before then would show a stale frame, and
1215+
* releasing them here would race the producer and could delete post-seek frames
1216+
* it has already enqueued (notably when seeking while paused). Keep rendering so
1217+
* the last frame stays up during the hold. */
1218+
if (__atomic_load_n(&d->videoSeekAck, __ATOMIC_ACQUIRE) == rsg) {
1219+
present_select(d);
1220+
}
11471221
return basis_vk_render_update(d->vk);
11481222
}
11491223
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) {
11701244
int basis_decoder_get_frame_origin(basis_decoder_t* d) { (void)d; return 0; }
11711245
/* Presentation position once a frame has shown; decode edge before that
11721246
* (start-up, audio-only) so early consumers still see the clock move. */
1247+
void basis_decoder_seek(basis_decoder_t* d, int64_t target_us) {
1248+
if (!d) return;
1249+
/* Drop any pre-seek PCM still queued so the audio callback stops serving it
1250+
* immediately rather than up to the next audio AU. ring_flush takes the pcm
1251+
* mutex, safe from this (caller) thread; the codec reset stays on the submit
1252+
* thread where the decoder is owned. */
1253+
ring_flush(&d->pcm);
1254+
/* Latch the target before bumping the generation so any leg that sees the new
1255+
* generation reads the matching target. presentedPosUs is set here so the seek
1256+
* bar snaps to the target immediately, before a post-seek frame presents. */
1257+
__atomic_store_n(&d->seekTargetUs, target_us, __ATOMIC_RELEASE);
1258+
__atomic_store_n(&d->presentedPosUs, target_us, __ATOMIC_RELAXED);
1259+
__atomic_add_fetch(&d->seekGen, 1, __ATOMIC_RELEASE);
1260+
}
1261+
11731262
int64_t basis_decoder_get_position_us(basis_decoder_t* d) {
11741263
if (!d) return -1;
11751264
int64_t presented = __atomic_load_n(&d->presentedPosUs, __ATOMIC_RELAXED);

Basis/Packages/com.basis.mediaplayer/Native~/android/basis_android_vk.c

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,25 @@ static int ensure_format_objects(basis_vk_present* v, uint64_t externalFormat,
269269
/* Y'CbCr conversion described by the AHB external format. */
270270
VkExternalFormatANDROID extFmt = { VK_STRUCTURE_TYPE_EXTERNAL_FORMAT_ANDROID };
271271
extFmt.externalFormat = externalFormat;
272+
/* Linear filtering is only valid when the format advertises the matching
273+
* feature bit, and each filter has its own bit: chroma reconstruction needs
274+
* YCBCR_CONVERSION_LINEAR_FILTER, the ordinary sampler mag/min needs
275+
* SAMPLED_IMAGE_FILTER_LINEAR, and the two filters may only differ when
276+
* SEPARATE_RECONSTRUCTION_FILTER is present. Qualcomm's UBWC external formats
277+
* (VP9/AV1 on Adreno) frequently advertise none of these, and forcing LINEAR
278+
* there is undefined behaviour. Pick each filter from its own bit; when separate
279+
* reconstruction is absent, force them equal (downgrading chroma to nearest if
280+
* the sampler cannot do linear). */
281+
VkFormatFeatureFlags features = fmt->formatFeatures;
282+
int separate = (features & VK_FORMAT_FEATURE_SAMPLED_IMAGE_YCBCR_CONVERSION_SEPARATE_RECONSTRUCTION_FILTER_BIT) != 0;
283+
int linearSample = (features & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT) != 0;
284+
VkFilter yf = (features & VK_FORMAT_FEATURE_SAMPLED_IMAGE_YCBCR_CONVERSION_LINEAR_FILTER_BIT)
285+
? VK_FILTER_LINEAR : VK_FILTER_NEAREST;
286+
VkFilter samplerFilter = linearSample ? VK_FILTER_LINEAR : VK_FILTER_NEAREST;
287+
if (!separate) {
288+
if (yf == VK_FILTER_LINEAR && !linearSample) yf = VK_FILTER_NEAREST;
289+
samplerFilter = yf;
290+
}
272291
VkSamplerYcbcrConversionCreateInfo cy = { VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO };
273292
cy.pNext = &extFmt;
274293
cy.format = VK_FORMAT_UNDEFINED;
@@ -277,15 +296,15 @@ static int ensure_format_objects(basis_vk_present* v, uint64_t externalFormat,
277296
cy.components = fmt->samplerYcbcrConversionComponents;
278297
cy.xChromaOffset = fmt->suggestedXChromaOffset;
279298
cy.yChromaOffset = fmt->suggestedYChromaOffset;
280-
cy.chromaFilter = VK_FILTER_LINEAR;
299+
cy.chromaFilter = yf;
281300
if (vkCreateSamplerYcbcrConversion(v->device, &cy, NULL, &v->ycbcr) != VK_SUCCESS) return -1;
282301

283302
/* immutable sampler with the conversion attached */
284303
VkSamplerYcbcrConversionInfo cvInfo = { VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_INFO };
285304
cvInfo.conversion = v->ycbcr;
286305
VkSamplerCreateInfo sci = { VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO };
287306
sci.pNext = &cvInfo;
288-
sci.magFilter = VK_FILTER_LINEAR; sci.minFilter = VK_FILTER_LINEAR;
307+
sci.magFilter = samplerFilter; sci.minFilter = samplerFilter;
289308
sci.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
290309
sci.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
291310
sci.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;

Basis/Packages/com.basis.mediaplayer/Native~/basis_media_core.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1359,6 +1359,11 @@ BASIS_API int BASIS_CALL basis_media_seek_us(basis_media_engine_t* e, int64_t ta
13591359
* that boundary before the generation is visible. */
13601360
e->seek_target_us = target_us;
13611361
e->seek_seq++;
1362+
/* Notify the decoder to drop its pre-seek audio/video buffers and re-anchor
1363+
* the present clock to the target (each leg does it on its own thread). The
1364+
* demuxer only repositions the byte source; without this the decoder keeps
1365+
* serving stale buffers — post-seek audio silence and a frozen video clock. */
1366+
if (e->decoder) basis_decoder_seek(e->decoder, target_us);
13621367
void* hls = e->active_hls;
13631368
int rc = hls ? basis_hls_request_seek(hls, target_us / 1000) : 0;
13641369
mutex_unlock(&e->lock);

Basis/Packages/com.basis.mediaplayer/Native~/basis_media_internal.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,12 @@ int basis_decoder_try_open_url(basis_decoder_t* dec, const char* url);
165165
int basis_decoder_render_update(basis_decoder_t* dec);
166166
void basis_decoder_render_release(basis_decoder_t* dec);
167167

168+
/* Seek notification (any thread; typically the caller of basis_media_seek_us).
169+
* Sets a target + bumps a generation the decoder's consumer legs observe; each
170+
* leg flushes its own stale buffers and re-anchors the clock ON ITS OWN THREAD,
171+
* so nothing is flushed across threads. Idempotent per generation. */
172+
void basis_decoder_seek(basis_decoder_t* dec, int64_t target_us);
173+
168174
/* Accessors mirrored by the public ABI (any thread unless noted). */
169175
void* basis_decoder_get_texture(basis_decoder_t* dec, int* out_w, int* out_h);
170176
uint64_t basis_decoder_get_frame_counter(basis_decoder_t* dec);

Basis/Packages/com.basis.mediaplayer/Native~/protocol/basis_hls.c

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,11 @@ static void hls_mutex_unlock(hls_mutex_t* m) { pthread_mutex_unlock(m); }
5656
#define HLS_MAX_PLAYLIST (1 << 20) /* 1 MiB playlist cap */
5757
#define HLS_MAX_EMPTY_RELOADS 8 /* consecutive no-new-media reloads before giving up */
5858
#define HLS_LIVE_MARGIN_SEGMENTS 3 /* playout buffer kept behind the live edge for plain (non-LL) HLS */
59-
#define HLS_RING_CAP (4 * 1024 * 1024) /* read-ahead byte buffer (~5 s of 1080p HD) */
59+
/* Read-ahead byte buffer (~5 s of 1080p HD). Kept small on purpose: a larger
60+
* read-ahead makes the VOD producer reach the end of the playlist further ahead
61+
* of playout, which widens the window where a seek is rejected outright (the
62+
* producer_done path). One heap allocation per HLS stream. */
63+
#define HLS_RING_CAP (4 * 1024 * 1024)
6064

6165
/* (msn, part) media position. part == -1 means a whole segment. */
6266
typedef struct {
@@ -785,11 +789,13 @@ void* basis_hls_open(const char* url, const basis_http_provider_t* http,
785789
/* Start the read-ahead producer so segments buffer ahead of playout. */
786790
h->ring_cap = HLS_RING_CAP;
787791
h->ring = (uint8_t*)malloc((size_t)h->ring_cap);
788-
if (!h->ring) { free(h); return NULL; }
792+
if (!h->ring) { free(h->vod_uri); free(h->vod_dur_ms); free(h); return NULL; }
789793
hls_mutex_init(&h->lock);
790794
if (!hls_thread_start(h)) {
791795
hls_mutex_destroy(&h->lock);
792796
free(h->ring);
797+
free(h->vod_uri);
798+
free(h->vod_dur_ms);
793799
free(h);
794800
return NULL;
795801
}

0 commit comments

Comments
 (0)