Skip to content

Video

An item with a video becomes a video card. The field list is in Item fields; this page explains how to fill it and what the host must allow. The wall plays only video that WebGL can read, which means the file must come with CORS headers. YouTube and Vimeo cannot be read that way, so they open as an embed.

video points atvideo_typeNotes
An .mp4 or .webm filefileThe simplest choice. H.264 mp4 plays everywhere.
An .m3u8 playlisthlsAdaptive streaming.
An .mpd manifestdashAdaptive streaming.

video_type is optional. When it is absent, a path ending in .m3u8 means hls, a path ending in .mpd means dash, and anything else means file. An item with a video and no media_type is a video: the wall (and every adapter and plugin) fills in media_type: "video".

{
"id": "clip-7", "link": "/clips/7", "title": "Harbour at dawn",
"image": "https://cdn.example.com/clip-7/poster-512.jpg", "aspect": 1.7778,
"video": "https://cdn.example.com/clip-7/master.m3u8",
"duration": 94
}

duration is in seconds and is shown on the card. A negative or non-numeric duration is ignored.

video_sources lists up to 8 alternative renditions. Use it for 480p, 720p and 1080p mp4 files, or for an AV1 and an H.264 version of the same stream. If video is absent, the first source becomes video.

"video_sources": [
{ "src": "https://cdn.example.com/clip-7/480.mp4", "height": 480, "type": "file" },
{ "src": "https://cdn.example.com/clip-7/1080.mp4", "height": 1080, "type": "file", "codecs": "avc1.640028", "bitrate": 5000000 },
{ "src": "https://cdn.example.com/clip-7/av1.m3u8", "type": "hls", "codecs": "av01.0.08M.08" }
]
FieldMeaning
srcURL of the rendition. Required. Only http(s) and relative URLs are kept.
typefile, hls or dash.
heightPixel height, an integer above 0.
codecsA codecs string such as avc1.640028.
bitrateBits per second, an integer above 0.

Entries beyond the eighth are dropped, and so is an entry without a valid src. The item stays.

projection is flat (the default), equirect180 or equirect360. stereo is mono (the default), sbs (side by side) or tb (top and bottom). List the projections you use in media.projections of the manifest.

Put the page URL in video. The link is used only when the item has neither video nor media_type. The adapters and plugins turn these forms into an embed:

ProviderRecognised forms
YouTubeyoutube.com/watch?v=ID, m.youtube.com/watch?v=ID, youtu.be/ID, youtube.com/shorts/ID, youtube.com/embed/ID, youtube-nocookie.com/embed/ID
Vimeovimeo.com/ID, vimeo.com/ID/HASH, player.vimeo.com/video/ID with an optional ?h=HASH

The result has no video, has media_type: "video", and has unplayable: { "provider": "YouTube", "embed": "https://www.youtube-nocookie.com/embed/ID" }. The host name must match exactly: youtube.com.evil.example is not YouTube. A YouTube ID is 11 characters, a Vimeo ID is digits.

  • A YouTube item with no image gets https://i.ytimg.com/vi/ID/hqdefault.jpg as its picture, at aspect 16/9.
  • A Vimeo item gets no invented picture (that would need Vimeo’s API). Without an image it is a text card when it has a title, and is dropped when it has none.
  • unplayable.embed is honoured only for https://www.youtube-nocookie.com/embed/ID, https://www.youtube.com/embed/ID (shown through the nocookie host) and https://player.vimeo.com/video/ID (the h= hash is kept). Any other embed is removed.
  • Access-Control-Allow-Origin: * (or the requesting origin) on the video file. For HLS and DASH it must be on every request: the playlist or manifest, each segment, and any key or init file. A playlist with CORS and segments without it will not play.
  • Accept-Ranges: bytes for mp4 and webm, so the player can seek.
  • List the host in media.hosts of the manifest so the validator can check it (only hosts other than your own site).
  • Live DASH: send Access-Control-Expose-Headers: Date on the DASH manifest (or add a UTCTiming element to it). The player reads the server’s clock from that header to find the live edge. Without it the stream still plays, but on the headset’s own clock, so the live position can be off by as much as that clock is.

If the page that embeds the wall sets a Content Security Policy, it needs these on top of what the wall itself needs:

DirectiveAllowWhy
media-srcthe video hosts, and blob: for HLS and DASHmp4 and webm play straight from their host. HLS and DASH play through Media Source Extensions: the video element’s source is a blob: URL.
connect-srcthe hosts of playlists, manifests and segmentshls.js and dash.js fetch them.
script-srcthe host of wall-hls.js and wall-dash.js (the same directory as wall.iife.js)loaded only when an HLS or DASH video opens.
worker-srcblob: (recommended)hls.js converts the video segments in a background worker started from a blob: URL when it can.
img-srcthe poster hostsposters are textures.
frame-srchttps://www.youtube-nocookie.com https://player.vimeo.comthe YouTube and Vimeo player outside VR.

A strict example, with the wall’s files and videos on https://cdn.example:

script-src 'self' https://cdn.example; img-src 'self' https://cdn.example; media-src 'self' https://cdn.example blob:;
connect-src 'self' https://cdn.example; worker-src 'self' blob:; frame-src https://www.youtube-nocookie.com https://player.vimeo.com

Tested with HLS: with media-src 'self' an mp4 plays and an HLS stream is refused; with media-src 'self' blob: the stream plays. DASH works the same way but was not tested under a CSP.

  • H.264 (AVC) in mp4 plays on every device. Use it as the default.
  • A Quest 3 should decode HEVC and AV1 up to 8K. We have not tested this on a real headset. A Quest 2 and Quest Pro should decode AV1 in software only, so offer an H.264 rendition next to it.
  • Elsewhere, keep video at 2160p or below. Larger streams cost more than the card can show.
SourceVideo fields it reads
JSON feed, well-knownAll the fields above, checked as described.
RSS and AtomA video enclosure, media:content with type="video/..." or medium="video" (also inside media:group; several give video_sources with height), media:thumbnail as the picture, media:content@duration or itunes:duration as duration.
DOM annotationsdata-wall-video, data-wall-video-sources (a JSON array), data-wall-video-type, data-wall-duration. Without data-wall-video: a <video src> or its first <source src> inside the item, then og:video:secure_url or og:video when the page holds one item.
Recipesvideo, video_type, video_sources (api recipes), duration, projection, stereo, media_type.

An RSS or DOM entry that has a video but no picture is still a text card, without the video: give it a thumbnail to make it a video card.