lets.dj

Serving media

The media route

GET <baseUrl>/media/<externalId>?t=<mediaToken>

How the address is built

The Stage builds the address from the baseUrl you last synced and the externalId of the track, and it does so like this:

  • Only the origin of baseUrl is used: scheme, host, and port. A path on baseUrl is discarded, so the route is always /media/… at the root of that origin, and not at /music/media/… because that is where you mounted it.
  • externalId is percent-encoded into the last path segment. Decode it. An id with a space or a slash in it arrives as %20 or %2F.
  • t carries the media token, and a preview adds &preview=1 (see Answering on demand).
  • The address is resolved when a song is about to play, from what you most recently synced. The Stage then keeps it for the length of that song.

The certificate for the host must be one the Stage’s browser already trusts. The Stage is a web page and has nobody to click through a warning, so a self-signed certificate does not work however it is installed. A provider on a home network can use the name and the certificate described in A name and a certificate.

What to answer

  • Reject a wrong or missing token with 403. An id you do not have is a 404.
  • Support range requests. Media elements seek with them. A server that ignores Range typically plays once and then breaks the moment anyone scrubs. The forms a browser sends are bytes=N- and bytes=N-M. Answer a range with 206, a Content-Range, and a Content-Length of the part and not the whole. Answer a range that starts past the end with 416 and Content-Range: bytes */<size>. Without a Range header, answer 200 with the whole file. Send Accept-Ranges: bytes on every response.
  • Send the right Content-Type for the container: video/mp4, video/webm, video/quicktime, audio/mpeg, audio/mp4. A browser will sometimes recover from a wrong one and sometimes will not, and it is not worth finding out on somebody’s television.
  • Answer OPTIONS with a 204 and the CORS headers below. The Stage’s requests are simple ones, so a preflight is not expected, but a browser is entitled to send one.
  • Send Access-Control-Allow-Private-Network: true if you are on a private address. Chrome does not currently enforce this, because it gates on a one-time user permission prompt instead, but enforcement has been deferred before and the header costs nothing.

CORS

CORS headers are required if you declare canAnalyseAudio, and recommended whether or not you do. A plain <video> or <audio> element plays cross-origin media without them, which is what the Stage does for a provider that does not declare the capability. Declaring it is what changes that, because the Stage then asks for the media in a mode that fails outright without them. See Capabilities for why, and what goes wrong.

When you send them, send them on every successful response to a request for a media address, 200 and 206 alike, and on a 416 too. They need not be on a 403 or a 404:

  • Access-Control-Allow-Origin naming the origin that asked (taken from the Origin header), or *. The Stage sends no credentials, so a wildcard is acceptable. If you echo the origin, also send Vary: Origin.
  • Access-Control-Expose-Headers: Content-Length, Content-Range.
  • Access-Control-Allow-Methods: GET, HEAD, OPTIONS and Access-Control-Allow-Headers: Range on the OPTIONS answer.

If the address redirects, every hop has to do the same.

The media token

The media token is yours. We store it, encrypted, and put it in the address the Stage is given, which is how the Stage can prove to you that the screen is allowed to ask. It keeps anyone else on the same network from walking your library by guessing ids, which on a shared or guest wifi is a real concern and not a theoretical one.

Rotating it, for instance on every restart, is fine, because the next sync tells us the new one. The consequence is that a Stage already holding an address for the song that is playing will get a 403 on its next range request, so a restart interrupts what is on the screen, and so does a song that begins before the first sync after the restart has landed. Send that sync as early as you can. Songs resolved after it use the new token and play.

What a person sees when it fails

There are two different failures, and the room sees them differently.

If the service has no address to give for a track, because the library was deleted or has never synced, the Stage says that the library holding the song is not reachable, and waits. It does not skip, because silently burning through a queue hides the cause.

If the service gives an address and the browser then cannot load it, the media element reports an error and the Stage skips the song and moves on to the next one, with nothing on the screen to say why. A refused permission, a computer that is asleep, a different network, a certificate the browser does not trust, a wrong token, and a file the browser cannot decode all look exactly alike from the room: songs that start, and go. That is one reason to answer a wrong token with a plain 403 and nothing else. It keeps the cases apart when you read your own logs.

Formats

The Stage plays through a browser media element, so what plays is what the browser opens. MP4/H.264, WebM, and MOV work directly. MKV, AVI, WMV, and FLV do not, and neither does CDG+MP3 — and CDG in particular is the real karaoke disc format, common in actual libraries.

A container is only half of it. A browser opens a file only if it can also decode what is inside:

  • MP4 and MOV play everywhere with H.264 video and AAC audio. HEVC (H.265) plays on some devices and not on others. A .mov from a camera or an editor is often one of those.
  • WebM means VP8 or VP9 video with Vorbis or Opus audio.
  • Audio in MP3, and AAC in an M4A, plays everywhere. Apple Lossless in an M4A does not play in every browser.

Nothing in the service looks inside a file. The bridge decides by extension alone, so an .mp4 it reports as playable can still refuse on a particular television. Whether a file is playable is your judgement, and you are the one placed to make it.

If your source has files the browser will not open, transcode. Until you can, say so per track with playable: false on anything the browser will not open.

A track marked that way is still catalogued, and its owner still sees it on the library page marked as one that will not play. It is kept out of browse and search, so nobody can queue it and watch it fail on the wall. That beats both alternatives: offering it and failing in front of a room, or dropping it silently and leaving someone to wonder where half their collection went.

Omitting the flag means playable. A provider written before this existed is taken at its word rather than second-guessed from a filename we were never shown.