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
baseUrlis used: scheme, host, and port. A path onbaseUrlis 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. externalIdis percent-encoded into the last path segment. Decode it. An id with a space or a slash in it arrives as%20or%2F.tcarries 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
Rangetypically plays once and then breaks the moment anyone scrubs. The forms a browser sends arebytes=N-andbytes=N-M. Answer a range with206, aContent-Range, and aContent-Lengthof the part and not the whole. Answer a range that starts past the end with416andContent-Range: bytes */<size>. Without aRangeheader, answer200with the whole file. SendAccept-Ranges: byteson every response. - Send the right
Content-Typefor 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
OPTIONSwith a204and 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: trueif 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-Originnaming the origin that asked (taken from theOriginheader), or*. The Stage sends no credentials, so a wildcard is acceptable. If you echo the origin, also sendVary: Origin.Access-Control-Expose-Headers: Content-Length, Content-Range.Access-Control-Allow-Methods: GET, HEAD, OPTIONSandAccess-Control-Allow-Headers: Rangeon theOPTIONSanswer.
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
.movfrom 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.