UNPKG

shaka-player

Version:
426 lines (390 loc) 13.2 kB
/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ goog.provide('shaka.util.MimeUtils'); goog.require('shaka.device.DeviceFactory'); goog.require('shaka.device.IDevice'); goog.require('shaka.transmuxer.TransmuxerEngine'); goog.require('shaka.util.ManifestParserUtils'); /** * @summary A set of utility functions for dealing with MIME types. * @export */ shaka.util.MimeUtils = class { /** * Takes a MIME type and optional codecs string and produces the full MIME * type. Also remove the codecs for raw formats. * * @param {string} mimeType * @param {string=} codecs * @return {string} * @export */ static getFullType(mimeType, codecs) { let fullMimeType = mimeType; if (codecs && !shaka.util.MimeUtils.RAW_FORMATS.includes(mimeType)) { fullMimeType += '; codecs="' + codecs + '"'; } return fullMimeType; } /** * Takes a MIME type and optional codecs string and produces the full MIME * type. * * @param {string} mimeType * @param {string=} codecs * @return {string} * @export */ static getFullTypeWithAllCodecs(mimeType, codecs) { let fullMimeType = mimeType; if (codecs) { fullMimeType += '; codecs="' + codecs + '"'; } return fullMimeType; } /** * Takes a MIME type and a codecs string and produces the full MIME * type. If it's a transport stream, convert its codecs to MP4 codecs. * Otherwise for multiplexed content, convert the video MIME types to * their audio equivalents if the content type is audio. * * @param {string} mimeType * @param {string} codecs * @param {string} contentType * @return {string} */ static getFullOrConvertedType(mimeType, codecs, contentType) { const MimeUtils = shaka.util.MimeUtils; const fullMimeType = MimeUtils.getFullType(mimeType, codecs); const fullMimeTypeWithAllCodecs = MimeUtils.getFullTypeWithAllCodecs( mimeType, codecs); const ContentType = shaka.util.ManifestParserUtils.ContentType; const TransmuxerEngine = shaka.transmuxer.TransmuxerEngine; if (TransmuxerEngine.isSupported(fullMimeTypeWithAllCodecs, contentType)) { return TransmuxerEngine.convertCodecs( contentType, fullMimeTypeWithAllCodecs); } else if (mimeType != 'video/mp2t' && contentType == ContentType.AUDIO) { // video/mp2t is the correct mime type for TS audio, so only replace the // word "video" with "audio" for non-TS audio content. return fullMimeType.replace('video', 'audio'); } return fullMimeType; } /** * Takes a Stream object and produces an extended MIME type with information * beyond the container and codec type, when available. * * @param {shaka.extern.Stream} stream * @param {string} mimeType * @param {string} codecs * @return {string} */ static getExtendedType(stream, mimeType, codecs) { const components = [mimeType]; const extendedMimeParams = shaka.util.MimeUtils.EXTENDED_MIME_PARAMETERS_; extendedMimeParams.forEach((mimeKey, streamKey) => { const value = stream[streamKey]; if (streamKey == 'codecs') { if (shaka.util.MimeUtils.RAW_FORMATS.includes(stream.mimeType)) { // Skip codecs for raw formats } else { components.push('codecs="' + codecs + '"'); } } else if (value) { components.push(mimeKey + '="' + value + '"'); } }); if (stream.hdr == 'PQ') { components.push('eotf="smpte2084"'); } return components.join(';'); } /** * Takes a full MIME type (with codecs) or basic MIME type (without codecs) * and returns a container type string ("mp2t", "mp4", "webm", etc.) * * @param {string} mimeType * @return {string} */ static getContainerType(mimeType) { return mimeType.split(';')[0].split('/')[1]; } /** * Split a list of codecs encoded in a string into a list of codecs. * @param {string} codecs * @return {!Array<string>} */ static splitCodecs(codecs) { return codecs.split(','); } /** * Get the normalized codec from a codec string, * independently of their container. * * @param {string} codecString * @return {string} */ static getNormalizedCodec(codecString) { const parts = shaka.util.MimeUtils.getCodecParts_(codecString); const base = parts[0].toLowerCase(); const profile = parts[1].toLowerCase(); switch (true) { case base === 'mp4a' && profile === '69': case base === 'mp4a' && profile === '6b': case base === 'mp4a' && profile === '40.34': return 'mp3'; case base === 'mp4a' && profile === '66': case base === 'mp4a' && profile === '67': case base === 'mp4a' && profile === '68': case base === 'mp4a' && profile === '40.2': case base === 'mp4a' && profile === '40.02': case base === 'mp4a' && profile === '40.5': case base === 'mp4a' && profile === '40.05': case base === 'mp4a' && profile === '40.29': case base === 'mp4a' && profile === '40.42': // Extended HE-AAC return 'aac'; case base === 'mp4a' && profile === 'a5': case base === 'ac3': case base === 'ac-3': return 'ac-3'; // Dolby Digital case base === 'mp4a' && profile === 'a6': case base === 'eac3': case base === 'ec-3': return 'ec-3'; // Dolby Digital Plus case base === 'ac-4': return 'ac-4'; // Dolby AC-4 case base === 'mp4a' && profile === 'b2': return 'dtsx'; // DTS:X case base === 'mp4a' && profile === 'a9': return 'dtsc'; // DTS Digital Surround case base === 'vp09': case base === 'vp9': return 'vp9'; case base === 'avc1': case base === 'avc3': return 'avc'; // H264 case base === 'hvc1': case base === 'hev1': return 'hevc'; // H265 case base === 'vvc1': case base === 'vvi1': return 'vvc'; // H266 case base === 'dvh1': case base === 'dvhe': if (profile && profile.startsWith('05')) { return 'dovi-p5'; // Dolby Vision profile 5 } return 'dovi-hevc'; // Dolby Vision based in HEVC case base === 'dvav': case base === 'dva1': return 'dovi-avc'; // Dolby Vision based in AVC case base === 'dav1': return 'dovi-av1'; // Dolby Vision based in AV1 case base === 'dvc1': case base === 'dvi1': return 'dovi-vvc'; // Dolby Vision based in VVC case base === 'lvc1': return 'lcevc'; // LCEVC } return base; } /** * Get the base codec from a codec string. * * @param {string} codecString * @return {string} */ static getCodecBase(codecString) { const codecsBase = []; for (const codec of codecString.split(',')) { const parts = shaka.util.MimeUtils.getCodecParts_(codec); codecsBase.push(parts[0]); } return codecsBase.sort().join(','); } /** * Takes a full MIME type (with codecs) or basic MIME type (without codecs) * and returns a basic MIME type (without codecs or other parameters). * * @param {string} mimeType * @return {string} */ static getBasicType(mimeType) { return mimeType.split(';')[0]; } /** * Takes a MIME type and returns the codecs parameter, or an empty string if * there is no codecs parameter. * * @param {string} mimeType * @return {string} */ static getCodecs(mimeType) { // Parse the basic MIME type from its parameters. const pieces = mimeType.split(/ *; */); pieces.shift(); // Remove basic MIME type from pieces. const codecs = pieces.find((piece) => piece.startsWith('codecs=')); if (!codecs) { return ''; } // The value may be quoted, so remove quotes at the beginning or end. const value = codecs.split('=')[1].replace(/^"|"$/g, ''); return value; } /** * Checks if the given MIME type is HLS MIME type. * * @param {string} mimeType * @return {boolean} */ static isHlsType(mimeType) { return mimeType === 'application/x-mpegurl' || mimeType === 'application/vnd.apple.mpegurl' || mimeType === 'audio/x-mpegurl'; } /** * Checks if the given MIME type is DASH MIME type. * * @param {string} mimeType * @return {boolean} */ static isDashType(mimeType) { return mimeType === 'application/dash+xml' || mimeType === 'video/vnd.mpeg.dash.mpd'; } /** * Generates the correct audio codec for MediaDecodingConfiguration and * for MediaSource.isTypeSupported. * @param {string} codecs * @param {string} mimeType * @return {string} */ static getCorrectAudioCodecs(codecs, mimeType) { // According to RFC 6381 section 3.3, 'fLaC' is actually the correct // codec string. We still need to map it to 'flac', as some browsers // currently don't support 'fLaC', while 'flac' is supported by most // major browsers. // See https://bugs.chromium.org/p/chromium/issues/detail?id=1422728 const device = shaka.device.DeviceFactory.getDevice(); const webkit = shaka.device.IDevice.BrowserEngine.WEBKIT; const lowerCaseCodecs = codecs.toLowerCase(); if (lowerCaseCodecs == 'flac') { if (device.getBrowserEngine() != webkit) { return 'flac'; } else { return 'fLaC'; } } // The same is true for 'Opus'. if (lowerCaseCodecs === 'opus') { if (device.getBrowserEngine() != webkit) { return 'opus'; } else { if (shaka.util.MimeUtils.getContainerType(mimeType) == 'mp4') { return 'Opus'; } else { return 'opus'; } } } if (lowerCaseCodecs == 'ac-3' && device.requiresEC3InitSegments()) { return 'ec-3'; } return codecs; } /** * Generates the correct video codec for MediaDecodingConfiguration and * for MediaSource.isTypeSupported. * @param {string} codec * @return {string} */ static getCorrectVideoCodecs(codec) { if (codec.includes('avc1')) { // Convert avc1 codec string from RFC-4281 to RFC-6381 for // MediaSource.isTypeSupported // Example, convert avc1.66.30 to avc1.42001e (0x42 == 66 and 0x1e == 30) const avcData = codec.split('.'); if (avcData.length == 3) { let result = avcData.shift() + '.'; result += parseInt(avcData.shift(), 10).toString(16); result += ('000' + parseInt(avcData.shift(), 10).toString(16)).slice(-4); return result; } } else if (codec == 'vp9') { // MediaCapabilities supports 'vp09...' codecs, but not 'vp9'. Translate // vp9 codec strings into 'vp09...', to allow such content to play with // mediaCapabilities enabled. // This means profile 0, level 4.1, 8-bit color. This supports 1080p @ // 60Hz. See https://en.wikipedia.org/wiki/VP9#Levels // // If we don't have more detailed codec info, assume this profile and // level because it's high enough to likely accommodate the parameters we // do have, such as width and height. If an implementation is checking // the profile and level very strictly, we want older VP9 content to // still work to some degree. But we don't want to set a level so high // that it is rejected by a hardware decoder that can't handle the // maximum requirements of the level. // // This became an issue specifically on Firefox on M1 Macs. return 'vp09.00.41.08'; } return codec; } /** * Get the base and profile of a codec string. Where [0] will be the codec * base and [1] will be the profile. * @param {string} codecString * @return {!Array<string>} * @private */ static getCodecParts_(codecString) { const parts = codecString.split('.'); const base = parts[0]; parts.shift(); const profile = parts.join('.'); // Make sure that we always return a "base" and "profile". return [base, profile]; } }; /** * A map from Stream object keys to MIME type parameters. These should be * ignored by platforms that do not recognize them. * * This initial set of parameters are all recognized by Chromecast. * * @const {!Map<string, string>} * @private */ shaka.util.MimeUtils.EXTENDED_MIME_PARAMETERS_ = new Map() .set('codecs', 'codecs') .set('frameRate', 'framerate') // Ours is camelCase, theirs is lowercase. .set('bandwidth', 'bitrate') // They are in the same units: bits/sec. .set('width', 'width') .set('height', 'height') .set('channelsCount', 'channels'); /** * A mimetype created for CEA-608 closed captions. * @const {string} */ shaka.util.MimeUtils.CEA608_CLOSED_CAPTION_MIMETYPE = 'application/cea-608'; /** * A mimetype created for CEA-708 closed captions. * @const {string} */ shaka.util.MimeUtils.CEA708_CLOSED_CAPTION_MIMETYPE = 'application/cea-708'; /** * MIME types of raw formats. * * @const {!Array<string>} */ shaka.util.MimeUtils.RAW_FORMATS = [ 'audio/aac', 'audio/ac3', 'audio/ec3', 'audio/mpeg', ];