Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
698edd9
fix(mediaplayer): bound RTSP auth base64 and cap RTP reassembly
towneh Jul 17, 2026
46fbdf1
fix(mediaplayer): re-check HLS sub-resource URLs against the SSRF guard
towneh Jul 17, 2026
fb0cf28
fix(mediaplayer): cap MP4 box-tree recursion depth
towneh Jul 17, 2026
157726d
fix(mediaplayer): cap MPEG-TS PES accumulation
towneh Jul 17, 2026
2de03a0
fix(mediaplayer): lock the Android Vulkan output-texture handoff
towneh Jul 17, 2026
e1c6451
fix(mediaplayer): guard render events against engine teardown
towneh Jul 17, 2026
ad40b02
fix(mediaplayer): bounds-check RTMP AMF _result parsing
towneh Jul 17, 2026
f2136e7
fix(mediaplayer): check Media Foundation allocations in the Windows d…
towneh Jul 17, 2026
5758b18
fix(mediaplayer): null-check Android MediaCodec input buffers
towneh Jul 17, 2026
ff3465f
test(mediaplayer): fuzz the URL, HLS, RTSP and RTMP parsers
towneh Jul 17, 2026
3611511
docs(mediaplayer): document native SSRF block and protocol fuzz coverage
towneh Jul 17, 2026
49d408d
chore(mediaplayer): rebuild native plugin binaries
towneh Jul 17, 2026
bf8bd6e
chore(mediaplayer): retire the self-hosted test-stream stack
towneh Jul 17, 2026
6bb70b9
docs(mediaplayer): freshness pass on TESTING.md
towneh Jul 17, 2026
ec472c4
docs(mediaplayer): use public/CC sources and ffmpeg recipes for codec…
towneh Jul 17, 2026
b9be97b
docs(mediaplayer): name CC dialogue sources for A/V-sync testing
towneh Jul 17, 2026
4e5315e
docs(mediaplayer): point the 4K row at Sintel 4K on media.xiph.org
towneh Jul 17, 2026
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 @@ -508,6 +508,7 @@ static void feed_extractor_sample(basis_decoder_t* d, AMediaCodec* codec, int tr
if (ii < 0) return;
size_t cap = 0;
uint8_t* buf = AMediaCodec_getInputBuffer(codec, ii, &cap);
if (!buf) { AMediaCodec_queueInputBuffer(codec, ii, 0, 0, 0, 0); return; }
ssize_t sz = AMediaExtractor_readSampleData(d->extractor, buf, cap);
if (sz < 0) {
AMediaCodec_queueInputBuffer(codec, ii, 0, 0, 0, AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM);
Expand Down Expand Up @@ -858,17 +859,23 @@ 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) return -1;
if (!d || !d->vcodec || !annexb || len <= 0) return -1;
int rc = -1;
ssize_t ii = AMediaCodec_dequeueInputBuffer(d->vcodec, 2000);
if (ii >= 0) {
size_t cap = 0;
uint8_t* buf = AMediaCodec_getInputBuffer(d->vcodec, ii, &cap);
int n = len < (int)cap ? len : (int)cap;
memcpy(buf, annexb, n);
AMediaCodec_queueInputBuffer(d->vcodec, ii, 0, n, pts_us, 0);
uint8_t* buf = AMediaCodec_getInputBuffer(d->vcodec, ii, &cap); /* size_t: no sign-cast */
if (buf && (size_t)len <= cap) {
memcpy(buf, annexb, (size_t)len);
rc = (AMediaCodec_queueInputBuffer(d->vcodec, ii, 0, (size_t)len, pts_us, 0) == AMEDIA_OK) ? 0 : -1;
} else {
/* NULL buffer or the AU doesn't fit: never queue a partial frame — it
* decodes to corruption. Release the slot empty and report the drop. */
AMediaCodec_queueInputBuffer(d->vcodec, ii, 0, 0, pts_us, 0);
}
}
drain_video_output(d);
return 0;
return rc;
}

/* Source-order -> WAVE-order channel map for the Blu-ray HDMV LPCM
Expand Down Expand Up @@ -928,21 +935,22 @@ int basis_decoder_submit_audio(basis_decoder_t* d, const uint8_t* data, int len,
if (!d || !data || len <= 0) return -1;
if (d->ac == BASIS_CODEC_LPCM) { submit_lpcm(d, data, len, pts_us); return 0; }
if (!d->acodec) return -1;
int rc = -1;
ssize_t ii = AMediaCodec_dequeueInputBuffer(d->acodec, 2000);
if (ii >= 0) {
size_t cap = 0;
uint8_t* buf = AMediaCodec_getInputBuffer(d->acodec, ii, &cap);
if ((size_t)len <= cap) {
if (buf && (size_t)len <= cap) {
memcpy(buf, data, (size_t)len);
AMediaCodec_queueInputBuffer(d->acodec, ii, 0, len, pts_us, 0);
rc = (AMediaCodec_queueInputBuffer(d->acodec, ii, 0, len, pts_us, 0) == AMEDIA_OK) ? 0 : -1;
} else {
/* Never feed a partial frame — it decodes to an error + silence.
* max-input-size should prevent this; return the buffer empty if not. */
AMediaCodec_queueInputBuffer(d->acodec, ii, 0, 0, pts_us, 0);
}
}
drain_audio_output(d);
return 0;
return rc;
}

