Files
shaka-player/lib/util/mime_utils.js
T
Ivan 921206dc1d feat: transmux in a worker (#9914)
This PR introduces a Web Worker for transmuxing resolving
https://github.com/shaka-project/shaka-player/issues/1735

- The worker bundle is compiled separately
- The build output is embedded as a string constant and then wrapped in
a Blob to create an inline Worker URL (HLS.js does this very similarly)
- `TransmuxerProxy` is created wrapping a real transmuxer, but no worker
is started yet - on the first `transmux()` call, it checks if the device
supports worker transmuxing
- For each transmux() call: the buffer is copied, then zero-copy
transferred to the worker. A PublicPromise is stored under a reqId with
a timeout timer, and the main thread awaits it.
- The worker transmuxes and posts back transmuxed (or error). The shared
message listener routes the response to the right proxy instance by id,
which resolves the promise and cancels the timer.
- When the last proxy instance is destroyed, the worker is terminated
and the blob URL is revoked.
  loaded inside the worker.
- Some low-end devices have been excluded since their Worker support is
questionable

There most likely is a better way to do this - please let me know
2026-05-27 21:51:03 +02:00

426 lines
13 KiB
JavaScript

/*! @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',
];