Skip to content

fix: crash on cancelled YouTube HLS synthesis - #9759

Merged
eordano merged 4 commits into
devfrom
fix/write-synthetized-hls
Aug 17, 2026
Merged

fix: crash on cancelled YouTube HLS synthesis#9759
eordano merged 4 commits into
devfrom
fix/write-synthetized-hls

Conversation

@lorux0

@lorux0 lorux0 commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

What

Fixes #9758

Fixes a fatal native crash on Windows that closes the client when a YouTube video
is being resolved and the player teleports or changes realm.

Why

Cancelling the byte-range requests in YouTubeVideoClient throws an
OperationCanceledException that re-throws through ~7 nested async state
machines, each invoking the next continuation from inside its own catch handler.
By the time it reaches TryWriteSynthesizedHlsAsync — which has both a catch
and two using blocks around its await — IL2CPP crashes while matching the
catch clause (Class::IsAssignableFromClass::Init, access violation).

The root cause is in the IL2CPP/MSVC exception handling, which we can't fix, so
the fix removes the code shape that triggers it.

How

  • FetchByteRangeAsync (was TryFetchByteRangeAsync) no longer rethrows on
    cancellation. It returns a Result<byte[]> via SuppressToResultAsync, so it
    completes through SetResult instead of SetException and the exception chain
    stops there.
  • TryWriteSynthesizedHlsAsync no longer has any try/catch/using around its
    await — the frame that crashed no longer exists.
  • The parsing and file-writing moved to a new synchronous WriteSynthesizedHls,
    where try/catch is safe.

No behaviour change: cancellation and fetch failures still fall back to the
non-segmented playlist exactly as before.

QA

Requires a Windows build

  1. Enter a scene with a VideoPlayer pointing at YouTube.
  2. Teleport away / change realm within ~1s of the video starting to load.
  3. No crashes are present

Also verify the happy path still works: a YouTube plays with segmented HLS
and starts within a few seconds.

@lorux0
lorux0 requested review from a team as code owners August 14, 2026 19:37
@github-actions
github-actions Bot requested a review from anicalbano August 14, 2026 19:37
@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

🚦 CI Status

Build

Windows and Mac build successful in Unity Cloud! You can find a link to the downloadable artifact below.

Name Link
Commit 327c09e
Logs https://github.com/decentraland/unity-explorer/actions/runs/31842906193
Download Windows https://github.com/decentraland/unity-explorer/suites/86390543225/artifacts/9235619222
Download Windows S3 https://explorer-artifacts.decentraland.org/@dcl/unity-explorer/branch/fix/write-synthetized-hls/pr-25164-327c09e/Decentraland_windows64.zip
Download Mac https://github.com/decentraland/unity-explorer/suites/86390543225/artifacts/9235733923
Download Mac S3 https://explorer-artifacts.decentraland.org/@dcl/unity-explorer/branch/fix/write-synthetized-hls/pr-25164-327c09e/Decentraland_macos.zip
Built on 2026-08-14T22:11:15Z

Lint

Warnings count reduced: 13156 => 13148

