Source: lib/msf/msf_transport.js

/*! @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;
  }

  /**
   * @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);
    options.protocols = offered.map((dialect) => dialect.getSubprotocol());

    this.webTransport_ = new WebTransport(uri, options);
    // 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.
    this.webTransport_.closed.catch(() => {});
    await this.webTransport_.ready;
    shaka.log.v1('WebTransport connection established');

    const dialect =
        shaka.msf.DialectRegistry.select(offered, this.webTransport_.protocol);
    shaka.log.info(`Connection established with ${dialect.getName()}`);

    this.session_ = await dialect.connect(
        this.webTransport_, this.config_, authorizationToken);

    return this.session_;
  }

  /** @override */
  release() {
    shaka.log.v1('Closing client connection');
    this.session_?.release();
    this.session_ = null;
  }

  /**
   * Subscribe to a track by namespace and track name
   *
   * @param {Array<string>} namespace
   * @param {string} trackName
   * @param {shaka.msf.Utils.ObjectCallback} callback
   * @return {!Promise<bigint>}
   */
  subscribeTrack(namespace, trackName, callback) {
    if (!this.session_) {
      throw new Error('Cannot subscribe: not connected');
    }
    return this.session_.subscribe(namespace, trackName, callback);
  }

  /**
   * 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);
  }

  /**
   * Fetch a track (one-shot retrieval instead of ongoing subscription)
   *
   * @param {Array<string>} namespace
   * @param {string} trackName
   * @param {shaka.msf.Utils.ObjectCallback} callback
   * @return {!Promise}
   */
  fetchTrack(namespace, trackName, callback) {
    if (!this.session_) {
      throw new Error('Cannot fetch: not connected');
    }
    return this.session_.fetch(namespace, trackName, callback);
  }

  /**
   * 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);
  }
};