/* ---- render thread + accessors ----------------------------------------- */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,12 @@ struct basis_vk_present {
* the YCbCr->RGB render pass, and rebuild that when Unity rotates the
* underlying VkImage (rare). */
void* unityNativeTex;
VkImage cachedUnityImage; /* the VkImage AccessTexture returned for unityNativeTex */
int unityDirty; /* handle changed on the main thread; the render thread drops the fbo */
VkImage cachedUnityImage; /* the VkImage AccessTexture returned for unityNativeTex (render thread) */
VkImageView unityImageView;
VkFramebuffer fbo;
int unityW, unityH;
int fboW, fboH; /* extent the fbo was built with — render-thread-owned, distinct from unityW/H */
int unityW, unityH; /* C#-registered RenderTexture size; written by the setter under v->lock */
int unityFormat; /* VkFormat returned by AccessTexture (UNORM or SRGB) */

uint64_t frameCounter;
Expand Down Expand Up @@ -407,7 +409,7 @@ static void destroy_unity_fbo(basis_vk_present* v) {
* or rotated under us). The image itself is OWNED BY UNITY — we never destroy
* it, only the view and framebuffer we created on top. */
static int ensure_unity_fbo(basis_vk_present* v, VkImage image, VkFormat format, int w, int h) {
if (v->fbo && v->cachedUnityImage == image && v->unityW == w && v->unityH == h) return 0;
if (v->fbo && v->cachedUnityImage == image && v->fboW == w && v->fboH == h) return 0;
destroy_unity_fbo(v);

VkImageViewCreateInfo vci = { VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
Expand All @@ -434,24 +436,28 @@ static int ensure_unity_fbo(basis_vk_present* v, VkImage image, VkFormat format,
if (vkCreateFramebuffer(v->device, &fci, NULL, &v->fbo) != VK_SUCCESS) return -1;

v->cachedUnityImage = image;
v->unityW = w;
v->unityH = h;
v->fboW = w;
v->fboH = h;
v->unityFormat = (int)format;
return 0;
}

void basis_vk_set_output_texture(basis_vk_present* v, void* native_texture, int w, int h) {
if (!v) return;
/* If the handle changed, drop the cached framebuffer; next render_update will
* rebuild against the new Unity image. We intentionally don't touch
* cachedUnityImage here because AccessTexture may legitimately return the
* same VkImage for a recreated RenderTexture if Unity reused the slot. */
if (v->unityNativeTex != native_texture) {
destroy_unity_fbo(v);
/* Runs on the main thread while the render thread may be mid-present using the
* framebuffer/image-view. Don't destroy them here — just record the change
* under the lock and let render_update drop+rebuild the fbo on its own thread
* (unityDirty). We intentionally don't touch cachedUnityImage because
* AccessTexture may legitimately return the same VkImage for a recreated
* RenderTexture if Unity reused the slot. */
pthread_mutex_lock(&v->lock);
if (v->unityNativeTex != native_texture || v->unityW != w || v->unityH != h) {
v->unityNativeTex = native_texture;
v->unityDirty = 1; /* a same-handle resize still needs the fbo rebuilt */
}
v->unityW = w;
v->unityH = h;
pthread_mutex_unlock(&v->lock);
}

/* ---- plugin-owned submission objects ------------------------------------ */
Expand Down Expand Up @@ -494,17 +500,31 @@ static int ensure_cmd_objects(basis_vk_present* v) {

int basis_vk_render_update(basis_vk_present* v) {
if (!v || !v->device || !v->getAHBProps) return 0;
/* Without a registered Unity output texture we have nowhere to render. C# is
* expected to call basis_media_set_output_texture once TryGetVideoSize
* returns a non-zero size; until then the demuxer's AHBs sit in v->pending. */
if (!v->unityNativeTex) return 0;

/* All Unity-registration state is read as one locked snapshot — no unlocked
* peek at unityNativeTex first. Without a registered output texture there is
* nowhere to render (C# calls basis_media_set_output_texture once
* TryGetVideoSize is non-zero; until then the demuxer's AHBs sit in pending). */
AHardwareBuffer* ahb = NULL; int w, h; float uv[4];
void* unityTex; int unityDirty; int regW, regH;
pthread_mutex_lock(&v->lock);
ahb = v->pending; v->pending = NULL; w = v->w; h = v->h;
unityTex = v->unityNativeTex; regW = v->unityW; regH = v->unityH;
unityDirty = v->unityDirty;
w = v->w; h = v->h;
uv[0] = v->uv[0]; uv[1] = v->uv[1]; uv[2] = v->uv[2]; uv[3] = v->uv[3];
/* Only detach the pending frame when there's a texture to render it into —
* otherwise it stays queued (the producer replaces + releases it) instead of
* being dropped here with its AHB reference leaked. */
if (unityTex) {
ahb = v->pending; v->pending = NULL;
if (ahb) v->unityDirty = 0; /* consume the dirty flag only when this pass rebuilds+renders */
}
pthread_mutex_unlock(&v->lock);
if (!unityTex) return 0;
if (!ahb) return 0;
/* Handle changed on the main thread: drop the old framebuffer/image-view here,
* on the render thread, so nothing is destroyed under a present in flight. */
if (unityDirty) destroy_unity_fbo(v);

/* AHB format + memory properties (drives the ycbcr conversion + allocation) */
VkAndroidHardwareBufferFormatPropertiesANDROID fmtProps = { VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_FORMAT_PROPERTIES_ANDROID };
Expand All @@ -520,16 +540,20 @@ int basis_vk_render_update(basis_vk_present* v) {
* it. The render pass takes the attachment from UNDEFINED and returns it
* SHADER_READ_ONLY, so no layout coordination with Unity is needed. */
uint64_t unityImageU64 = 0; int unityFormat = 0, unityW = 0, unityH = 0;
if (!basis_gfx_vk_access_texture(v->unityNativeTex,
if (!basis_gfx_vk_access_texture(unityTex,
&unityImageU64, &unityFormat, &unityW, &unityH)) {
AHardwareBuffer_release(ahb);
return 0;
}
VkImage unityImage = (VkImage)(uintptr_t)unityImageU64;

if (ensure_unity_fbo(v, unityImage, (VkFormat)unityFormat,
unityW > 0 ? unityW : w,
unityH > 0 ? unityH : h) != 0) {
/* One target extent for both the framebuffer and the render area, or the
* render pass area can disagree with the framebuffer it renders into. Prefer
* the RenderTexture's actual extent (AccessTexture), then the registered size,
* then the source AHB. */
int targetW = unityW > 0 ? unityW : (regW > 0 ? regW : w);
int targetH = unityH > 0 ? unityH : (regH > 0 ? regH : h);
if (ensure_unity_fbo(v, unityImage, (VkFormat)unityFormat, targetW, targetH) != 0) {
AHardwareBuffer_release(ahb);
return 0;
}
Expand Down Expand Up @@ -572,7 +596,7 @@ int basis_vk_render_update(basis_vk_present* v) {
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
0, 0, NULL, 0, NULL, 1, &toRead);

int rw = v->unityW, rh = v->unityH;
int rw = targetW, rh = targetH;
VkRenderPassBeginInfo rp = { VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO };
rp.renderPass = v->renderPass; rp.framebuffer = v->fbo;
rp.renderArea.extent.width = (uint32_t)rw; rp.renderArea.extent.height = (uint32_t)rh;
Expand Down
82 changes: 80 additions & 2 deletions Basis/Packages/com.basis.mediaplayer/Native~/basis_media_core.c
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,62 @@ int basis_engine_is_paused(basis_media_engine_t* e) { return e ? e->paused : 0;
int basis_engine_is_running(basis_media_engine_t* e) { return e ? e->running : 0; }
int basis_engine_is_paced(basis_media_engine_t* e) { return e ? e->paced : 0; }

/* ---- render-event liveness registry ------------------------------------
* OnRenderEvent (Unity render thread) is handed the engine pointer and can fire
* concurrently with basis_media_close on the main thread. C# quiesces render
* events before closing, but a stale event must be a safe no-op, not a
* use-after-free. Every open engine is registered here; basis_engine_render_event
* dispatches under g_registry_lock only while the engine is still registered, and
* close removes it under the same lock — waiting out any in-flight event — before
* it frees the decoder and engine.
*
* Engines are keyed by pointer, so this stops a dispatch against a freed engine but
* not the narrow ABA case where a delayed event's pointer matches a *new* engine
* that reused the freed address. For the shipping C# binding that is benign: it
* issues only RENDER_UPDATE (idempotent — republishes the current frame). But
* RENDER_RELEASE is part of the public render-event ABI, and a caller that delivers
* one across a close+reopen could tear down the reused engine's decoder — so this
* registry's ABA-safety is only as strong as "no RELEASE is delivered after close."
* Closing the window fully needs a generation-stamped handle in the event payload
* (a C# ABI change) — deliberately out of scope here. */
#define BASIS_MAX_ENGINES 64
static basis_mutex_t g_registry_lock;
static int g_registry_ready;
static basis_media_engine_t* g_engines[BASIS_MAX_ENGINES];

/* opens run on Unity's main thread, so first-use init needs no extra guard. */
static void registry_ensure(void) {
if (!g_registry_ready) { mutex_init(&g_registry_lock); g_registry_ready = 1; }
}
static int registry_add(basis_media_engine_t* e) {
registry_ensure();
int ok = 0;
mutex_lock(&g_registry_lock);
for (int i = 0; i < BASIS_MAX_ENGINES; ++i) if (!g_engines[i]) { g_engines[i] = e; ok = 1; break; }
mutex_unlock(&g_registry_lock);
return ok; /* 0 => registry full */
}
static void registry_remove(basis_media_engine_t* e) {
if (!g_registry_ready) return;
mutex_lock(&g_registry_lock);
for (int i = 0; i < BASIS_MAX_ENGINES; ++i) if (g_engines[i] == e) { g_engines[i] = NULL; break; }
mutex_unlock(&g_registry_lock);
}

void basis_engine_render_event(basis_media_engine_t* e, int event_id) {
if (!e || !g_registry_ready) return;
mutex_lock(&g_registry_lock);
int live = 0;
for (int i = 0; i < BASIS_MAX_ENGINES; ++i) if (g_engines[i] == e) { live = 1; break; }
/* Dispatch under the lock so registry_remove (in close) blocks until this
* returns — the decoder can't be freed while a render event is using it. */
if (live && e->decoder) {
if (event_id == BASIS_RENDER_UPDATE) basis_decoder_render_update(e->decoder);
else if (event_id == BASIS_RENDER_RELEASE) basis_decoder_render_release(e->decoder);
}
mutex_unlock(&g_registry_lock);
}

/* Real-time delivery pacing. Blocks the demux thread so an access unit is handed to the
* decoder no more than BASIS_PACE_LEAD_US ahead of a fixed 1x clock anchored to the first
* AU — stalling the socket read (TCP backpressure) so a faster-than-real-time source can't
Expand Down Expand Up @@ -1177,6 +1233,22 @@ static basis_media_engine_t* open_impl(const char* url, const char* audio_url, i
}
if (has_audio) e->audio_thread_started = 1;

/* Live now: the pointer is about to reach C#, which may issue render events.
* Registered last so no partially-built engine is ever visible to a dispatch.
* If the registry is full (too many concurrent players), fail cleanly rather
* than hand back an engine whose render events would be silently ignored. */
if (!registry_add(e)) {
e->running = 0;
thread_join(e);
audio_thread_join(e);
basis_decoder_destroy(e->decoder);
basis_io_global_shutdown();
basis_caption_destroy(e->captions);
mutex_destroy(&e->submit_lock);
mutex_destroy(&e->lock);
free(e);
return NULL;
}
return e;
}

Expand All @@ -1195,8 +1267,14 @@ BASIS_API basis_media_engine_t* BASIS_CALL basis_media_open_dual(const char* vid
BASIS_API void BASIS_CALL basis_media_close(basis_media_engine_t* e) {
if (!e) return;

/* Stop the demux threads first so nothing submits while we tear down. Both
* legs observe the same running flag; join both before freeing the decoder. */
/* Deregister first, before anything is torn down: this blocks until any
* in-flight render event returns and makes every later one a no-op, so no
* render callback can touch the decoder while the demux threads are still
* exiting or the decoder is being freed. */
registry_remove(e);

/* Stop the demux threads so nothing submits while we tear down. Both legs
* observe the same running flag; join both before freeing the decoder. */
e->running = 0;
thread_join(e);
audio_thread_join(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,12 @@ void basis_engine_set_state(basis_media_engine_t* engine, basis_media_sta
void basis_engine_set_error(basis_media_engine_t* engine, const char* message);
basis_decoder_t* basis_engine_get_decoder(basis_media_engine_t* engine);

/* Render-thread entry point (the Unity plugin's OnRenderEvent forwards here). The
* engine pointer comes from Unity and can arrive after basis_media_close has freed
* it; this checks a liveness registry under a lock and no-ops on a stale engine,
* so a late render event can't use-after-free. event_id is a BASIS_RENDER_* value. */
void basis_engine_render_event(basis_media_engine_t* engine, int event_id);

/* Consulted by the platform backend: paused freezes video publishing and mutes
* audio reads; running going to 0 tells decode/demux loops to unwind. */
int basis_engine_is_paused(basis_media_engine_t* engine);
Expand Down
Loading
Loading