feat(mediaplayer): split-stream, on-demand delivery, and optional page-URL resolution#871
Draft
towneh wants to merge 17 commits into
Draft
feat(mediaplayer): split-stream, on-demand delivery, and optional page-URL resolution#871towneh wants to merge 17 commits into
towneh wants to merge 17 commits into
Conversation
… streams Play a video-only stream and a separate audio-only stream in sync on one decoder and clock, for sources that deliver A/V as distinct streams (e.g. adaptive YouTube above ~360p: H.264 video-only + AAC audio-only). Native: basis_media_open_dual(video_url, audio_url) runs a second demux thread for the audio-only URL into the shared decoder. The protocol path is parameterised per pipeline so the second leg isn't hardcoded as "audio", and state/errors route through the sink so a subordinate leg suppresses them. A null/empty audio URL is exactly basis_media_open, so the single-stream path is unchanged. C#: BasisMediaSource gains AudioUri (+ AudioHeaders). The audio URL passes the same trust gate as the video URL and threads through to a new OpenWithAudio P/Invoke, which falls back to basis_media_open when no audio stream is set.
…tion Add a paced mode for on-demand sources that arrive faster than real time (e.g. CDN-delivered split streams), threaded as BasisMediaSource.Paced -> basis_media_open_dual(video, audio, paced). When set, the engine throttles delivery to ~1x and presents on a smooth clock held a fixed buffer behind the decode edge, rather than the live-edge clock that fast-forwards on bursty or faster-than-real-time delivery; a paced source also plays once and stops at end-of-file instead of looping. The live path (RTSP/RTMP/RIST/live HLS) is unchanged. Also select the HTTP demuxer by sniffing the leading bytes (ISO-BMFF box vs MPEG-TS sync) instead of the URL extension, so extensionless CDN URLs that deliver fragmented MP4 with no .mp4 in the path demux correctly rather than falling through to the MPEG-TS demuxer.
…d playback SelectSource picks the best H.264 video-only (<=1080p) + AAC audio-only formats and returns a split BasisMediaSource (Uri + AudioUri) with Paced = true, since adaptive YouTube above ~360p is delivered as separate streams faster than real time. Falls back to a single muxed/HLS stream (Twitch, live YouTube, progressive ~360p) when no split pair is available.
Paced HTTP sources now drain the network into a compressed byte ring on a dedicated reader thread, while the demuxer consumes from the ring at the paced 1x rate. This decouples delivery from decode: bursty CDN delivery (e.g. googlevideo ships ~2.5s chunks with ~1s gaps) is banked in the ring instead of starving the decoder, which previously paused-and-caught-up every few seconds. Compressed bytes are cheap (a 16MB ring holds many seconds), unlike the VRAM-bound decode ring. Read-ahead is paced-mode only; live sources read directly with no added latency.
BasisMediaUrlRouter lets an optional package (e.g. a yt-dlp-based resolver) install a URL resolver the player consults before loading, without the player core depending on it. IsDirectlyPlayable is the single classifier for whether the engine can open a URL directly (a transport scheme, or an http(s) URL whose path ends in a media-container extension) versus a page URL that needs resolving. It only steers; it never blocks. No consumer yet — wired next.
… URLs BasisMediaSource.Delivery (Auto/Live/OnDemand) selects the playback clock, and basis_media_open_dual's third argument carries it as a hint. Auto resolves at open: the engine treats a finite, byte-range-seekable HTTP body (known Content-Length + Accept-Ranges) or an HLS playlist with EXT-X-ENDLIST as on-demand and paces it; an open-ended HTTP response or a non-HTTP transport is live. A VOD source that arrives faster than real time is paced rather than fast-forwarding, without the caller having to flag it. BasisMediaPlayerStreaming routes its URL through BasisMediaUrlRouter: a page URL (no media extension) goes to an installed resolver, a direct stream loads straight through, and with no resolver installed a page URL reports that the resolver package is needed instead of failing silently. The yt-dlp integration registers its resolver into the seam and requests on-demand for the split adaptive pairs it selects; NeedsResolution defers to the player's IsDirectlyPlayable so the classification has one home. Includes the rebuilt Windows plugin.
…ution Bring the media-player README up to date: live vs on-demand and the Delivery field, split-stream playback via AudioUri, and the optional page-URL resolver seam (with the extensionless-HTTP steering gap called out). Add a README to the yt-dlp integration package covering what it resolves, requirements, and that removing it drops common-site resolution while direct streams keep working.
…er-ytdlp-resolution
LoadUrl now consults BasisMediaUrlRouter before loading, so every entry point — the in-game UI, BasisMediaPlayerNetworking, and the streaming example — steers a page URL (YouTube/Twitch/…) through an installed resolver, and loads a directly playable URL straight through. With no resolver installed, a page URL reports that rather than silently failing to demux an HTML page. LoadSource stays unrouted (it receives already-resolved or direct sources, including the resolver's own output). Centralising here lets BasisMediaPlayerStreaming drop its own routing call.
…orked playback Networked playback now broadcasts the URL that was set — the page URL for resolved sources, or the direct stream URL — instead of the resolved CDN URL. Resolved URLs (e.g. googlevideo) are per-client and expiring, so they can't be shared: each client resolves the page URL itself. Remote clients apply a page URL via LoadUrl (routing through the resolver) and a directly-playable URL via LoadSource with the synced seek position as before. currentSyncedUrl is only back-filled from the active source when nothing was set explicitly. Seek/pause fidelity for resolved sources is best-effort.
27 tasks
Adds pace_delivery, decoupling "throttle AU delivery" from "present on the paced VOD clock". Live HLS now sets pace_delivery — so the engine's PTS pace_gate meters delivery, tracking VBR exactly and recovering from stalls via its wall-clock anchor — while keeping paced=0, so it still presents at and converges to the live edge. The HLS source's byte-rate token bucket is disabled; pace_gate replaces it. VOD keeps both (paced delivery + paced clock); RTSP/RIST and open-ended live HTTP are unaffected. Verified on a live Twitch stream (VBR HLS via yt-dlp) at 1080p60. Includes the rebuilt Windows plugin.
dooly123
added a commit
that referenced
this pull request
Jun 15, 2026
… playback (#872) ## Summary **Fixes stuttering on HLS streams** (Twitch and similar — the single-stream, non-split path). On variable-bitrate video you'd get occasional audio dropouts and uneven playback. The HLS source controls how fast it feeds the decoder, which drops frames if fed faster than real time. It was pacing to the stream's **average** bitrate — but live video is variable-bitrate, so during busier scenes (which run above average) it fed too slowly, the buffers ran dry, and playback hitched. The fix paces to the **current segment's** bitrate instead of a stream-wide average, so each part of the stream is fed at the rate it actually needs and busy scenes no longer starve playback. Self-contained to the HLS source (`protocol/basis_hls.c`) — no engine or clock changes. ### Follow-up This paces by byte count at segment granularity — a big improvement, but not yet frame-exact. The frame-timestamp-exact version (sub-segment) is built on #871, which carries the engine's pacing machinery, and will supersede this once that lands. This stands on its own until then. ## Required checks All boxes below must be ticked before this PR can merge. If a check is genuinely N/A, tick it anyway and explain under **Notes**. <!-- 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 Tick the platforms you actually tested on. Leave the rest unticked — these are informational and do not block merge. - [x] Windows - [ ] Linux - [ ] Android - [ ] iOS - [ ] macOS Input / control mode coverage: - [ ] 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 **Tested (Windows, desktop):** a live VBR HLS stream that previously stuttered (occasional audio dropouts + uneven video) now plays smoothly with the per-segment metering. **The checklist rows are N/A** — this is a single native-C change in `protocol/basis_hls.c` (the HLS byte source's pacing), with no managed/Unity code, no per-frame work, no transform/camera/Addressables/scene-discovery, and no new allocations on any per-frame path. Jobification N/A — it's socket I/O pacing on the existing HLS producer/consumer threads.
The byte-rate token bucket was disabled when HLS moved to engine AU-pacing; this removes everything it left behind — the struct fields (target_bps, acc_bytes/ acc_dur_ms, tb_tokens/tb_last_us, the per-segment counters), the unused hls_now_us helper, and basis_hls_read's vestigial budget variable, always-true guard and stale "rate-limited" comment. No behaviour change.
0d9853b to
f7d2528
Compare
Resolved basis_hls.c + the plugin to ours: this branch's engine-level AU pacing supersedes the byte-rate per-segment metering that landed on developer via BasisVR#872, so the HLS source carries no byte-level metering here.
…esolved stream The playback panel's refresh mirrored the player's resolved ActiveMediaSource.Uri back into the editable URL field. After a page URL (Twitch/YouTube) resolved, the field showed the per-client, expiring CDN URL — so pressing Load URL again called SetUrl with that resolved URL and broadcast it to every peer, defeating the page-URL sync (peers must resolve their own stream). Mirror the networked SyncedUrl (the input/page URL) when a networking component is present, falling back to ActiveMediaSource.Uri only for solo playback. Re-loading now re-shares the page URL.
… sync Spell out that an extensionless direct HTTP stream sent to the resolver fails with an error (no extractable stream) rather than loading. Add a Networked sync section: the input/page URL is synced (each client resolves its own stream), along with play/pause/stop and drift-corrected position, and note that a remote-applied resolved source auto-plays from the live edge rather than applying the owner's exact position or initial pause/stop state.
Two related improvements so peers watching a resolved (YouTube/Twitch) source stay aligned with the owner: - Broadcast a page URL up front (in SetUrl) instead of only after the owner's OnReady, so peers resolve it in parallel rather than starting their seconds-long resolution only once the owner is already playing. Scoped to page URLs; directly-playable URLs keep the OnReady-gated path (no resolution latency to hide, and it avoids announcing a load before it is confirmed openable). - Add an owner-side position heartbeat: a small Sequenced (unreliable, latest-wins) position ping every few seconds feeds the existing drift correction, pulling any client that fell behind back onto the owner's clock and converging late joiners. Drift-only — it never changes play/pause/stop, so a stale or out-of-order ping cannot resurrect a stopped state. VOD-only (live reports no position and converges to the edge); the 9-byte payload keeps fan-out cheap in large instances. Update the README networked-sync section to match.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Several related additions to the media player, plus an optional integration package, that together let it play high-resolution adaptive sources (YouTube/Twitch and similar) — not just the live broadcast URLs it handles today.
1. Split-stream playback. Adaptive sources above ~360p serve video and audio as separate streams.
basis_media_open_dualruns a second demux thread for an audio-only URL into the same decoder, so both present in sync on one clock. Exposed viaBasisMediaSource.AudioUri(null = single muxed stream, unchanged).2. Live vs on-demand, auto-detected.
BasisMediaSource.Delivery(Auto/Live/OnDemand) selects the playback clock.Autoresolves at open: a finite, byte-range-seekable HTTP body (knownContent-Length+Accept-Ranges) or an HLS playlist withEXT-X-ENDLISTis treated as on-demand; an open-ended response or a non-HTTP transport is live. On-demand throttles delivery and presents on a fixed 1× clock with a compressed read-ahead buffer, so VOD that arrives faster than real time no longer fast-forwards — and the caller doesn't have to flag it.3. Optional page-URL resolution.
BasisMediaUrlRouteris a small seam the player consults before loading a URL.BasisMediaPlayerStreamingroutes itsStreamUrlthrough it: a directly-playable URL (transport scheme, or an HTTP URL with a media extension) loads straight through; a page URL (no media extension) goes to an installed resolver. With no resolver installed, a page URL reports that the resolver package is needed instead of failing silently.The new
com.basis.integration.ytdlppackage registers a yt-dlp-based resolver into that seam — turning a YouTube/Twitch page URL into the playable stream(s) (a split avc1+mp4a pair for VOD, a single HLS stream for live).4. Smooth live HLS. Live HLS (Twitch and similar) is paced by the engine's per-AU clock rather than a byte-rate token bucket, so variable-bitrate streams stay smooth through busy scenes while still presenting at — and converging to — the live edge.
5. Multiplayer-aware sync.
BasisMediaPlayerNetworkingsyncs the page URL, not the resolved CDN URL (which is per-client and expiring), so every client resolves it independently; direct stream URLs sync as before.Why a new seam (
BasisMediaUrlRouter)No existing player hook carries "resolve this URL before loading it," and the player package deliberately can't reference the integration package (removability — deleting the integration leaves the player intact). Dependency inversion via a registration seam is the only way to let the existing
StreamUrlfield steer page URLs without coupling the player to the resolver. The seam isn't an orphan extension point: its consumer (BasisMediaPlayerStreaming.TryResolveAndLoad) ships in this PR; the integration registers a second consumer.IsDirectlyPlayablelives on the player as the single classifier, and the resolver defers to it so the live-vs-resolve rule has one home.Scope / prerequisite
com.yewnyx.ytdlp) is intentionally not in this PR — how it enters the project is a separate vendoring decision, and it's a large binary package. The integration is gated#if BASIS_MEDIAPLAYER_EXISTS && YTDLP_EXISTS, so it compiles to nothing until that plugin is present. This PR is therefore safe to land without it — the resolver simply stays dormant until the plugin lands, then activates.Known limitations (documented in the package READMEs)
BufferMilliseconds/BufferModetune the live clock only.Demo
End-to-end: loading a range of sources through the same
BasisMediaPlayerStreamingURL field — VRCDN live streams (direct), plus Twitch and YouTube (resolved via the yt-dlp integration) — each resolving and playing.basis_pr871_video.mp4
3981a9f landing also fixes the above video's observed stutter on the initial A State Of Trance test HLS stream.
basis_pr871_hls_stutter_fix.mp4
What I need help with confirming?
Speaking to @dooly123 we need some consensus on how dlp-native gets embedded in to the client either dynamically vs static package, which needs comments from the maintainer (@yewnyx).
By that I mean either static — vendoring
com.yewnyx.ytdlp(the native plugin plus its bundled CPython runtime) into the repo as an embedded package, so it ships with the client and works on a plain checkout, at the cost of a sizable binary blob in git — or dynamic, keeping those binaries out of the tree and pulling them at build/install time, which keeps the repo lean but adds a fetch step and a version-pinning/provenance story. It's largely a repo-weight vs build-pipeline trade-off, so we're after your steer on how you'd prefer the package to be consumed.To be clear though, this isn't a blocker and there's no pressure on @yewnyx to land a particular answer quickly. The integration package is asmdef-guarded (
#if BASIS_MEDIAPLAYER_EXISTS && YTDLP_EXISTS), so with dlp-native absent it compiles to nothing and the URL handling just falls back to the media player directly: a directly-playable URL loads exactly as it does today, and a YouTube/Twitch page URL surfaces a clearBasisDebugmessage that a resolver package is needed rather than failing silently or breaking the load path. So this PR is safe to land as-is with the resolver dormant — the embedding decision can be taken on its own timeline, and nothing here regresses without it.Required checks
All boxes below must be ticked before this PR can merge. If a check is genuinely N/A, tick it anyway and explain under Notes.
TransformAccessArrayor are otherwise batched. I have not added per-frametransform.position/transform.rotation/transform.localPositioncalls inside loops. Whenever I need both position and rotation, I use the combined APIs —SetPositionAndRotation/SetLocalPositionAndRotationfor writes,GetPositionAndRotation/GetLocalPositionAndRotationfor reads — instead of two separate property accesses; the combined call does one local-to-world matrix traversal instead of two.Resources.Load, no direct asset references that pull large content into memory on scene load.GetComponent/AddComponentwhere avoidable — Where unavoidable, the result is cached on a field, and anyGetComponent<T>is replaced withTryGetComponent<T>(out var x)— bareGetComponentwill be denied.TryGetComponentis the modern API (Unity 2019.2+) and skips the Editor-only GC allocationGetComponentcauses when a component is missing: Unity wraps thenullreturn in a managed "fake null" object so its overloaded==operator can still detect destroyed C++ objects, and constructing that wrapper allocates;TryGetComponentreturns aboolplusoutparameter and never builds the wrapper. None of these calls run insideUpdate,LateUpdate,FixedUpdate, jobs, or other per-frame code paths.BasisEventDriver— Any new per-frame work hooks intoBasisEventDriverrather than adding standaloneUpdate/LateUpdate/FixedUpdatecallbacks on a MonoBehaviour.BasisEventDriveris bulletproof, or guarded bytry/catch—BasisEventDriverruns 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 atry/catchthat contains the failure and surfaces it throughBasisDebug— logged once / rate-limited, never every frame (see the existingHVRBasisBuiltInAddresses.Simulate()guard for the pattern). Expect this to be scrutinized closely in review.{ get; set; }properties or access lockdowns — Public fields are fine; Basis is meant to be read and modified freely, so don't wall things offprivate/internalwithout 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.Instancesingletons, callers reassigningType.Instanceis allowed; if that would break your code, log a warning or throw — don't block the assignment. Locking down access is not your call.BasisLocalCameraDriver— Code that needs the local camera (transform, projection, rig data, etc.) pulls it fromBasisLocalCameraDriverrather than looking one up itself. Don't roll a separate camera discovery path.BasisDebug— All new logging calls go throughBasisDebug.Log/BasisDebug.LogWarning/BasisDebug.LogError(with an appropriateLogTag) instead ofUnityEngine.Debug.Log/Debug.LogWarning/Debug.LogError.BasisDebugroutes through Basis's tagged, color-coded logger and respects the project-wideLoggingDisabledtoggle so logging can be killed at runtime; bareDebug.Logcalls bypass that and will be denied.FindObjectOfType/FindObjectsOfType/GameObject.Find/FindGameObjectsWithTagto 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.newon reference types, no LINQ, nostringconcatenation/interpolation, no boxing, noforeachover interface-typed collections. Allocate once at init and reuse the buffer.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_EDITORand remove (or leave gated) before merge..Count(lists) /.Length(arrays) into a localintbefore the loop instead of re-reading the property each iteration. PreferT[](with a separate length int when the array is over-sized) overList<T>where the data is hot — Unity's mono BCL doesn't exposeCollectionsMarshal.AsSpan(List<T>), so a list can't be fed intoSpan<T>/ unsafe paths cleanly. Where the perf justifies it, drop intoSpan<T>/reflocals /Unsafe.As/unsafepointer code to skip bounds checks and copies, and call out the invariants you're relying on under Notes so reviewers can sanity-check them.Testing details
Tick the platforms you actually tested on. Leave the rest unticked — these are informational and do not block merge.
Input / control mode coverage:
Where applicable, confirm these flows still work after your changes:
Notes
What "Tested" covers (Windows): In the editor — direct stream playback (RTSPT / fMP4 / TS / HLS) unchanged; a split avc1+mp4a VOD source playing in sync and paced (no fast-forward); auto-detect classifying a seekable HTTP body as on-demand and a live HLS stream as live; the
StreamUrlfield resolving real YouTube and Twitch links (and direct URLs bypassing the resolver); and the clear "resolver package needed" message when no resolver is installed. In a built player — live Twitch HLS playing smoothly at 1080p60 under the per-AU pacing, and the synced page URL resolving independently across two networked clients.Most checklist rows are N/A for this change — it's load-path / native decode / integration code, with no per-frame gameplay work:
BasisEventDriverand there are no hot paths — the "hot path" allocation/logging/collection rows don't apply. (The one cold-path delimiter array inIsDirectlyPlayableis hoisted to astatic readonlyfield regardless.)GetComponent;BasisMediaPlayerStreaminguses the existingTryGetComponent.Delivery,BasisMediaUrlRouter.Resolver) are plain fields per house style; the resolver delegate is freely reassignable.Prerequisite: the yt-dlp resolver only activates once the
com.yewnyx.ytdlpplugin is present (separate PR). Until then this change is inert on that path and direct-URL playback is unaffected.Draft status is about the dlp-native embedding decision above, not the player work: split-stream, VOD pacing, live-HLS pacing, page-URL resolution and the multiplayer page-URL sync are all validated in the editor and in a built player (incl. live Twitch and two-client sync). The integration stays inert until the plugin lands.