UNPKG

shaka-player

Version:
554 lines (508 loc) • 17.2 kB
/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ goog.provide('shaka.msf.draft18.MessageType'); goog.provide('shaka.msf.draft18.MessageTypeId'); goog.provide('shaka.msf.draft18.MessageWriter'); goog.require('shaka.msf.Utils'); goog.require('shaka.util.BufferUtils'); goog.require('shaka.util.DataViewWriter'); goog.require('shaka.util.StringUtils'); /** * Message type IDs for draft-18. * * Deliberately owned by this draft family rather than shared. The drafts * reassign IDs and, more dangerously, reuse them with different meanings: 0x7 * was PUBLISH_NAMESPACE_OK and is now the generic REQUEST_OK, 0x8 was * PUBLISH_NAMESPACE_ERROR and is now NAMESPACE, 0xE was TRACK_STATUS_OK and is * now NAMESPACE_DONE, 0xF was TRACK_STATUS_ERROR and is now PUBLISH_BLOCKED, * and SUBSCRIBE_NAMESPACE moved from 0x11 to 0x50. A shared enum would * mis-parse silently instead of failing. * * Note the values are var ints, not single bytes: SETUP is 0x2F00. * * @enum {number} */ shaka.msf.draft18.MessageTypeId = { SETUP: 0x2f00, GOAWAY: 0x10, SUBSCRIBE: 0x3, SUBSCRIBE_OK: 0x4, PUBLISH: 0x1d, PUBLISH_OK: 0x1e, PUBLISH_DONE: 0xb, FETCH: 0x16, FETCH_OK: 0x18, TRACK_STATUS: 0xd, PUBLISH_NAMESPACE: 0x6, SUBSCRIBE_NAMESPACE: 0x50, SUBSCRIBE_TRACKS: 0x51, NAMESPACE: 0x8, NAMESPACE_DONE: 0xe, PUBLISH_BLOCKED: 0xf, REQUEST_UPDATE: 0x2, REQUEST_OK: 0x7, REQUEST_ERROR: 0x5, }; /** * Internal discriminators for decoded draft-18 messages. * * @enum {string} */ shaka.msf.draft18.MessageType = { SETUP: 'setup', GOAWAY: 'goaway', SUBSCRIBE: 'subscribe', SUBSCRIBE_OK: 'subscribe_ok', PUBLISH: 'publish', PUBLISH_OK: 'publish_ok', PUBLISH_DONE: 'publish_done', FETCH: 'fetch', FETCH_OK: 'fetch_ok', TRACK_STATUS: 'track_status', PUBLISH_NAMESPACE: 'publish_namespace', SUBSCRIBE_NAMESPACE: 'subscribe_namespace', SUBSCRIBE_TRACKS: 'subscribe_tracks', NAMESPACE: 'namespace', NAMESPACE_DONE: 'namespace_done', PUBLISH_BLOCKED: 'publish_blocked', REQUEST_UPDATE: 'request_update', REQUEST_OK: 'request_ok', REQUEST_ERROR: 'request_error', }; /** * Fetch Type values, from draft-18 section 10.12. * * @enum {number} */ shaka.msf.draft18.FetchType = { STANDALONE: 0x1, RELATIVE_JOINING: 0x2, ABSOLUTE_JOINING: 0x3, }; /** * Serializes draft-18 control messages. * * Every message is a var int type, a 16-bit length, then the payload. Two * things differ structurally from draft-16 beyond the field layouts: * * - The type is a var int rather than a single byte, because SETUP is 0x2F00. * - Responses carry no Request ID. They arrive on the bidirectional stream * that carried the request, and that stream is the correlation, so there is * nothing to write. * * Draft-20 changed one message body and nothing else, so the field writers and * the type/length framing are protected rather than private: draft-20 extends * this class and overrides only marshalFetch(). */ shaka.msf.draft18.MessageWriter = class { /** * @param {!shaka.extern.MsfCodec} codec * @param {number=} initialSize */ constructor(codec, initialSize = 1024) { /** @private {!shaka.extern.MsfCodec} */ this.codec_ = codec; /** @private {!shaka.util.DataViewWriter} */ this.writer_ = new shaka.util.DataViewWriter( initialSize, shaka.util.DataViewWriter.Endianness.BIG_ENDIAN); } /** * @return {!Uint8Array} */ getBytes() { return this.writer_.getBytes(); } /** * @return {!shaka.extern.MsfCodec} * @protected */ getCodec() { return this.codec_; } /** Resets the buffer to start writing from the beginning. */ reset() { this.writer_.reset(); } /** * @param {bigint|number} value * @protected */ writeVarInt(value) { this.codec_.encodeVarInt( this.writer_, typeof value == 'bigint' ? value : BigInt(value)); } /** * @param {string} str * @protected */ writeString(str) { const bytes = shaka.util.BufferUtils.toUint8( shaka.util.StringUtils.toUTF8(str)); this.writeVarInt(bytes.length); this.writer_.writeBytes(bytes); } /** * A Track Namespace is a count of fields followed by each length-prefixed * field. * * @param {Array<string>} namespace * @protected */ writeNamespace(namespace) { const fields = namespace || []; this.writeVarInt(fields.length); for (const field of fields) { this.writeString(field); } } /** * @param {shaka.msf.Utils.Location} location * @protected */ writeLocation(location) { this.writeVarInt(location.group); this.writeVarInt(location.object); } /** * Writes delta-encoded Key-Value-Pairs without a count prefix, which is how * Setup Options are framed: they span the whole message payload and are * bounded by the message length. * * Draft-22 gave LOCATION_FILTER a value that leads with its own Location * Filter Type rather than a length, so a parameter list asks * getParameterEncoding() whether an odd type's value carries a length. * Setup Options always do. * * @param {!Array<shaka.msf.Utils.KeyValuePair>} pairs * @param {boolean=} isParameterList * @protected */ writeKeyValuePairs(pairs, isParameterList = false) { const Encoding = shaka.msf.draft18.MessageWriter.ParameterEncoding; // Delta encoding requires ascending type order. /** @type {!Array<shaka.msf.Utils.KeyValuePair>} */ const sorted = [...pairs].sort( (a, b) => (a.type < b.type ? -1 : (a.type > b.type ? 1 : 0))); let prevType = BigInt(0); for (const pair of sorted) { this.writeVarInt(pair.type - prevType); prevType = pair.type; if (pair.type % BigInt(2) === BigInt(0)) { if (typeof pair.value !== 'bigint') { throw new Error( `Invalid value for even key ${pair.type}: expected bigint`); } this.writeVarInt(pair.value); } else { if (!ArrayBuffer.isView(pair.value)) { throw new Error( `Invalid value for odd key ${pair.type}: expected Uint8Array`); } const bytes = /** @type {!Uint8Array} */ (pair.value); const selfDelimited = isParameterList && this.getParameterEncoding(Number(pair.type)) == Encoding.LOCATION_FILTER; if (!selfDelimited) { this.writeVarInt(bytes.byteLength); } this.writer_.writeBytes(bytes); } } } /** * Writes a count-prefixed parameter list, which is how every message other * than SETUP frames its parameters. * * @param {(Array<shaka.msf.Utils.KeyValuePair>|undefined)} pairs * @protected */ writeParameters(pairs) { const params = pairs || []; this.writeVarInt(params.length); this.writeKeyValuePairs(params, /* isParameterList= */ true); } /** * How this draft encodes the value of a Message Parameter type, or null for * a type it does not define. Since draft-18 that is set per type rather * than by the parity rule Properties use, so a type nobody told us about * cannot be stepped over when reading a response. * * @param {number} type * @return {?shaka.msf.draft18.MessageWriter.ParameterEncoding} */ getParameterEncoding(type) { return shaka.msf.draft18.MessageWriter.PARAMETER_ENCODINGS_.get(type) ?? null; } /** * Writes the var int type and 16-bit length around a payload. * * @param {shaka.msf.draft18.MessageTypeId} type * @param {function()} writeContent * @return {!shaka.msf.draft18.MessageWriter} * @protected */ marshal(type, writeContent) { this.writeVarInt(type); const lengthPosition = this.writer_.getPosition(); this.writer_.writeUint16(0); // Placeholder const contentStart = this.writer_.getPosition(); writeContent(); this.writer_.patchUint16( lengthPosition, this.writer_.getPosition() - contentStart); return this; } /** * Draft-17 collapsed CLIENT_SETUP and SERVER_SETUP into one message whose * payload is nothing but Setup Options, uncounted and bounded by the * message length. * * @param {!Array<shaka.msf.Utils.KeyValuePair>} options * @return {!shaka.msf.draft18.MessageWriter} */ marshalSetup(options) { return this.marshal(shaka.msf.draft18.MessageTypeId.SETUP, () => { this.writeKeyValuePairs(options); }); } /** * @param {{ * requestId: bigint, * namespace: Array<string>, * trackName: string, * params: (Array<shaka.msf.Utils.KeyValuePair>|undefined), * }} msg * @return {!shaka.msf.draft18.MessageWriter} */ marshalSubscribe(msg) { return this.marshal(shaka.msf.draft18.MessageTypeId.SUBSCRIBE, () => { this.writeVarInt(msg.requestId); this.writeNamespace(msg.namespace); this.writeString(msg.trackName); this.writeParameters(msg.params); }); } /** * A standalone FETCH. Start and End are Location structures rather than the * four loose var ints draft-16 used, and the range is a field of the message * rather than a parameter, which is what draft-20 changed. * * The range is an inclusive pair of absolute Locations, so the zeroed pair * the caller may leave out asks for exactly the Object at {0, 0}, not for * the whole track. Draft-20 says the same thing with a LOCATION_FILTER. * * @param {{ * requestId: bigint, * namespace: Array<string>, * trackName: string, * startLocation: (shaka.msf.Utils.Location|undefined), * endLocation: (shaka.msf.Utils.Location|undefined), * params: (Array<shaka.msf.Utils.KeyValuePair>|undefined), * }} msg * @return {!shaka.msf.draft18.MessageWriter} */ marshalFetch(msg) { const range = shaka.msf.draft18.MessageWriter.fetchRange(msg); return this.marshal(shaka.msf.draft18.MessageTypeId.FETCH, () => { this.writeVarInt(msg.requestId); this.writeVarInt(shaka.msf.draft18.FetchType.STANDALONE); this.writeNamespace(msg.namespace); this.writeString(msg.trackName); this.writeLocation(range.start); this.writeLocation(range.end); this.writeParameters(msg.params); }); } /** * A Relative Joining FETCH: the Groups leading up to a subscription's * Joining Location, contiguous with what the subscription delivers after it * (draft-18 section 10.12.2). Joining Start 0 asks for the current Group * from its first Object. * * @param {{ * requestId: bigint, * joiningRequestId: bigint, * joiningStart: bigint, * params: (Array<shaka.msf.Utils.KeyValuePair>|undefined), * }} msg * @return {!shaka.msf.draft18.MessageWriter} */ marshalJoiningFetch(msg) { return this.marshal(shaka.msf.draft18.MessageTypeId.FETCH, () => { this.writeVarInt(msg.requestId); this.writeVarInt(shaka.msf.draft18.FetchType.RELATIVE_JOINING); this.writeVarInt(msg.joiningRequestId); this.writeVarInt(msg.joiningStart); this.writeParameters(msg.params); }); } /** * The SUBSCRIBE parameter that also delivers the current Group from its * first Object, or null when this draft has none and a Joining FETCH has to * be sent instead, as it does in draft-18. * * @return {?shaka.msf.Utils.KeyValuePair} */ fillCurrentGroupParam() { return null; } /** * The parameter that makes a SUBSCRIBE start at a given Location rather than * at the live edge, which is what seeking into already-published content * needs. * * Draft-18 spells it SUBSCRIPTION_FILTER (0x21): a Filter Type followed by * the fields that type takes. AbsoluteStart takes a Start Location and no * end, so the subscription runs from there to the live edge and onwards, and * the draft says outright that the Start Location MAY be smaller than the * publisher's Largest Object (section 5.1.2). Draft-20 keeps the parameter * type and changes its contents, so the draft-20 writer overrides this. * * @param {shaka.msf.Utils.Location} startLocation * @return {shaka.msf.Utils.KeyValuePair} */ locationFilterParam(startLocation) { return { type: BigInt(shaka.msf.draft18.MessageWriter.Parameter.LOCATION_FILTER), value: this.encodeVarIntField([ BigInt(shaka.msf.draft18.MessageWriter.FilterType.ABSOLUTE_START), startLocation.group, startLocation.object, ]), }; } /** * The Location range a FETCH covers, filling in the default the callers that * do not care rely on. Shared with draft-20, which carries the same range in * a parameter instead of in the message. * * @param {{ * startLocation: (shaka.msf.Utils.Location|undefined), * endLocation: (shaka.msf.Utils.Location|undefined), * }} msg * @return {{ * start: shaka.msf.Utils.Location, * end: shaka.msf.Utils.Location, * }} * @protected */ static fetchRange(msg) { const origin = {group: BigInt(0), object: BigInt(0)}; return { start: msg.startLocation || origin, end: msg.endLocation || origin, }; } /** * Encodes a sequence of var ints into a standalone byte field, for the * parameters whose value is itself a var int structure rather than a single * integer. * * @param {!Array<bigint>} values * @return {!Uint8Array} * @protected */ encodeVarIntField(values) { // Nine bytes is the widest a var int gets in this draft family. const writer = new shaka.util.DataViewWriter( 9 * values.length, shaka.util.DataViewWriter.Endianness.BIG_ENDIAN); for (const value of values) { this.codec_.encodeVarInt(writer, value); } return writer.getBytes(); } /** * @param {{ * requestId: bigint, * namespace: Array<string>, * params: (Array<shaka.msf.Utils.KeyValuePair>|undefined), * }} msg * @return {!shaka.msf.draft18.MessageWriter} */ marshalSubscribeNamespace(msg) { return this.marshal( shaka.msf.draft18.MessageTypeId.SUBSCRIBE_NAMESPACE, () => { this.writeVarInt(msg.requestId); this.writeNamespace(msg.namespace); this.writeParameters(msg.params); }); } /** * REQUEST_OK answers PUBLISH_NAMESPACE, SUBSCRIBE_NAMESPACE, TRACK_STATUS * and the rest. It carries no Request ID: it goes back on the request's own * bidirectional stream. * * @param {(Array<shaka.msf.Utils.KeyValuePair>|undefined)=} params * @return {!shaka.msf.draft18.MessageWriter} */ marshalRequestOk(params) { return this.marshal(shaka.msf.draft18.MessageTypeId.REQUEST_OK, () => { this.writeParameters(params); // Track Properties are empty for everything but TRACK_STATUS_OK. }); } }; /** * The parameter types this player sends on a SUBSCRIBE or a FETCH. * * Draft-20 renamed 0x21 from SUBSCRIPTION_FILTER to LOCATION_FILTER and * changed what its value holds, but the code point is the same in both, so * there is one constant rather than one per draft. * * @enum {number} */ shaka.msf.draft18.MessageWriter.Parameter = { FORWARD: 0x10, SUBSCRIBER_PRIORITY: 0x20, LOCATION_FILTER: 0x21, FILL_PARAMETERS: 0x23, }; /** * The SUBSCRIPTION_FILTER Filter Types this player sends (draft-18 section * 5.1.2). Draft-20 and draft-21 have no Filter Type, and draft-22 numbers its * own differently. * * @enum {number} */ shaka.msf.draft18.MessageWriter.FilterType = { ABSOLUTE_START: 0x3, }; /** * How a Message Parameter's value is encoded (draft-18 section 10.2). * * @enum {number} */ shaka.msf.draft18.MessageWriter.ParameterEncoding = { UINT8: 0, VARINT: 1, LOCATION: 2, LENGTH_PREFIXED: 3, // A Location Filter Type followed by the var ints that type selects, with // no length (draft-22 section 9.20.9). Only draft-22 uses it. LOCATION_FILTER: 4, }; /** * The encoding of the Message Parameter types defined across draft-18, * draft-20 and draft-21. The types are numbers rather than bigint so that * building this map needs no BigInt at load time. * * @private @const {!Map<number, * shaka.msf.draft18.MessageWriter.ParameterEncoding>} */ shaka.msf.draft18.MessageWriter.PARAMETER_ENCODINGS_ = new Map([ [0x02, shaka.msf.draft18.MessageWriter.ParameterEncoding.VARINT], [0x03, shaka.msf.draft18.MessageWriter.ParameterEncoding.LENGTH_PREFIXED], [0x06, shaka.msf.draft18.MessageWriter.ParameterEncoding.VARINT], [0x08, shaka.msf.draft18.MessageWriter.ParameterEncoding.VARINT], [0x09, shaka.msf.draft18.MessageWriter.ParameterEncoding.LOCATION], [0x10, shaka.msf.draft18.MessageWriter.ParameterEncoding.UINT8], [0x20, shaka.msf.draft18.MessageWriter.ParameterEncoding.UINT8], [0x21, shaka.msf.draft18.MessageWriter.ParameterEncoding.LENGTH_PREFIXED], [0x22, shaka.msf.draft18.MessageWriter.ParameterEncoding.UINT8], [0x23, shaka.msf.draft18.MessageWriter.ParameterEncoding.LENGTH_PREFIXED], [0x32, shaka.msf.draft18.MessageWriter.ParameterEncoding.VARINT], [0x35, shaka.msf.draft18.MessageWriter.ParameterEncoding.UINT8], ]);