Skip to content

3. Loading Media & HLS Basics

Doc 2 ended with a LOAD message flowing over the media namespace, and a MEDIA_STATUS message flowing back. This doc is about what’s actually inside that LOAD message, and the video streaming format almost every app — including this one — uses to make it possible.

The TV does not “download the video from your phone.” Your phone tells the TV a URL, and the TV fetches that URL directly from the internet (or your local network), completely independently of the phone, using its own network connection. Your phone is a remote control, not a video source. This single fact explains almost every design decision in this app’s cast code — including the whole of doc 4.

❌ NOT what happens: ✅ what actually happens:
Phone ──(video bytes)──► TV Phone ──(a URL string)──► TV
(phone has to stream │
the whole video itself) ▼
TV ──(fetches the URL itself)──► CDN
(phone is done — just sent
one small instruction)

Here’s a simplified version of what actually gets sent (see doc 2 for the envelope it travels in):

{
"type": "LOAD",
"autoplay": true,
"currentTime": 0,
"media": {
"contentId": "https://cdn.example.com/movie/master.m3u8",
"contentType": "application/x-mpegURL",
"streamType": "BUFFERED",
"duration": 5400,
"metadata": { "title": "Some Movie", "images": [...] },
"tracks": [ /* subtitle tracks, see below */ ]
}
}
  • contentId — the URL. This is the one field that matters most: everything else is metadata dressing.
  • contentType — the MIME type, telling the receiver’s player how to interpret that URL. application/x-mpegURL means “this is an HLS playlist,” as opposed to, say, a direct video/mp4 file.
  • streamType — BUFFERED (a normal video-on-demand with a seekable timeline) vs LIVE (a live stream, where showing a seek bar the user can scrub to “the end” doesn’t make sense).
  • tracks — optional sideloaded subtitle tracks (more below).

contentType: application/x-mpegURL names the format this app (and most streaming apps) actually uses: HLS, an Apple-originated but now near-universal standard for delivering video over plain HTTP. The core trick of HLS is that a “video” isn’t one file — it’s a small text manifest that lists a sequence of much smaller video chunks (segments, typically ~4-10 seconds each), plus enough metadata for a player to request them in order and stitch them together seamlessly.

There are two levels of manifest:

master.m3u8
(the "menu" — lists quality options)
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
1080p/media.m3u8 720p/media.m3u8 audio/media.m3u8
(the actual segment list for that one rendition)
│
▼
segment_000.ts segment_001.ts segment_002.ts ...
(a few seconds of video each, fetched one at a time as playback advances)
  • The master playlist doesn’t contain any video — it’s a menu of available renditions (1080p, 720p, audio-only, etc.), each tagged with its bitrate and resolution, so the player can pick one based on available bandwidth (this is the “adaptive” in “adaptive bitrate streaming” — a player can switch renditions mid-playback if the network gets worse).
  • Each media (variant) playlist is the “real” list: a sequence of segment URLs (often relative, like segment_000.ts) plus timing info.
  • Segments are the actual video bytes, usually MPEG-TS (.ts) containers, fetched one at a time as playback progresses — which is exactly why HLS streams well over flaky networks: losing one segment is a small, recoverable blip, not a dropped connection to one giant file.

Separately from the video/audio in the manifest, LOAD can list sideloaded subtitle tracks — WebVTT files the receiver overlays on top of the video, independent of whatever the HLS manifest itself contains:

"tracks": [
{
"trackId": 1,
"type": "TEXT",
"subtype": "SUBTITLES",
"trackContentId": "https://cdn.example.com/subs/en.vtt",
"trackContentType": "text/vtt",
"language": "en",
"name": "English"
}
]

One practical constraint worth knowing: the Chromecast Default Media Receiver only renders WebVTT (and TTML) — not .srt, .ass, or other common subtitle formats. An app has to filter those out before offering them, or the receiver may reject the whole LOAD.

Once loaded, the receiver periodically (and on every state change) reports back a MEDIA_STATUS message:

{
"type": "MEDIA_STATUS",
"status": [{
"mediaSessionId": 7,
"playerState": "PLAYING",
"currentTime": 145.2,
"media": { "contentId": "...", "duration": 5400 }
}]
}

mediaSessionId is worth calling out specifically: once a LOAD succeeds, every follow-up command (pause, seek, change subtitle track) has to reference that specific session id — it’s how the receiver disambiguates “which currently-loaded video are you talking about,” in case a queue or a reload is in flight.

For series playback (autoplay-next), CASTV2 has a QUEUE_LOAD variant of LOAD that hands over a whole ordered list of items up front, so the receiver can advance to the next episode on its own without the phone needing to send a fresh LOAD the moment one episode ends (which would cause a visible stall at every episode boundary).

  • lib/data/services/cast/cast_media_builder.dart — CastMediaBuilder.mediaFor() builds the media object above: picks contentType, computes streamType from whether the content is a live stream, builds the metadata block, and filters subtitle tracks down to ones the receiver can actually render.
  • lib/data/services/cast/cast_messages.dart — wraps that media object into the full LOAD / QUEUE_LOAD JSON payload.
  • lib/data/services/cast/cast_media_status.dart — parses incoming MEDIA_STATUS back into a typed Dart object.
  • lib/presentation/widgets/player/cast_controller.dart — ties it together: dispatches the load, polls status, and exposes qualityVariants / subtitleTracks / activeSubtitleTrack for the UI.

Casting a video is, at its core, “send the TV a URL and a MIME type, then listen for status updates.” HLS is the format that URL almost always points to, precisely because it’s just plain HTTP + a text manifest + small segment files — nothing about it requires the TV and the phone to have any special relationship beyond “both can reach the same URL over HTTP.” Which raises the obvious next question: what happens when the phone can reach that URL (because it’s logged in, or has a special header) but the TV can’t? That’s doc 4.