Warnings/errors in files changed by this PR (18)
Assets/DCL/Social/RPCSocialServices.cs:100  CSharpWarnings::CS8603  Possible null reference return
Assets/DCL/Web3/Authenticators/Implementations/ThirdWeb/ThirdWebEthereumApi.cs:343  CSharpWarnings::CS8604  Possible null reference argument for parameter 'hexValue' in 'DCL.Web3.Web3Utils.ParseHexToBigInteger'
Assets/DCL/Web3/Authenticators/Implementations/ThirdWeb/ThirdWebEthereumApi.cs:86  ConditionIsAlwaysTrueOrFalse  Expression is always false
Assets/DCL/Web3/Authenticators/Implementations/ThirdWeb/ThirdWebEthereumApi.cs:271  ConditionalAccessQualifierIsNonNullableAccordingToAPIContract  Conditional access qualifier expression is never null according to nullable reference types' annotations
Assets/DCL/Web3/Authenticators/Implementations/ThirdWeb/ThirdWebEthereumApi.cs:272  ConditionalAccessQualifierIsNonNullableAccordingToAPIContract  Conditional access qualifier expression is never null according to nullable reference types' annotations
Assets/DCL/Multiplayer/Connections/Archipelago/LiveConnections/ArchipelagoSignedConnection.cs:238  EmptyStatement  Empty statement is redundant
Assets/DCL/Social/RPCSocialServices.cs:17  InconsistentNaming  Name 'IRPCSocialServices' does not match rule 'interfaces_should_be_pascal_case_with_i_prefix'. Suggested name is 'IRpcSocialServices'.
Assets/DCL/Social/RPCSocialServices.cs:36  InconsistentNaming  Name 'RPCSocialServices' does not match rule 'members_should_be_pascal_case'. Suggested name is 'RpcSocialServices'.
Assets/DCL/Web3/Authenticators/Implementations/ThirdWeb/ThirdWebEthereumApi.cs:52  ParameterHidesMember  Parameter 'chainId' hides field 'DCL.Web3.Authenticators.ThirdWebEthereumApi.chainId'
Assets/DCL/Web3/Authenticators/Implementations/ThirdWeb/ThirdWebEthereumApi.cs:129  RedundantSuppressNullableWarningExpression  The nullable warning suppression expression is redundant
Assets/DCL/Web3/Authenticators/Implementations/ThirdWeb/ThirdWebEthereumApi.cs:133  RedundantSuppressNullableWarningExpression  The nullable warning suppression expression is redundant
Assets/DCL/Web3/Authenticators/Implementations/ThirdWeb/ThirdWebEthereumApi.cs:223  RedundantSuppressNullableWarningExpression  The nullable warning suppression expression is redundant
Assets/DCL/Web3/Authenticators/Implementations/ThirdWeb/ThirdWebEthereumApi.cs:237  RedundantSuppressNullableWarningExpression  The nullable warning suppression expression is redundant
Assets/DCL/Web3/Authenticators/Implementations/ThirdWeb/ThirdWebEthereumApi.cs:285  RedundantSuppressNullableWarningExpression  The nullable warning suppression expression is redundant
Assets/DCL/Web3/Authenticators/Implementations/ThirdWeb/ThirdWebEthereumApi.cs:297  RedundantSuppressNullableWarningExpression  The nullable warning suppression expression is redundant
Assets/DCL/Web3/Authenticators/Implementations/ThirdWeb/ThirdWebEthereumApi.cs:362  RedundantSuppressNullableWarningExpression  The nullable warning suppression expression is redundant
Assets/DCL/Multiplayer/Connections/Archipelago/LiveConnections/ArchipelagoSignedConnection.cs:5  RedundantUsingDirective  Using directive is not required by the code and can be safely removed
Assets/DCL/Multiplayer/Connections/Archipelago/LiveConnections/ArchipelagoSignedConnection.cs:9  RedundantUsingDirective  Using directive is not required by the code and can be safely removed

Tests

All Unity tests passed ✅

TESTS SUITE Result Passed Failed Skipped
EditMode ✅ Passed 24993 0 13
PlayMode ✅ Passed 236 0 37

@decentraland-bot
decentraland-bot self-requested a review August 14, 2026 19:37

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — PR #9759

STEP 2 — Root-cause check: PASS ✅

The PR fixes a fatal IL2CPP/MSVC crash on Windows triggered when OperationCanceledException unwinds through ~7 nested async state machines and hits TryWriteSynthesizedHlsAsync — whose catch/using funclets around the await produce a code shape that crashes IL2CPP's Class::IsAssignableFromClass::Init path.

