Repository navigation
Serve HLS segments to clients that authenticate with a token - #302
Merged
Merged
Conversation
Transcoded video played black on iOS: the clock ran and the duration was right but there was no picture. AVURLAssetHTTPHeaderFieldsKey applies to the playlist request only, and AVFoundation does not propagate those headers to the .ts segment fetches, so a segment arrived with no credentials. The segment route sat behind session auth, which answered 302 -> /login; AVPlayer followed the redirect and decoded the login page as video. The duration worked because the playlist had carried the header, which is what made it look like a video problem. A token client now gets signed segment URLs: the signature travels in the URL, which is the one credential a player reliably keeps, and it is unguessable, scoped to one session and file, and expires. The session id cannot do that job -- it is a deterministic hash of (item, updated_at, height, start) -- and there is a test asserting its stability so nobody mistakes it for a secret. The playback endpoint also pointed at the web playlist, which was the other half of the same mistake. Browsers are unaffected: a cookie rides along on every segment, so the web routes keep plain URLs.
Owner
Author
|
Reviewed on a5 — clean. 8 tests pass. Based on current main, no migration. This is the real root cause of the Midnight Gospel black-video finding, and it's server-side — my earlier call (not the file; identical to the working E01) was right, but I leaned 'app bug' when the mechanism is here: HLS The fix is the correct Laravel pattern:
Nothing blocking. Note this pairs with #301 (playlist decision) — together they make native HLS actually play; the iOS app still has to request |
This was referenced Oct 6, 2026
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.
Tracker #514. Reported on The Midnight Gospel: every episode but the first starts, runs the clock, shows the right duration — and stays black.
Why
AVURLAssetHTTPHeaderFieldsKeyapplies to the playlist request only. AVFoundation does not propagate those headers to the individual.tsfetches, so a segment arrived with no credentials at all. The segment route sat behind session auth, which answered 302 → /login, and AVPlayer followed the redirect and decoded the login page as video.The duration and clock worked because the playlist had carried the header. That is what made it look like a video problem rather than an auth one.
Measured directly rather than inferred:
The fix
A token client gets signed segment URLs. The signature travels in the URL — the one credential a player reliably keeps — and is unguessable, scoped to one session and file, and expires after 12 hours.
The session id cannot do that job, which is worth stating because the shortcut is tempting. It is a deterministic
sha256of(item, updated_at, height, start), so anyone who knows the item can compute it. There is a test asserting it is stable, precisely so it is never later mistaken for a secret.Browsers are untouched: a cookie rides along on every segment, so the web routes keep plain URLs and signing would be noise. The switch is keyed on the request reaching an
api/*route, not on the guard.Also fixes the other half:
/playbackwas handing token clients the web playlist URL.Testing
8 new tests, 290 pass across the streaming and API suites. Every one confirmed to fail against the bug it guards:
an_unplayable_file_is_sent_to_the_api_playlistsignedmiddleware removedan_api_playlist_names_signed_segmentsThe unsigned case asserts specifically that the refusal is not a 302 — a redirect is the thing that broke this.
For review
The 12-hour expiry is a judgement call. Long enough to outlast any viewing including pauses; short enough that a URL in a log is not useful forever. If you would rather it were tighter, the tradeoff is a long pause mid-film failing to resume.
Not verified end-to-end. There is no transcodable video on this machine, so I could not watch a real MKV play through to confirm the picture appears. The auth path is proven by test; the pixels are not. That is the one thing worth checking on a5.