mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-14 15:56:38 +03:00
945 lines
29 KiB
JavaScript
945 lines
29 KiB
JavaScript
/*! @license
|
|
* Shaka Player
|
|
* Copyright 2016 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
goog.provide('shaka.media.SegmentUtils');
|
|
|
|
goog.require('goog.asserts');
|
|
goog.require('shaka.log');
|
|
goog.require('shaka.drm.DrmUtils');
|
|
goog.require('shaka.drm.PlayReady');
|
|
goog.require('shaka.media.Capabilities');
|
|
goog.require('shaka.media.ClosedCaptionParser');
|
|
goog.require('shaka.util.BufferUtils');
|
|
goog.require('shaka.util.Error');
|
|
goog.require('shaka.util.ManifestParserUtils');
|
|
goog.require('shaka.util.MimeUtils');
|
|
goog.require('shaka.util.Mp4BoxParsers');
|
|
goog.require('shaka.util.Mp4Parser');
|
|
goog.require('shaka.util.TsParser');
|
|
goog.require('shaka.util.Uint8ArrayUtils');
|
|
|
|
|
|
/**
|
|
* @summary Utility functions for segment parsing.
|
|
*/
|
|
shaka.media.SegmentUtils = class {
|
|
/**
|
|
* @param {string} mimeType
|
|
* @return {shaka.media.SegmentUtils.BasicInfo}
|
|
*/
|
|
static getBasicInfoFromMimeType(mimeType) {
|
|
const baseMimeType = shaka.util.MimeUtils.getBasicType(mimeType);
|
|
const type = baseMimeType.split('/')[0];
|
|
const codecs = shaka.util.MimeUtils.getCodecs(mimeType);
|
|
return {
|
|
type: type,
|
|
mimeType: baseMimeType,
|
|
codecs: codecs,
|
|
language: null,
|
|
height: null,
|
|
width: null,
|
|
channelCount: null,
|
|
sampleRate: null,
|
|
closedCaptions: new Map(),
|
|
videoRange: null,
|
|
colorGamut: null,
|
|
frameRate: null,
|
|
timescale: null,
|
|
drmInfos: [],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {!BufferSource} data
|
|
* @param {boolean} disableAudio
|
|
* @param {boolean} disableVideo
|
|
* @param {boolean} disableText
|
|
* @return {?shaka.media.SegmentUtils.BasicInfo}
|
|
*/
|
|
static getBasicInfoFromTs(data, disableAudio, disableVideo, disableText) {
|
|
const uint8ArrayData = shaka.util.BufferUtils.toUint8(data);
|
|
const tsParser = new shaka.util.TsParser().parse(uint8ArrayData);
|
|
const tsCodecs = tsParser.getCodecs();
|
|
const videoInfo = tsParser.getVideoInfo();
|
|
const codecs = [];
|
|
let hasAudio = false;
|
|
let hasVideo = false;
|
|
if (!disableAudio) {
|
|
switch (tsCodecs.audio) {
|
|
case 'aac':
|
|
case 'aac-loas':
|
|
if (tsParser.getAudioData().length) {
|
|
codecs.push('mp4a.40.2');
|
|
hasAudio = true;
|
|
}
|
|
break;
|
|
case 'mp3':
|
|
if (tsParser.getAudioData().length) {
|
|
codecs.push('mp4a.40.34');
|
|
hasAudio = true;
|
|
}
|
|
break;
|
|
case 'ac3':
|
|
if (tsParser.getAudioData().length) {
|
|
codecs.push('ac-3');
|
|
hasAudio = true;
|
|
}
|
|
break;
|
|
case 'ec3':
|
|
if (tsParser.getAudioData().length) {
|
|
codecs.push('ec-3');
|
|
hasAudio = true;
|
|
}
|
|
break;
|
|
case 'opus':
|
|
if (tsParser.getAudioData().length) {
|
|
codecs.push('opus');
|
|
hasAudio = true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if (!disableVideo) {
|
|
switch (tsCodecs.video) {
|
|
case 'avc':
|
|
if (videoInfo.codec) {
|
|
codecs.push(videoInfo.codec);
|
|
} else {
|
|
codecs.push('avc1.42E01E');
|
|
}
|
|
hasVideo = true;
|
|
break;
|
|
case 'hvc':
|
|
if (videoInfo.codec) {
|
|
codecs.push(videoInfo.codec);
|
|
} else {
|
|
codecs.push('hvc1.1.6.L93.90');
|
|
}
|
|
hasVideo = true;
|
|
break;
|
|
case 'av1':
|
|
codecs.push('av01.0.01M.08');
|
|
hasVideo = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!codecs.length) {
|
|
return null;
|
|
}
|
|
const onlyAudio = hasAudio && !hasVideo;
|
|
const closedCaptions = new Map();
|
|
if (hasVideo && !disableText) {
|
|
const captionParser = new shaka.media.ClosedCaptionParser('video/mp2t');
|
|
captionParser.parseFrom(data);
|
|
for (const stream of captionParser.getStreams()) {
|
|
closedCaptions.set(stream, stream);
|
|
}
|
|
captionParser.reset();
|
|
}
|
|
return {
|
|
type: onlyAudio ? 'audio' : 'video',
|
|
mimeType: 'video/mp2t',
|
|
codecs: codecs.join(', '),
|
|
language: null,
|
|
height: videoInfo.height,
|
|
width: videoInfo.width,
|
|
channelCount: null,
|
|
sampleRate: null,
|
|
closedCaptions: closedCaptions,
|
|
videoRange: null,
|
|
colorGamut: null,
|
|
frameRate: videoInfo.frameRate,
|
|
timescale: null,
|
|
drmInfos: [],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {?BufferSource} initData
|
|
* @param {?BufferSource} data
|
|
* @param {boolean} disableText
|
|
* @return {?shaka.media.SegmentUtils.BasicInfo}
|
|
*/
|
|
static getBasicInfoFromMp4(initData, data, disableText) {
|
|
goog.asserts.assert(initData != null || data != null,
|
|
'Either initData or data must be non-null.');
|
|
const Mp4Parser = shaka.util.Mp4Parser;
|
|
const SegmentUtils = shaka.media.SegmentUtils;
|
|
|
|
const audioCodecs = [];
|
|
let videoCodecs = [];
|
|
const textCodecs = [];
|
|
|
|
let hasAudio = false;
|
|
let hasVideo = false;
|
|
let hasText = false;
|
|
|
|
const addCodec = (codec) => {
|
|
const codecLC = codec.toLowerCase();
|
|
switch (codecLC) {
|
|
case 'avc1':
|
|
case 'avc3':
|
|
videoCodecs.push(codecLC + '.42E01E');
|
|
hasVideo = true;
|
|
break;
|
|
case 'hev1':
|
|
case 'hvc1':
|
|
videoCodecs.push(codecLC + '.1.6.L93.90');
|
|
hasVideo = true;
|
|
break;
|
|
case 'dvh1':
|
|
case 'dvhe':
|
|
videoCodecs.push(codecLC + '.05.04');
|
|
hasVideo = true;
|
|
break;
|
|
case 'vp09':
|
|
videoCodecs.push(codecLC + '.00.10.08');
|
|
hasVideo = true;
|
|
break;
|
|
case 'av01':
|
|
videoCodecs.push(codecLC + '.0.01M.08');
|
|
hasVideo = true;
|
|
break;
|
|
case 'mp4a':
|
|
// We assume AAC, but this can be wrong since mp4a supports
|
|
// others codecs
|
|
audioCodecs.push('mp4a.40.2');
|
|
hasAudio = true;
|
|
break;
|
|
case 'ac-3':
|
|
case 'ec-3':
|
|
case 'ac-4':
|
|
case 'opus':
|
|
case 'flac':
|
|
audioCodecs.push(codecLC);
|
|
hasAudio = true;
|
|
break;
|
|
case 'apac':
|
|
audioCodecs.push('apac.31.00');
|
|
hasAudio = true;
|
|
break;
|
|
}
|
|
};
|
|
|
|
const codecBoxParser = (box) => addCodec(box.name);
|
|
|
|
/** @type {?string} */
|
|
let language = null;
|
|
/** @type {?string} */
|
|
let height = null;
|
|
/** @type {?string} */
|
|
let width = null;
|
|
/** @type {?number} */
|
|
let channelCount = null;
|
|
/** @type {?number} */
|
|
let sampleRate = null;
|
|
/** @type {?string} */
|
|
let realVideoRange = null;
|
|
/** @type {?string} */
|
|
let realColorGamut = null;
|
|
/** @type {?number} */
|
|
let realFrameRate = null;
|
|
/** @type {?number} */
|
|
let timescale = null;
|
|
|
|
/** @type {!Map<string, shaka.extern.DrmInfo>} */
|
|
const drmInfoMap = new Map();
|
|
/** @type {!Set<string>} */
|
|
const seenPssh = new Set();
|
|
|
|
/** @type {?string} */
|
|
let encryptionScheme = null;
|
|
/** @type {?string} */
|
|
let defaultKID = null;
|
|
|
|
/** @type {?string} */
|
|
let baseBox;
|
|
|
|
/** @type {number} */
|
|
let defaultSampleDuration = 0;
|
|
|
|
/** @type {!Array<shaka.util.ParsedTRUNSample>} */
|
|
let sampleData = [];
|
|
|
|
/** @type {!Map<number, number>} */
|
|
const trackIdToTimescale = new Map();
|
|
|
|
/** @type {?number} */
|
|
let currentTrackId = null;
|
|
|
|
const closedCaptions = new Map();
|
|
|
|
/** @type {!Uint8Array} */
|
|
let combinedData;
|
|
|
|
if (initData && data) {
|
|
const initView = shaka.util.BufferUtils.toUint8(initData);
|
|
const dataView = shaka.util.BufferUtils.toUint8(data);
|
|
combinedData = shaka.util.Uint8ArrayUtils.concat(initView, dataView);
|
|
} else if (data) {
|
|
combinedData = shaka.util.BufferUtils.toUint8(data);
|
|
} else if (initData) {
|
|
combinedData = shaka.util.BufferUtils.toUint8(initData);
|
|
}
|
|
|
|
const genericAudioBox = (box) => {
|
|
const parsedAudioSampleEntryBox =
|
|
shaka.util.Mp4BoxParsers.audioSampleEntry(box.reader);
|
|
channelCount = parsedAudioSampleEntryBox.channelCount;
|
|
sampleRate = parsedAudioSampleEntryBox.sampleRate;
|
|
codecBoxParser(box);
|
|
if (box.reader.hasMoreData()) {
|
|
Mp4Parser.children(box);
|
|
}
|
|
};
|
|
|
|
const genericVideoBox = (box) => {
|
|
baseBox = box.name;
|
|
const parsedVisualSampleEntryBox =
|
|
shaka.util.Mp4BoxParsers.visualSampleEntry(box.reader);
|
|
width = String(parsedVisualSampleEntryBox.width);
|
|
height = String(parsedVisualSampleEntryBox.height);
|
|
if (box.reader.hasMoreData()) {
|
|
Mp4Parser.children(box);
|
|
}
|
|
};
|
|
|
|
let uuidMap;
|
|
|
|
new Mp4Parser()
|
|
.box('moof', shaka.util.Mp4Parser.children)
|
|
.box('moov', Mp4Parser.children)
|
|
.box('trak', Mp4Parser.children)
|
|
.fullBox('tkhd', (box) => {
|
|
goog.asserts.assert(
|
|
box.version != null,
|
|
'TKHD is a full box and should have a valid version.');
|
|
const parsedTKHDBox = shaka.util.Mp4BoxParsers.parseTKHD(
|
|
box.reader, box.version);
|
|
currentTrackId = parsedTKHDBox.trackId;
|
|
})
|
|
.box('mdia', Mp4Parser.children)
|
|
.fullBox('mdhd', (box) => {
|
|
goog.asserts.assert(
|
|
box.version != null,
|
|
'MDHD is a full box and should have a valid version.');
|
|
const parsedMDHDBox = shaka.util.Mp4BoxParsers.parseMDHD(
|
|
box.reader, box.version);
|
|
language = parsedMDHDBox.language;
|
|
if (currentTrackId !== null) {
|
|
trackIdToTimescale.set(currentTrackId, parsedMDHDBox.timescale);
|
|
}
|
|
})
|
|
.box('minf', Mp4Parser.children)
|
|
.box('stbl', Mp4Parser.children)
|
|
.fullBox('stsd', Mp4Parser.sampleDescription)
|
|
|
|
// Frame rate calculation
|
|
.box('traf', Mp4Parser.children)
|
|
.fullBox('tfhd', (box) => {
|
|
goog.asserts.assert(
|
|
box.flags != null,
|
|
'A TFHD box should have a valid flags value');
|
|
const parsedTFHDBox = shaka.util.Mp4BoxParsers.parseTFHD(
|
|
box.reader, box.flags);
|
|
if (parsedTFHDBox.defaultSampleDuration) {
|
|
defaultSampleDuration = parsedTFHDBox.defaultSampleDuration;
|
|
}
|
|
})
|
|
.fullBox('trun', (box) => {
|
|
goog.asserts.assert(
|
|
box.version != null,
|
|
'A TRUN box should have a valid version value');
|
|
goog.asserts.assert(
|
|
box.flags != null,
|
|
'A TRUN box should have a valid flags value');
|
|
const parsedTRUNBox = shaka.util.Mp4BoxParsers.parseTRUN(
|
|
box.reader, box.version, box.flags);
|
|
|
|
sampleData = parsedTRUNBox.sampleData;
|
|
})
|
|
|
|
// AUDIO
|
|
// These are the various boxes that signal a codec.
|
|
.box('mp4a', (box) => {
|
|
const parsedAudioSampleEntryBox =
|
|
shaka.util.Mp4BoxParsers.audioSampleEntry(box.reader);
|
|
channelCount = parsedAudioSampleEntryBox.channelCount;
|
|
sampleRate = parsedAudioSampleEntryBox.sampleRate;
|
|
if (box.reader.hasMoreData()) {
|
|
Mp4Parser.children(box);
|
|
} else {
|
|
codecBoxParser(box);
|
|
}
|
|
})
|
|
.box('esds', (box) => {
|
|
const parsedESDSBox = shaka.util.Mp4BoxParsers.parseESDS(box.reader);
|
|
audioCodecs.push(parsedESDSBox.codec);
|
|
hasAudio = true;
|
|
})
|
|
.box('ac-3', genericAudioBox)
|
|
.box('ec-3', genericAudioBox)
|
|
.box('ac-4', genericAudioBox)
|
|
.box('Opus', genericAudioBox)
|
|
.box('fLaC', genericAudioBox)
|
|
.box('apac', genericAudioBox)
|
|
|
|
// VIDEO
|
|
// These are the various boxes that signal a codec.
|
|
.box('avc1', genericVideoBox)
|
|
.box('avc3', genericVideoBox)
|
|
.box('hev1', genericVideoBox)
|
|
.box('hvc1', genericVideoBox)
|
|
.box('dva1', genericVideoBox)
|
|
.box('dvav', genericVideoBox)
|
|
.box('dvh1', genericVideoBox)
|
|
.box('dvhe', genericVideoBox)
|
|
.box('vp09', genericVideoBox)
|
|
.box('av01', genericVideoBox)
|
|
.box('avcC', (box) => {
|
|
let codecBase = baseBox || '';
|
|
switch (baseBox) {
|
|
case 'dvav':
|
|
codecBase = 'avc3';
|
|
break;
|
|
case 'dva1':
|
|
codecBase = 'avc1';
|
|
break;
|
|
}
|
|
const parsedAVCCBox = shaka.util.Mp4BoxParsers.parseAVCC(
|
|
codecBase, box.reader);
|
|
videoCodecs.push(parsedAVCCBox.codec);
|
|
hasVideo = true;
|
|
})
|
|
.box('hvcC', (box) => {
|
|
let codecBase = baseBox || '';
|
|
switch (baseBox) {
|
|
case 'dvh1':
|
|
codecBase = 'hvc1';
|
|
break;
|
|
case 'dvhe':
|
|
codecBase = 'hev1';
|
|
break;
|
|
}
|
|
const parsedHVCCBox = shaka.util.Mp4BoxParsers.parseHVCC(
|
|
codecBase, box.reader);
|
|
videoCodecs.push(parsedHVCCBox.codec);
|
|
hasVideo = true;
|
|
})
|
|
.box('dvcC', (box) => {
|
|
let codecBase = baseBox || '';
|
|
switch (baseBox) {
|
|
case 'hvc1':
|
|
codecBase = 'dvh1';
|
|
break;
|
|
case 'hev1':
|
|
codecBase = 'dvhe';
|
|
break;
|
|
case 'avc1':
|
|
codecBase = 'dva1';
|
|
break;
|
|
case 'avc3':
|
|
codecBase = 'dvav';
|
|
break;
|
|
case 'av01':
|
|
codecBase = 'dav1';
|
|
break;
|
|
}
|
|
const parsedDVCCBox = shaka.util.Mp4BoxParsers.parseDVCC(
|
|
codecBase, box.reader);
|
|
videoCodecs.push(parsedDVCCBox.codec);
|
|
hasVideo = true;
|
|
})
|
|
.box('dvvC', (box) => {
|
|
let codecBase = baseBox || '';
|
|
switch (baseBox) {
|
|
case 'hvc1':
|
|
codecBase = 'dvh1';
|
|
break;
|
|
case 'hev1':
|
|
codecBase = 'dvhe';
|
|
break;
|
|
case 'avc1':
|
|
codecBase = 'dva1';
|
|
break;
|
|
case 'avc3':
|
|
codecBase = 'dvav';
|
|
break;
|
|
case 'av01':
|
|
codecBase = 'dav1';
|
|
break;
|
|
}
|
|
const parsedDVCCBox = shaka.util.Mp4BoxParsers.parseDVVC(
|
|
codecBase, box.reader);
|
|
videoCodecs.push(parsedDVCCBox.codec);
|
|
hasVideo = true;
|
|
})
|
|
.fullBox('vpcC', (box) => {
|
|
const codecBase = baseBox || '';
|
|
const parsedVPCCBox = shaka.util.Mp4BoxParsers.parseVPCC(
|
|
codecBase, box.reader);
|
|
videoCodecs.push(parsedVPCCBox.codec);
|
|
hasVideo = true;
|
|
})
|
|
.box('av1C', (box) => {
|
|
let codecBase = baseBox || '';
|
|
switch (baseBox) {
|
|
case 'dav1':
|
|
codecBase = 'av01';
|
|
break;
|
|
}
|
|
const parsedAV1CBox = shaka.util.Mp4BoxParsers.parseAV1C(
|
|
codecBase, box.reader);
|
|
videoCodecs.push(parsedAV1CBox.codec);
|
|
hasVideo = true;
|
|
})
|
|
|
|
// TEXT
|
|
.box('wvtt', (box) => {
|
|
textCodecs.push(box.name);
|
|
hasText = true;
|
|
})
|
|
.box('stpp', (box) => {
|
|
textCodecs.push(box.name);
|
|
hasText = true;
|
|
})
|
|
.box('c608', () => {
|
|
closedCaptions.set('CC1', 'CC1');
|
|
if (currentTrackId !== null) {
|
|
trackIdToTimescale.delete(currentTrackId);
|
|
}
|
|
})
|
|
|
|
// This signals an encrypted sample, which we can go inside of to
|
|
// find the codec used.
|
|
// Note: If encrypted, you can only have audio or video, not both.
|
|
.box('enca', genericAudioBox)
|
|
.box('encv', genericVideoBox)
|
|
.box('sinf', Mp4Parser.children)
|
|
.box('frma', (box) => {
|
|
const {codec} = shaka.util.Mp4BoxParsers.parseFRMA(box.reader);
|
|
addCodec(codec);
|
|
})
|
|
|
|
.box('colr', (box) => {
|
|
videoCodecs = videoCodecs.map((codec) => {
|
|
if (codec.startsWith('av01.')) {
|
|
return shaka.util.Mp4BoxParsers.updateAV1CodecWithCOLRBox(
|
|
codec, box.reader);
|
|
}
|
|
return codec;
|
|
});
|
|
const {videoRange, colorGamut} =
|
|
shaka.util.Mp4BoxParsers.parseCOLR(box.reader);
|
|
realVideoRange = videoRange;
|
|
realColorGamut = colorGamut;
|
|
})
|
|
|
|
.fullBox('schm', (box) => {
|
|
const parsedSCHMBox =
|
|
shaka.util.Mp4BoxParsers.parseSCHM(box.reader);
|
|
encryptionScheme = parsedSCHMBox.encryptionScheme;
|
|
})
|
|
.box('schi', Mp4Parser.children)
|
|
.fullBox('tenc', (box) => {
|
|
const parsedTENCBox = shaka.util.Mp4BoxParsers.parseTENC(box.reader);
|
|
defaultKID = parsedTENCBox.defaultKID;
|
|
})
|
|
.fullBox('pssh', (box) => {
|
|
goog.asserts.assert(
|
|
box.version != null,
|
|
'PSSH is a full box and should have a valid version.');
|
|
// The "reader" gives us a view on the payload of the box. Create a
|
|
// new view that contains the whole box.
|
|
const dataView = box.reader.getDataView();
|
|
goog.asserts.assert(
|
|
dataView.byteOffset >= 12, 'DataView at incorrect position');
|
|
const pssh = shaka.util.BufferUtils.toUint8(dataView, -12, box.size);
|
|
|
|
const psshHex = shaka.util.Uint8ArrayUtils.toHex(pssh);
|
|
if (seenPssh.has(psshHex)) {
|
|
return;
|
|
}
|
|
seenPssh.add(psshHex);
|
|
|
|
const systemIdData = box.reader.readBytes(16,
|
|
// Don't clone.
|
|
// The payload is temporary, and is parsed immediately.
|
|
/* clone= */ false);
|
|
const systemId = shaka.util.Uint8ArrayUtils.toHex(systemIdData);
|
|
|
|
if (!uuidMap) {
|
|
uuidMap = shaka.drm.DrmUtils.getUuidMap(/* withoutDashes= */ true);
|
|
}
|
|
|
|
const keySystem = uuidMap[systemId.toLowerCase()];
|
|
if (!keySystem) {
|
|
return;
|
|
}
|
|
const drmInfo = shaka.util.ManifestParserUtils.createDrmInfo(
|
|
keySystem,
|
|
'cenc', // It will be updated later
|
|
/* initData= */ [
|
|
{initDataType: 'cenc', initData: pssh},
|
|
]);
|
|
if (shaka.drm.DrmUtils.isPlayReadyKeySystem(keySystem)) {
|
|
drmInfo.licenseServerUri =
|
|
shaka.drm.PlayReady.getLicenseUrlFromPssh(pssh);
|
|
}
|
|
if (box.version > 0) {
|
|
const numKeyIds = box.reader.readUint32();
|
|
for (let i = 0; i < numKeyIds; i++) {
|
|
const keyIdData = box.reader.readBytes(16,
|
|
// Don't clone.
|
|
// The payload is temporary, and is parsed immediately.
|
|
/* clone= */ false);
|
|
drmInfo.keyIds.add(shaka.util.Uint8ArrayUtils.toHex(keyIdData));
|
|
}
|
|
}
|
|
|
|
drmInfoMap.set(keySystem, drmInfo);
|
|
})
|
|
.parse(combinedData,
|
|
/* partialOkay= */ true, /* stopOnPartial= */ true);
|
|
if (!audioCodecs.length && !videoCodecs.length && !textCodecs.length) {
|
|
return null;
|
|
}
|
|
|
|
timescale = trackIdToTimescale.values().next().value;
|
|
|
|
const onlyAudio = hasAudio && !hasVideo;
|
|
if (hasVideo && !disableText && data && !closedCaptions.size) {
|
|
const captionParser = new shaka.media.ClosedCaptionParser('video/mp4');
|
|
if (initData) {
|
|
captionParser.init(initData);
|
|
}
|
|
try {
|
|
captionParser.parseFrom(data);
|
|
for (const stream of captionParser.getStreams()) {
|
|
closedCaptions.set(stream, stream);
|
|
}
|
|
} catch (e) {
|
|
shaka.log.debug('Error detecting CC streams', e);
|
|
}
|
|
captionParser.reset();
|
|
}
|
|
const codecs = audioCodecs.concat(videoCodecs).concat(textCodecs);
|
|
// Special case for FairPlay.
|
|
if (encryptionScheme === 'cbcs' && defaultKID &&
|
|
!drmInfoMap.has('com.apple.fps')) {
|
|
drmInfoMap.set('com.apple.fps',
|
|
shaka.util.ManifestParserUtils.createDrmInfo(
|
|
'com.apple.fps', /** @type {string} */(encryptionScheme),
|
|
/* initData= */ null));
|
|
}
|
|
for (const drmInfo of drmInfoMap.values()) {
|
|
if (encryptionScheme) {
|
|
drmInfo.encryptionScheme = /** @type {string} */(encryptionScheme);
|
|
}
|
|
if (defaultKID) {
|
|
drmInfo.keyIds.add(/** @type {string} */(defaultKID));
|
|
}
|
|
}
|
|
let type = 'video';
|
|
let mimeType = 'video/mp4';
|
|
if (hasText) {
|
|
type = 'text';
|
|
mimeType = 'application/mp4';
|
|
} else if (onlyAudio) {
|
|
type = 'audio';
|
|
mimeType = 'audio/mp4';
|
|
}
|
|
|
|
if (type === 'video' && timescale != null && sampleData.length) {
|
|
const ts = timescale;
|
|
|
|
let totalDuration = 0;
|
|
for (const sample of sampleData) {
|
|
totalDuration += sample.sampleDuration ?? defaultSampleDuration;
|
|
}
|
|
|
|
if (totalDuration > 0) {
|
|
realFrameRate = (ts * sampleData.length) / totalDuration;
|
|
}
|
|
}
|
|
|
|
return {
|
|
type: type,
|
|
mimeType: mimeType,
|
|
codecs: SegmentUtils.codecsFiltering(codecs).join(', '),
|
|
language: language,
|
|
height: height,
|
|
width: width,
|
|
channelCount: channelCount,
|
|
sampleRate: sampleRate,
|
|
closedCaptions: closedCaptions,
|
|
videoRange: realVideoRange,
|
|
colorGamut: realColorGamut,
|
|
frameRate: realFrameRate,
|
|
timescale: timescale,
|
|
drmInfos: Array.from(drmInfoMap.values()),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {!Array<string>} codecs
|
|
* @return {!Array<string>} codecs
|
|
*/
|
|
static codecsFiltering(codecs) {
|
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
const ManifestParserUtils = shaka.util.ManifestParserUtils;
|
|
const SegmentUtils = shaka.media.SegmentUtils;
|
|
const allCodecs = SegmentUtils.filterDuplicateCodecs_(codecs);
|
|
const audioCodecs =
|
|
ManifestParserUtils.guessAllCodecsSafe(ContentType.AUDIO, allCodecs);
|
|
const videoCodecs =
|
|
ManifestParserUtils.guessAllCodecsSafe(ContentType.VIDEO, allCodecs);
|
|
const textCodecs =
|
|
ManifestParserUtils.guessAllCodecsSafe(ContentType.TEXT, allCodecs);
|
|
const validVideoCodecs = SegmentUtils.chooseBetterCodecs_(videoCodecs);
|
|
const finalCodecs =
|
|
audioCodecs.concat(validVideoCodecs).concat(textCodecs);
|
|
if (allCodecs.length && !finalCodecs.length) {
|
|
return allCodecs;
|
|
}
|
|
return finalCodecs;
|
|
}
|
|
|
|
/**
|
|
* @param {!BufferSource} data
|
|
* @param {number} timescale
|
|
* @return {{startTime: number, duration: number}}
|
|
*/
|
|
static getStartTimeAndDurationFromMp4(data, timescale) {
|
|
let startTime = 0;
|
|
let defaultSampleDuration = 0;
|
|
let sampleData = [];
|
|
const Mp4Parser = shaka.util.Mp4Parser;
|
|
new Mp4Parser()
|
|
.box('moof', Mp4Parser.children)
|
|
.box('traf', Mp4Parser.children)
|
|
.fullBox('tfhd', (box) => {
|
|
goog.asserts.assert(
|
|
box.flags != null,
|
|
'A TFHD box should have a valid flags value');
|
|
const parsedTFHDBox = shaka.util.Mp4BoxParsers.parseTFHD(
|
|
box.reader, box.flags);
|
|
defaultSampleDuration = parsedTFHDBox.defaultSampleDuration;
|
|
})
|
|
.fullBox('tfdt', (box) => {
|
|
goog.asserts.assert(
|
|
box.version == 0 || box.version == 1,
|
|
'TFDT version can only be 0 or 1');
|
|
const parsed = shaka.util.Mp4BoxParsers.parseTFDTInaccurate(
|
|
box.reader, box.version);
|
|
startTime = parsed.baseMediaDecodeTime / timescale;
|
|
})
|
|
.fullBox('trun', (box) => {
|
|
goog.asserts.assert(
|
|
box.version != null,
|
|
'A TRUN box should have a valid version value');
|
|
goog.asserts.assert(
|
|
box.flags != null,
|
|
'A TRUN box should have a valid flags value');
|
|
const parsedTRUNBox = shaka.util.Mp4BoxParsers.parseTRUN(
|
|
box.reader, box.version, box.flags);
|
|
|
|
sampleData = parsedTRUNBox.sampleData;
|
|
box.parser.stop();
|
|
}).parse(data);
|
|
let sumSampleDuration = 0;
|
|
for (const sample of sampleData) {
|
|
sumSampleDuration += sample.sampleDuration ?? defaultSampleDuration;
|
|
}
|
|
const duration = sumSampleDuration / timescale;
|
|
return {
|
|
startTime,
|
|
duration,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {!Array<string>} codecs
|
|
* @return {!Array<string>} codecs
|
|
* @private
|
|
*/
|
|
static filterDuplicateCodecs_(codecs) {
|
|
// Filter out duplicate codecs.
|
|
const seen = new Set();
|
|
const ret = [];
|
|
for (const codec of codecs) {
|
|
const shortCodec = shaka.util.MimeUtils.getCodecBase(codec);
|
|
if (!seen.has(shortCodec)) {
|
|
ret.push(codec);
|
|
seen.add(shortCodec);
|
|
} else {
|
|
shaka.log.debug('Ignoring duplicate codec');
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Prioritizes Dolby Vision if supported. This is necessary because with
|
|
* Dolby Vision we could have hvcC and dvcC boxes at the same time.
|
|
*
|
|
* @param {!Array<string>} codecs
|
|
* @return {!Array<string>} codecs
|
|
* @private
|
|
*/
|
|
static chooseBetterCodecs_(codecs) {
|
|
if (codecs.length <= 1) {
|
|
return codecs;
|
|
}
|
|
const dolbyVision = codecs.find((codec) => {
|
|
return codec.startsWith('dvav.') ||
|
|
codec.startsWith('dva1.') ||
|
|
codec.startsWith('dvh1.') ||
|
|
codec.startsWith('dvhe.') ||
|
|
codec.startsWith('dav1.') ||
|
|
codec.startsWith('dvc1.') ||
|
|
codec.startsWith('dvi1.');
|
|
});
|
|
if (!dolbyVision) {
|
|
return codecs;
|
|
}
|
|
const type = `video/mp4; codecs="${dolbyVision}"`;
|
|
if (shaka.media.Capabilities.isTypeSupported(type)) {
|
|
return [dolbyVision];
|
|
}
|
|
return codecs.filter((codec) => codec != dolbyVision);
|
|
}
|
|
|
|
/**
|
|
* @param {!BufferSource} data
|
|
* @return {?string}
|
|
*/
|
|
static getDefaultKID(data) {
|
|
const Mp4Parser = shaka.util.Mp4Parser;
|
|
|
|
let defaultKID = null;
|
|
new Mp4Parser()
|
|
.box('moov', Mp4Parser.children)
|
|
.box('trak', Mp4Parser.children)
|
|
.box('mdia', Mp4Parser.children)
|
|
.box('minf', Mp4Parser.children)
|
|
.box('stbl', Mp4Parser.children)
|
|
.fullBox('stsd', Mp4Parser.sampleDescription)
|
|
.box('encv', Mp4Parser.visualSampleEntry)
|
|
.box('enca', Mp4Parser.audioSampleEntry)
|
|
.box('sinf', Mp4Parser.children)
|
|
.box('schi', Mp4Parser.children)
|
|
.fullBox('tenc', (box) => {
|
|
const parsedTENCBox = shaka.util.Mp4BoxParsers.parseTENC(box.reader);
|
|
defaultKID = parsedTENCBox.defaultKID;
|
|
})
|
|
|
|
.parse(data, /* partialOkay= */ true);
|
|
return defaultKID;
|
|
}
|
|
|
|
/**
|
|
* @param {!BufferSource} rawResult
|
|
* @param {shaka.extern.aesKey} aesKey
|
|
* @param {number} position
|
|
* @return {!Promise<!BufferSource>}
|
|
*/
|
|
static async aesDecrypt(rawResult, aesKey, position) {
|
|
const key = aesKey;
|
|
if (!key.cryptoKey) {
|
|
goog.asserts.assert(key.fetchKey, 'If AES cryptoKey was not ' +
|
|
'preloaded, fetchKey function should be provided');
|
|
await key.fetchKey();
|
|
goog.asserts.assert(key.cryptoKey, 'AES cryptoKey should now be set');
|
|
}
|
|
|
|
if (aesKey.blockCipherMode == 'GCM' && aesKey.bitsKey == 256) {
|
|
const buffer = shaka.util.BufferUtils.toUint8(rawResult);
|
|
|
|
if (buffer.byteLength < 32) {
|
|
throw new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.MANIFEST,
|
|
shaka.util.Error.Code.HLS_INVALID_GCM_SEGMENT);
|
|
}
|
|
|
|
const iv = buffer.slice(0, 16);
|
|
const ciphertextAndTag = buffer.slice(16);
|
|
|
|
const algorithm = {
|
|
name: 'AES-GCM',
|
|
iv,
|
|
tagLength: 128,
|
|
};
|
|
|
|
return window.crypto.subtle.decrypt(
|
|
algorithm, key.cryptoKey, ciphertextAndTag);
|
|
}
|
|
|
|
let iv = key.iv;
|
|
if (!iv) {
|
|
iv = shaka.util.BufferUtils.toUint8(new ArrayBuffer(16));
|
|
let sequence = key.firstMediaSequenceNumber + position;
|
|
for (let i = iv.byteLength - 1; i >= 0; i--) {
|
|
iv[i] = sequence & 0xff;
|
|
sequence >>= 8;
|
|
}
|
|
}
|
|
let algorithm;
|
|
if (aesKey.blockCipherMode == 'CBC') {
|
|
algorithm = {
|
|
name: 'AES-CBC',
|
|
iv,
|
|
};
|
|
} else {
|
|
algorithm = {
|
|
name: 'AES-CTR',
|
|
counter: iv,
|
|
// NIST SP800-38A standard suggests that the counter should occupy half
|
|
// of the counter block
|
|
length: 64,
|
|
};
|
|
}
|
|
return window.crypto.subtle.decrypt(algorithm, key.cryptoKey, rawResult);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @typedef {{
|
|
* type: string,
|
|
* mimeType: string,
|
|
* codecs: string,
|
|
* language: ?string,
|
|
* height: ?string,
|
|
* width: ?string,
|
|
* channelCount: ?number,
|
|
* sampleRate: ?number,
|
|
* closedCaptions: Map<string, string>,
|
|
* videoRange: ?string,
|
|
* colorGamut: ?string,
|
|
* frameRate: ?number,
|
|
* timescale: ?number,
|
|
* drmInfos: !Array<shaka.extern.DrmInfo>,
|
|
* }}
|
|
*
|
|
* @property {string} type
|
|
* @property {string} mimeType
|
|
* @property {string} codecs
|
|
* @property {?string} language
|
|
* @property {?string} height
|
|
* @property {?string} width
|
|
* @property {?number} channelCount
|
|
* @property {?number} sampleRate
|
|
* @property {Map<string, string>} closedCaptions
|
|
* @property {?string} videoRange
|
|
* @property {?string} colorGamut
|
|
* @property {?number} frameRate
|
|
* @property {?number} timescale
|
|
* @property {!Array<shaka.extern.DrmInfo>} drmInfos
|
|
*/
|
|
shaka.media.SegmentUtils.BasicInfo;
|