Since the true root cause is an IL2CPP AOT compiler bug (outside the project's control), the fix correctly eliminates the code shape that triggers it:

  • Async layer (TryWriteSynthesizedHlsAsync) — deliberately exception-free; no try/catch/using around await.
  • Fetch (FetchByteRangeAsync) — enforces the exception-free contract via SuppressToResultAsync(), converting all exceptions (including OperationCanceledException) into Result<byte[]> values.
  • Sync layer (WriteSynthesizedHls) — all throwable code (parsing, file I/O) lives here, where try/catch operates on a normal stack frame, not an async state machine.

This addresses the cause from the application's perspective, not a symptom.

STEP 3 — Design & integration: PASS ✅

  • No new long-lived units introduced. The PR restructures existing private methods within YouTubeVideoClient. No new systems, plugins, managers, services, or controllers.
  • No lifecycle/ownership changes. No new subscriptions, events, connections, or buffers.
  • Teardown trace: The using blocks for ListPool are preserved in WriteSynthesizedHls. No new resources requiring teardown are introduced.
  • No public API surface changes. The IYouTubeVideoClient interface is untouched.

STEP 4 — Member audit: PASS ✅

Member Visibility Consumers Assessment
FetchByteRangeAsync private 2 call sites (video + audio in UniTask.WhenAll) DRY extraction — eliminates duplicated request chain. Not single-use. ✅
WriteSynthesizedHls private static 1 call site (TryWriteSynthesizedHlsAsync) Single-use, but extraction is mandatory — separating sync try/catch from async state machine is the core crash fix. Not gratuitous per CLAUDE.md §11. ✅

STEP 5 — Line-level review: PASS ✅

Pass A (blocking issues):

  1. Code qualitySuppressToResultAsync() usage per CLAUDE.md §9. ct.IsCancellationRequested (not ThrowIfCancellationRequested) in the exception-free flow per CLAUDE.md §9.
  2. BugsResult.Value accessed only after Result.Success guard. Cancellation checked after UniTask.WhenAll before proceeding.
  3. Security — URLs from YouTube API, file writes to Application.temporaryCachePath. No new attack surface.
  4. Performance — No change to runtime characteristics. Parallel sidx fetch preserved.
  5. Error handlingFetchByteRangeAsyncSuppressToResultAsync catches everything. WriteSynthesizedHls has its own catch (Exception). TryWriteSynthesizedHlsAsync is exception-free by contract with all paths covered.
  6. Resource leaks — No new subscriptions/events/connections. ListPool using-blocks preserved. Result<byte[]> does not require disposal.
  7. Nullability — Return type string? correctly nullable. No ! operators added.
  8. Logging — Correctly uses ReportHub.Log (informational) and ReportHub.LogWarning (unexpected failure) with ReportCategory.MEDIA_STREAM. Improved diagnostics: fetch failures now distinguish video vs. audio sidx.

Pass B (design smells): No issues found.

  • Doc comments explain "why" (IL2CPP crash avoidance, Sentry reference) — not narrating caller behavior.
  • static on WriteSynthesizedHls correctly signals no instance-state dependency.
  • No magic values, no constructor issues, no encapsulation leaks.

Security review: No security issues found ✅

Reviewed against: secrets/credentials, injection, auth/authz, sensitive data exposure, path traversal, SSRF. No new attack surface — URLs sourced from YouTube API response, file operations in temp cache, no user-supplied input in paths or requests.

STEP 6 — Complexity

COMPLEX — Modifies async/UniTask cancellation flow and Result<T> patterns.

STEP 7 — QA

YES — Changes runtime code affecting video playback and crash behavior on Windows.

STEP 8 — Non-blocking warnings

None.

STEP 9 — Verdict

Clean, well-targeted fix. The async/sync split is the minimal structural change needed to eliminate the IL2CPP crash shape. All project conventions (async error handling, cancellation, naming, logging, resource cleanup) are followed. Doc comments provide essential guardrails against reintroducing the crash pattern.

REVIEW_RESULT: PASS ✅
COMPLEXITY: COMPLEX
COMPLEXITY_REASON: Modifies async/UniTask cancellation flow, SuppressToResultAsync / Result<T> patterns in YouTube media stream handling
QA_REQUIRED: YES


Reviewed by Jarvis 🤖 · Requested by decentraland-bot via GitHub

@github-actions

Copy link
Copy Markdown
Contributor

🔍 Jarvis reviewed this PR and found no blocking issues, but assessed it as complex — human DEV review is still required before merging.

@decentraland-bot

This comment has been minimized.

@Ludmilafantaniella Ludmilafantaniella left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Approve

Tested on Windows and Mac, using a scene with a Video Screen (Smart Item) pointing to a YouTube URL. Verified in two scenes: my own world and Genesis City.

  • Teleported away within ~1s of the YouTube video starting to load - no crash on either platform
  • Happy path: video plays via segmented HLS and starts within a few seconds on both platforms

Minor observation (not blocking): video start took a couple seconds longer on Windows in Genesis City specifically compared to my own world/Mac - likely just scene weight/network, not clearly tied to this change.

Note: the automated performance test on this PR shows a real CPU regression on both platforms (Intel ~14% slower average, up to 758% worse 1%-worst; M1 ~11% slower average, up to 526% worse 1%-worst) - worth a look from the dev/perf side, though it didn't surface as a noticeable issue in my manual testing.

No blockers on the crash fix itself.

14.08.2026_18.16.40_REC.mp4

@lorux0

lorux0 commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

@decentraland-bot rerun performance tests

@decentraland-bot

Copy link
Copy Markdown
Contributor

PR #9759, run #31845644851

Builds: Windows change, Windows baseline, macOS change, macOS baseline

How to read this table
  • Each build is measured 3 times. The values are the median, and (min–max) is the lowest and highest of those runs — a wide range means the metric is noisy and small differences are not trustworthy.
  • Δ is Change minus Baseline (a negative Δ means Change is faster).
  • 🟢 faster / 🔴 slower — a real difference: larger than both 3% and the run-to-run range.
  • ⚪ within noise — the difference is smaller than how much the build varies between its own runs, so it cannot be told apart from random variation. Treat it as no change.
  • Exceptions per run — the average number of exceptions in a run's log; more than the baseline is flagged 🔴 even when frame times look fine. The Exception breakdown under each table groups them by the explorer's report category and exception type (as totals across the runs).
  • A run that logged unusually many exceptions (at least 10 and 5× the median of its build's runs — e.g. a service was down during it) is excluded from all numbers and called out under the table.

Intel Core i5

Metric Baseline Change Δ Result
Samples 2701 (×3) 2371 (×3)
CPU average 33.2 ms (33.2–34.6) 37.7 ms (37.1–37.7) 4.5 ms 🔴 14% slower
CPU 1% worst 34.3 ms (33.5–184.5) 291.0 ms (283.1–297.5) 256.8 ms 🔴 749% slower
CPU 0.1% worst 41.4 ms (33.7–332.0) 308.4 ms (308.0–308.6) 267.0 ms ⚪ within noise
GPU average 9.3 ms (9.2–9.4) 9.5 ms (9.2–9.5) 0.2 ms ⚪ within noise
GPU 1% worst 20.7 ms (19.8–26.9) 31.2 ms (30.6–31.2) 10.5 ms 🔴 51% slower
GPU 0.1% worst 36.3 ms (31.6–37.7) 38.5 ms (35.8–41.3) 2.2 ms ⚪ within noise
Exceptions per run 66 66 0 ⚪ none new
Exception breakdown
Exception Baseline (3 runs) Change (3 runs)
[UI] DllNotFoundException 192 192
[ENGINE] NullReferenceException 3 3
[ENGINE] ObjectDisposedException 3 3

Apple M1

Metric Baseline Change Δ Result
Samples 4368 (×3) 3892 (×3)
CPU average 20.5 ms (20.3–21.6) 23.0 ms (22.2–23.1) 2.5 ms 🔴 12% slower
CPU 1% worst 34.7 ms (33.9–34.7) 226.7 ms (223.6–226.9) 192.0 ms 🔴 554% slower
CPU 0.1% worst 34.9 ms (34.9–35.3) 229.3 ms (227.7–230.2) 194.4 ms 🔴 556% slower
GPU average 1.0 ms (0.1–1.6) 7.0 ms (3.1–9.4) 6.1 ms ⚪ within noise
GPU 1% worst 34.2 ms (7.7–34.8) 36.7 ms (34.9–37.1) 2.5 ms ⚪ within noise
GPU 0.1% worst 35.9 ms (35.1–37.2) 37.8 ms (35.8–38.2) 1.9 ms ⚪ within noise
Exceptions per run 0 0 0 ⚪ none new

@lorux0
lorux0 enabled auto-merge (squash) August 17, 2026 16:24
@eordano
eordano disabled auto-merge August 17, 2026 17:06
@eordano
eordano merged commit 1bd63e7 into dev Aug 17, 2026
23 of 27 checks passed
@eordano
eordano deleted the fix/write-synthetized-hls branch August 17, 2026 17:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

il2cpp::vm::Class::Init

4 participants