UNPKG

shaka-player

Version:
339 lines (305 loc) • 8.57 kB
/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ goog.provide('shaka.msf.Reader'); goog.provide('shaka.msf.Writer'); goog.require('shaka.log'); goog.require('shaka.util.BufferUtils'); goog.require('shaka.util.IReleasable'); goog.require('shaka.util.StringUtils'); goog.requireType('shaka.msf.Utils'); /** * Reader wraps a stream and provides convenience methods for reading * pieces from a stream. * * @implements {shaka.util.IReleasable} */ shaka.msf.Reader = class { /** * @param {!Uint8Array} buffer * @param {!ReadableStream<!Uint8Array>} stream * @param {!shaka.extern.MsfCodec} codec The primitive codec of the negotiated * draft. Integer encoding differs between drafts, so the reader cannot * assume one. */ constructor(buffer, stream, codec) { /** @private {!Uint8Array} */ this.buffer_ = buffer; /** @private {!shaka.extern.MsfCodec} */ this.codec_ = codec; /** @private {!ReadableStreamDefaultReader<!Uint8Array>} */ this.reader_ = /** @type {!ReadableStreamDefaultReader<!Uint8Array>} */ ( stream.getReader()); } /** * @return {number} */ getByteLength() { return this.buffer_.byteLength; } /** * @return {!Uint8Array} */ getBuffer() { return shaka.util.BufferUtils.toUint8(this.buffer_); } /** * Adds more data to the buffer, returning true if more data was added. * * @return {!Promise<boolean>} * @private */ async fill_() { const result = await this.reader_.read(); if (result.done) { return false; } const buffer = shaka.util.BufferUtils.toUint8(result.value); if (this.buffer_.byteLength === 0) { this.buffer_ = buffer; } else { const temp = new Uint8Array(this.buffer_.byteLength + buffer.byteLength); temp.set(this.buffer_); temp.set(buffer, this.buffer_.byteLength); this.buffer_ = temp; } return true; } /** * Add more data to the buffer until it's at least size bytes. * * @param {number} size * @return {!Promise} * @private */ async fillTo_(size) { while (this.buffer_.byteLength < size) { // eslint-disable-next-line no-await-in-loop if (!(await this.fill_())) { throw new Error('unexpected end of stream'); } } } /** * Consumes the first size bytes of the buffer. * * @param {number} size * @return {!Uint8Array} * @private */ slice_(size) { const result = shaka.util.BufferUtils.toUint8(this.buffer_, 0, size); this.buffer_ = shaka.util.BufferUtils.toUint8(this.buffer_, size); return result; } /** * @param {number} size * @return {!Promise<!Uint8Array>} */ async read(size) { if (size === 0) { return new Uint8Array([]); } await this.fillTo_(size); return this.slice_(size); } /** * @return {!Promise<!Uint8Array>} */ async readAll() { // eslint-disable-next-line no-empty,no-await-in-loop while (await this.fill_()) {} return this.slice_(this.buffer_.byteLength); } /** * @return {!Promise<!Array<string>>} */ async tuple() { // Get the count of tuple elements const count = await this.u53(); // Read each tuple element individually const tupleElements = []; for (let i = 0; i < count; i++) { // Each element is a var int length followed by that many bytes // eslint-disable-next-line no-await-in-loop const length = await this.u53(); // eslint-disable-next-line no-await-in-loop const bytes = await this.read(length); const element = shaka.util.StringUtils.fromUTF8(bytes); tupleElements.push(element); } return tupleElements; } /** * @param {(number|undefined)=} maxLength * @return {!Promise<string>} */ async string(maxLength) { const length = await this.u53(); if (maxLength !== undefined && length > maxLength) { throw new Error( `string length ${length} exceeds max length ${maxLength}`); } const buffer = await this.read(length); return shaka.util.StringUtils.fromUTF8(buffer); } /** * @return {!Promise<number>} */ async u8() { await this.fillTo_(1); return this.slice_(1)[0]; } /** * @return {!Promise<boolean>} */ async u8Bool() { return (await this.u8()) !== 0; } /** * Returns a Number using 53-bits, the max Javascript can use for integer math * @return {!Promise<number>} */ async u53() { const result = await this.u53WithSize(); return result.value; } /** * Returns a Number using 53-bits and tracks the number of bytes read * @return {!Promise<{value: number, bytesRead: number}>} */ async u53WithSize() { const result = await this.u62WithSize(); const v = result.value; if (v > Number.MAX_SAFE_INTEGER) { throw new Error('value larger than 53-bits; use v62 instead'); } return {value: Number(v), bytesRead: result.bytesRead}; } /** * If the number is greater than 53 bits, it throws an error. * * @return {!Promise<bigint>} */ async u62() { const result = await this.u62WithSize(); return result.value; } /** * Returns a number and tracks the number of bytes read * * @return {!Promise<{value: bigint, bytesRead: number}>} */ async u62WithSize() { await this.fillTo_(1); const bytesRead = this.codec_.varIntLength(this.buffer_[0]); await this.fillTo_(bytesRead); const value = this.codec_.decodeVarInt(this.slice_(bytesRead)); return {value, bytesRead}; } /** * @return {!Promise<!Array<shaka.msf.Utils.KeyValuePair>>} */ async deltaKeyValuePairs() { const count = await this.u53(); /** @type {!Array<shaka.msf.Utils.KeyValuePair>} */ const params = []; let prevType = BigInt(0); for (let i = 0; i < count; i++) { // eslint-disable-next-line no-await-in-loop const delta = await this.u62(); const paramType = prevType + delta; prevType = paramType; if (paramType % BigInt(2) === BigInt(0)) { // eslint-disable-next-line no-await-in-loop const value = await this.u62(); params.push({ type: paramType, value, }); } else { // eslint-disable-next-line no-await-in-loop const length = await this.u53(); // eslint-disable-next-line no-await-in-loop const value = await this.read(length); params.push({ type: paramType, value, }); } } return params; } /** * @return {!Promise<boolean>} */ async done() { if (this.buffer_.byteLength > 0) { return false; } return !(await this.fill_()); } /** * Cancels the underlying stream. This is best-effort cleanup: when the * session is already gone, cancelling rejects with the same error that * tore the session down, and there is nothing left to do about it. Callers * fire this from a finally block without awaiting it, so a rejection here * would surface as an unhandled rejection. * * This cancels through the reader rather than releasing the lock first: * older browsers (e.g. Chromecast) throw from releaseLock() while a read() * is still pending, which is the normal state of a stream being listened * on. Cancelling settles any pending read() with done, on every browser. * * @return {!Promise} */ async close() { try { await this.reader_.cancel('Reader closed'); } catch (error) { shaka.log.debug('Ignoring error while cancelling a stream:', error); } } /** * @override */ release() { this.reader_.releaseLock(); } }; /** * Writer wraps a stream and writes chunks of data. */ shaka.msf.Writer = class { /** * @param {!WritableStream} stream */ constructor(stream) { /** @private {!WritableStream} */ this.stream_ = stream; /** @private {!WritableStreamDefaultWriter} */ this.writer_ = stream.getWriter(); } /** * @param {!Uint8Array} value * @return {!Promise} */ async write(value) { await this.writer_.write(value); } /** * Aborts the underlying stream. Like Reader.close(), this is best-effort * cleanup: once the session is gone there is nothing left to abort. * * @return {!Promise} */ async close() { try { await this.writer_.abort('Writer closed'); } catch (error) { shaka.log.debug('Ignoring error while aborting a stream:', error); } } };