Source: lib/media/manifest_parser.js

/**
 * @license
 * Copyright 2016 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

goog.provide('shaka.media.ManifestParser');

goog.require('goog.Uri');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.net.NetworkingEngine');
goog.require('shaka.util.Error');
goog.require('shaka.util.Platform');


/**
 * @namespace shaka.media.ManifestParser
 * @summary An interface to register manifest parsers.
 * @exportDoc
 */


/**
 * Contains the parser factory functions indexed by MIME type.
 *
 * @type {!Object.<string, shaka.extern.ManifestParser.Factory>}
 */
shaka.media.ManifestParser.parsersByMime = {};


/**
 * Contains the parser factory functions indexed by file extension.
 *
 * @type {!Object.<string, shaka.extern.ManifestParser.Factory>}
 */
shaka.media.ManifestParser.parsersByExtension = {};


/**
 * Registers a manifest parser by file extension.
 *
 * @param {string} extension The file extension of the manifest.
 * @param {shaka.extern.ManifestParser.Factory} parserFactory The factory
 *   used to create parser instances.
 * @export
 */
shaka.media.ManifestParser.registerParserByExtension = function(
    extension, parserFactory) {
  shaka.media.ManifestParser.parsersByExtension[extension] = parserFactory;
};


/**
 * Registers a manifest parser by MIME type.
 *
 * @param {string} mimeType The MIME type of the manifest.
 * @param {shaka.extern.ManifestParser.Factory} parserFactory The factory
 *   used to create parser instances.
 * @export
 */
shaka.media.ManifestParser.registerParserByMime = function(
    mimeType, parserFactory) {
  shaka.media.ManifestParser.parsersByMime[mimeType] = parserFactory;
};


/**
 * Returns a map of manifest support for well-known types.
 *
 * @return {!Object.<string, boolean>}
 */
shaka.media.ManifestParser.probeSupport = function() {
  const ManifestParser = shaka.media.ManifestParser;
  let support = {};

  // Make sure all registered parsers are shown, but only for MSE-enabled
  // platforms where our parsers matter.
  if (shaka.util.Platform.supportsMediaSource()) {
    for (let type in ManifestParser.parsersByMime) {
      support[type] = true;
    }
    for (let type in ManifestParser.parsersByExtension) {
      support[type] = true;
    }
  }

  // Make sure all well-known types are tested as well, just to show an explicit
  // false for things people might be expecting.
  const testMimeTypes = [
    // DASH
    'application/dash+xml',
    // HLS
    'application/x-mpegurl',
    'application/vnd.apple.mpegurl',
    // SmoothStreaming
    'application/vnd.ms-sstr+xml',
  ];
  const testExtensions = {
    // DASH
    'mpd': 'application/dash+xml',
    // HLS
    'm3u8': 'application/x-mpegurl',
    // SmoothStreaming
    'ism': 'application/vnd.ms-sstr+xml',
  };

  for (let type of testMimeTypes) {
    // Only query our parsers for MSE-enabled platforms.  Otherwise, query a
    // temporary media element for native support for these types.
    if (shaka.util.Platform.supportsMediaSource()) {
      support[type] = !!ManifestParser.parsersByMime[type];
    } else {
      support[type] = shaka.util.Platform.supportsMediaType(type);
    }
  }

  for (let extension in testExtensions) {
    // Only query our parsers for MSE-enabled platforms.  Otherwise, query a
    // temporary media element for native support for these MIME type for the
    // extension.
    if (shaka.util.Platform.supportsMediaSource()) {
      support[extension] = !!ManifestParser.parsersByExtension[extension];
    } else {
      const type = testExtensions[extension];
      support[extension] = shaka.util.Platform.supportsMediaType(type);
    }
  }

  return support;
};


/**
 * Create a manifest parser to parse the manifest at |uri|.
 *
 * @param {string} uri
 * @param {!shaka.net.NetworkingEngine} netEngine
 * @param {shaka.extern.RetryParameters} retryParams
 * @param {?string} mimeType
 * @return {!Promise.<!shaka.extern.ManifestParser>}
 */
