From 92f0a956b9305cc22a6bd309bbed493a333b791f Mon Sep 17 00:00:00 2001 From: Calvin Montgomery Date: Tue, 8 Aug 2017 20:46:10 -0700 Subject: [PATCH] custom-media: import spec and fix a minor missed validation --- docs/custom-media.md | 160 +++++++++++++++++++++++++++++++++++++++++++ src/custom-media.js | 13 +++- test/custom-media.js | 10 +++ 3 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 docs/custom-media.md diff --git a/docs/custom-media.md b/docs/custom-media.md new file mode 100644 index 00000000..beee96b1 --- /dev/null +++ b/docs/custom-media.md @@ -0,0 +1,160 @@ +CyTube Custom Content Metadata +============================== + +Last Updated: 2017-08-08 + +Status: Experimental + +## Purpose ## + +CyTube currently supports adding custom audio/video content by allowing the user +to supply a direct URL to an audio/video file. The server uses `ffprobe` to +probe the file for various metadata, including the codec/container format and +the duration. This approach has a few disadvantages over the officially +supported media providers, namely: + + * Since it accepts a single file, it is not possible to provide multiple + source URLs with varying formats or bitrates to allow viewers to select the + best source for their computer. + - It also means it is not possible to provide text tracks for subtitles or + closed captioning, or to provide image URLs for thumbnails/previews. + * Probing the file with `ffprobe` is slow, especially if the content is hosted + in a far away network location, which at best is inconvenient and at worst + results in timeouts and inability to add the content. + * Parsing the `ffprobe` output is inexact, and may sometimes result in + detecting the wrong format, or failing to detect the title. + +This document specifies a new supported media provider which allows users to +provide a JSON manifest specifying the metadata for custom content in a way that +avoids the above issues and is more flexible for extension. + +## Manifest Format ## + +To add custom content, the user provides a JSON object with the following keys: + + * `title`: A nonempty string specifying the title of the content. For legacy + reasons, CyTube currently truncates this to 100 UTF-8 characters. + * `duration`: A non-negative, finite number specifying the duration, in + seconds, of the content. This is what the server will use for timing + purposes. Decimals are allowed, but CyTube's timer truncates the value as + an integer number of seconds, so including fractional seconds lends no + advantage. + * `live`: An optional boolean (default: `false`) indicating whether the + content is live or pre-recorded. For live content, the `duration` is + ignored, and the server won't advance the playlist automatically. + * `thumbnail`: An optional string specifying a URL for a thumbnail image of + the content. CyTube currently does not support displaying thumbnails in the + playlist, but this functionality may be offered in the future. + * `sources`: A nonempty list of playable sources for the content. The format + is described below. + * `textTracks`: An optional list of text tracks for subtitles or closed + captioning. The format is described below. + +### Source Format ### + +Each source entry is a JSON object with the following keys: + + * `url`: A valid URL that browsers can use to retrieve the content. The URL + must resolve to a publicly-routed IP address, and must the `https:` scheme. + * `contentType`: A string representing the MIME type of the content at `url`. + A list of acceptable MIME types is provided below. + * `quality`: A number representing the quality level of the source. The + supported quality levels are `240`, `360`, `480`, `540`, `720`, `1080`, + `1440`, and `2160`. This may be extended in the future. + * `bitrate`: An optional number indicating the bitrate (in Kbps) of the + content. It must be a positive, finite number if provided. The bitrate is + not currently used by CyTube, but may be used by extensions or custom + scripts to determine whether this source is feasible to play on the viewer's + internet connection. + +#### Acceptable MIME Types #### + +The following MIME types are accepted for the `contentType` field: + + * `video/mp4` + * `video/webm` + * `video/ogg` + * `application/x-mpegURL` (HLS streams) + - HLS is only supported for livestreams. Metadata with HLS sources + but without `live: true` will be rejected. + * ~~`rtmp/flv`~~ + - In light of Adobe phasing out support for Flash, and many browsers + already dropping support, RTMP is not supported by this feature. + RTMP streams are only supported through the existing `rt:` media + type. + * `audio/aac` + * `audio/ogg` + * `audio/mpeg` + +Other audio or video formats, such as AVI, MKV, and FLAC, are not supported due +to lack of common support across browsers for playing these formats. For more +information, refer to +[MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats#Browser_compatibility). + +### Text Track Format ### + +Each text track entry is a JSON object with the following keys: + + + * `url`: A valid URL that browsers can use to retrieve the track. The URL + must resolve to a publicly-routed IP address, and must the `https:` scheme. + * `contentType`: A string representing the MIME type of the track at `url`. + The only currently supported MIME type is + [`text/vtt`](https://developer.mozilla.org/en-US/docs/Web/API/WebVTT_API). + * `name`: A name for the text track. This is displayed in the menu for the + viewer to select a text track. + +**Important note regarding text tracks and CORS:** + +By default, browsers block requests for WebVTT tracks hosted on different +domains than the current page. In order for text tracks to work cross-origin, +the `Access-Control-Allow-Origin` header needs to be set by the remote server +when serving the VTT file. See +[MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) +for more information about setting this header. + +## Example ## + + { + "title": "Test Video", + "duration": 10, + "live": false, + "thumbnail": "https://example.com/thumb.jpg", + "sources": [ + { + "url": "https://example.com/video.mp4", + "contentType": "video/mp4", + "quality": 1080, + "bitrate": 5000 + } + ], + "textTracks": [ + { + "url": "https://example.com/subtitles.vtt", + "contentType": "text/vtt", + "name": "English Subtitles" + } + ] + } + +## Permissions ## + +The permission node to allow users to add custom content is the same as the +permission node for the existing raw file support. Custom content is considered +as an extension of the existing feature. + +## Unsupported/Undefined Behavior ## + +The behavior under any the following circumstances is not defined by this +specification, and any technical support in these cases is voided. This list is +non-exhaustive. + + * Source URLs or text track URLs are hosted on a third-party website that does + not have knowledge of its content being played on CyTube. + * The webserver hosting the source or text track URLs serves a different MIME + type than the one specified in the manifest. + * The webserver hosting the source or text track URLs serves a file that does + not match the MIME type specified in the `Content-Type` HTTP header returned + to the browser. + * The manifest includes source URLs or text track URLs with expiration times, + session IDs, etc. in the URL querystring. diff --git a/src/custom-media.js b/src/custom-media.js index 149f6b0d..68482e07 100644 --- a/src/custom-media.js +++ b/src/custom-media.js @@ -29,6 +29,10 @@ const SOURCE_CONTENT_TYPES = new Set([ 'video/webm' ]); +const LIVE_ONLY_CONTENT_TYPES = new Set([ + 'application/x-mpegURL' +]); + export function lookup(url, opts) { if (!opts) opts = {}; if (!opts.hasOwnProperty('timeout')) opts.timeout = 10000; @@ -158,11 +162,11 @@ export function validate(data) { validateURL(data.thumbnail); } - validateSources(data.sources); + validateSources(data.sources, data); validateTextTracks(data.textTracks); } -function validateSources(sources) { +function validateSources(sources, data) { if (!Array.isArray(sources)) throw new ValidationError('sources must be a list'); if (sources.length === 0) @@ -178,6 +182,11 @@ function validateSources(sources) { `unacceptable source contentType "${source.contentType}"` ); + if (LIVE_ONLY_CONTENT_TYPES.has(source.contentType) && !data.live) + throw new ValidationError( + `contentType "${source.contentType}" requires live: true` + ); + if (!SOURCE_QUALITIES.has(source.quality)) throw new ValidationError(`unacceptable source quality "${source.quality}"`); diff --git a/test/custom-media.js b/test/custom-media.js index 30d8d4e9..96922432 100644 --- a/test/custom-media.js +++ b/test/custom-media.js @@ -89,6 +89,16 @@ describe('custom-media', () => { assert.throws(() => validate(invalid), /URL protocol must be HTTPS/); }); + + it('rejects non-live HLS', () => { + invalid.live = false; + invalid.sources[0].contentType = 'application/x-mpegURL'; + + assert.throws( + () => validate(invalid), + /contentType "application\/x-mpegURL" requires live: true/ + ); + }); }); describe('#validateSources', () => {