UNPKG

shaka-player

Version:
280 lines (249 loc) • 8.69 kB
/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ goog.provide('shaka.msf.MSFTransport'); goog.require('shaka.log'); goog.require('shaka.msf.DialectRegistry'); goog.require('shaka.util.Error'); goog.require('shaka.util.IReleasable'); goog.requireType('shaka.msf.Utils'); /** * MOQT (Media over QUIC Transport). * * Owns the WebTransport connection and the choice of draft; everything that * depends on which draft was negotiated lives behind the session that the * dialect hands back. The methods here are pure delegation, so the manifest * parser above never sees a draft-specific concept. * * @implements {shaka.util.IReleasable} * @export */ shaka.msf.MSFTransport = class { /** * @param {!shaka.extern.MsfManifestConfiguration} config */ constructor(config) { /** @private {?WebTransport} */ this.webTransport_ = null; /** @private {!shaka.extern.MsfManifestConfiguration} */ this.config_ = config; /** @private {?shaka.extern.MsfSession} */ this.session_ = null; /** @private {?shaka.extern.MsfCodec} */ this.codec_ = null; /** * Resolves with the reason the MoQT session ended. A session the peer * hangs up on -- one whose SETUP it rejects, say -- fails everything in * flight with the same opaque WebTransport error, so the reason the * session itself gives is the only useful one. * * @private {!Promise.PromiseWithResolvers<string>} */ this.sessionEnded_ = Promise.withResolvers(); } /** * @param {!shaka.extern.MsfManifestConfiguration} config */ configure(config) { this.config_ = config; this.session_?.configure(config); } /** * @return {!shaka.extern.MsfManifestConfiguration} */ getConfiguration() { return this.config_; } /** * @param {string} uri * @param {?Uint8Array} fingerprint * @param {?string=} authorizationToken * @return {!Promise<!shaka.extern.MsfSession>} */ async connect(uri, fingerprint, authorizationToken) { if (!window.WebTransport) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.WEBTRANSPORT_NOT_AVAILABLE); } const options = { allowPooling: false, congestionControl: 'low-latency', }; if (fingerprint) { options.serverCertificateHashes = [ { algorithm: 'sha-256', value: fingerprint, }, ]; } // Offer every draft this build speaks that the configuration allows, // newest first, as WebTransport subprotocols. const offered = shaka.msf.DialectRegistry.getForVersion(this.config_.version); let webTransport = await this.open_( uri, options, offered.map((dialect) => dialect.getSubprotocol())); let dialect = shaka.msf.DialectRegistry.select(offered, webTransport.protocol); if (!dialect) { // The server took one of our offers but did not say which. The opening // handshake is itself a version negotiator — a server rejects a // connection whose subprotocols it does not speak — so ask again with // one draft at a time, newest first, and let the first server that // accepts answer the question the echo did not. shaka.log.debug( 'Server accepted a MoQT subprotocol without echoing it; ' + 'negotiating one draft at a time'); webTransport.close(); /** @type {?WebTransport} */ let negotiated = null; for (const candidate of offered) { try { // The attempts are a negotiation: each one asks whether the server // speaks this draft, so they must run in order, newest first. // eslint-disable-next-line no-await-in-loop negotiated = await this.open_( uri, options, [candidate.getSubprotocol()]); } catch (error) { shaka.log.debug( `Server rejected ${candidate.getSubprotocol()}`, error); continue; } dialect = candidate; break; } if (!dialect || !negotiated) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, shaka.util.Error.Code.WEBTRANSPORT_INITIALIZATION_FAILED, 'Server accepted no MoQT subprotocol on its own'); } webTransport = negotiated; } this.webTransport_ = webTransport; this.watchForSessionEnd_(webTransport); shaka.log.info(`Connection established with ${dialect.getName()}`); this.codec_ = dialect.getCodec(); this.session_ = await dialect.connect( this.webTransport_, this.config_, authorizationToken); return this.session_; } /** * Opens a WebTransport connection offering the given subprotocols, and * resolves once it is ready. * * @param {string} uri * @param {!Object} options The connection options, minus the subprotocols. * @param {!Array<string>} protocols * @return {!Promise<!WebTransport>} * @private */ async open_(uri, options, protocols) { const webTransport = new WebTransport(uri, Object.assign({}, options, {protocols})); // Attach a handler to wt.closed up front. When the handshake fails (e.g. // self-signed cert with no fingerprint to pin against), Safari rejects // both wt.ready and wt.closed; without a handler on closed, Safari // surfaces an "Unhandled Promise Rejection: WebTransportError". The // rejection is also seen later by Connection.closed() — promises stay // rejected, so multiple handlers each see the same value. webTransport.closed.catch(() => {}); await webTransport.ready; shaka.log.v1( `WebTransport connection established offering ${protocols.join(', ')}`); return webTransport; } /** * @param {!WebTransport} webTransport * @private */ watchForSessionEnd_(webTransport) { webTransport.closed.then( (info) => { const closeInfo = /** @type {{closeCode: number, reason: string}} */ ( info || {closeCode: 0, reason: ''}); this.sessionEnded_.resolve( `session closed with code ${closeInfo.closeCode}` + (closeInfo.reason ? `: ${closeInfo.reason}` : '')); }, (error) => { this.sessionEnded_.resolve(String(error)); }); } /** * Resolves with the reason the MoQT session ended, and stays pending while * it is alive. * * @return {!Promise<string>} */ waitForSessionEnd() { return this.sessionEnded_.promise; } /** * The primitive codec of the draft that was negotiated, or null before a * connection has been made. Packagings need it to read the MoQT-encoded * fields their objects carry. * * @return {?shaka.extern.MsfCodec} */ getCodec() { return this.codec_; } /** @override */ release() { shaka.log.v1('Closing client connection'); this.session_?.release(); this.session_ = null; this.codec_ = null; } /** * Subscribe to a track by namespace and track name * * @param {Array<string>} namespace * @param {string} trackName * @param {shaka.msf.Utils.ObjectCallback} callback * @param {?shaka.msf.Utils.Location=} startLocation Where delivery should * begin, for a subscription that is not meant to start at the live edge. * @param {boolean=} joinCurrentGroup Also deliver the current Group from * its first Object. See shaka.extern.MsfSession#subscribe. * @return {!Promise<bigint>} */ subscribeTrack(namespace, trackName, callback, startLocation, joinCurrentGroup) { if (!this.session_) { throw new Error('Cannot subscribe: not connected'); } return this.session_.subscribe( namespace, trackName, callback, startLocation, joinCurrentGroup); } /** * Unsubscribe from a track by track alias * * @param {bigint} trackAlias * @return {!Promise} */ unsubscribeTrack(trackAlias) { if (!this.session_) { throw new Error('Cannot unsubscribe: not connected'); } return this.session_.unsubscribe(trackAlias); } /** * Register a callback to be notified when a PublishNamespace message is * received. * * @param {function(Array<string>)} callback * @return {function()} A function to unregister the callback */ registerPublishNamespaceCallback(callback) { if (!this.session_) { throw new Error('Cannot register callback: not connected'); } return this.session_.onNamespacePublished(callback); } };