shaka.media.ManifestParser.create = async function(
    uri, netEngine, retryParams, mimeType) {
  try {
    const Factory = await shaka.media.ManifestParser.getFactory_(
        uri, netEngine, retryParams, mimeType);

    return new Factory();
  } catch (error) {
    goog.asserts.assert(
        error instanceof shaka.util.Error, 'Incorrect error type');

    // Regardless of what the error was, we need to upgrade it to a critical
    // error. We can't do anything if we can't create a manifest parser.
    error.severity = shaka.util.Error.Severity.CRITICAL;

    throw error;
  }
};


/**
 * Get a factory that can create a manifest parser that should be able to parse
 * the manifest at |uri|.
 *
 * @param {string} uri
 * @param {!shaka.net.NetworkingEngine} netEngine
 * @param {shaka.extern.RetryParameters} retryParams
 * @param {?string} mimeType
 * @return {!Promise.<shaka.extern.ManifestParser.Factory>}
 * @private
 */
shaka.media.ManifestParser.getFactory_ = async function(
    uri, netEngine, retryParams, mimeType) {
  const ManifestParser = shaka.media.ManifestParser;

  // Try using the MIME type we were given.
  if (mimeType) {
    const factory = ManifestParser.parsersByMime[mimeType.toLowerCase()];
    if (factory) {
      return factory;
    }

    shaka.log.warning(
        'Could not determine manifest type using MIME type ', mimeType);
  }

  const extension = ManifestParser.getExtension(uri);
  if (extension) {
    const factory = ManifestParser.parsersByExtension[extension];
    if (factory) {
      return factory;
    }

    shaka.log.warning(
        'Could not determine manifest type for extension ', extension);
  } else {
    shaka.log.warning('Could not find extension for ', uri);
  }

  if (!mimeType) {
    mimeType = await ManifestParser.getMimeType(uri, netEngine, retryParams);

    if (mimeType) {
      const factory = shaka.media.ManifestParser.parsersByMime[mimeType];
      if (factory) {
        return factory;
      }

      shaka.log.warning('Could not determine manifest type using MIME type',
                        mimeType);
    }
  }

  throw new shaka.util.Error(
      shaka.util.Error.Severity.CRITICAL,
      shaka.util.Error.Category.MANIFEST,
      shaka.util.Error.Code.UNABLE_TO_GUESS_MANIFEST_TYPE,
      uri);
};


/**
 * @param {string} uri
 * @param {!shaka.net.NetworkingEngine} netEngine
 * @param {shaka.extern.RetryParameters} retryParams
 * @return {!Promise.<string>}
 */
shaka.media.ManifestParser.getMimeType = async function(
    uri, netEngine, retryParams) {
  const type = shaka.net.NetworkingEngine.RequestType.MANIFEST;

  let request = shaka.net.NetworkingEngine.makeRequest([uri], retryParams);
  request.method = 'HEAD';

  let response = await netEngine.request(type, request).promise;

  // https://bit.ly/2K9s9kf says this header should always be available,
  // but just to be safe:
  const mimeType = response.headers['content-type'];
  return mimeType ? mimeType.toLowerCase().split(';').shift() : '';
};


/**
 * @param {string} uri
 * @return {string}
 */
shaka.media.ManifestParser.getExtension = function(uri) {
  const uriObj = new goog.Uri(uri);
  const uriPieces = uriObj.getPath().split('/');
  const uriFilename = uriPieces.pop();
  const filenamePieces = uriFilename.split('.');

  // Only one piece means there is no extension.
  if (filenamePieces.length == 1) {
    return '';
  }

  return filenamePieces.pop().toLowerCase();
};


/**
 * Determines whether or not this URI and MIME type are supported by our own
 * manifest parsers on this platform.  This takes into account whether or not
 * MediaSource is available, as well as which parsers are registered to the
 * system.
 *
 * @param {string} uri
 * @param {string} mimeType
 * @return {boolean}
 */
shaka.media.ManifestParser.isSupported = function(uri, mimeType) {
  // Without MediaSource, our own parsers are useless.
  if (!shaka.util.Platform.supportsMediaSource()) {
    return false;
  }

  if (mimeType in shaka.media.ManifestParser.parsersByMime) {
    return true;
  }

  const extension = shaka.media.ManifestParser.getExtension(uri);
  if (extension in shaka.media.ManifestParser.parsersByExtension) {
    return true;
  }

  return false;
};