Buckets:
| import { utf8ArrayToStr } from '@svta/common-media-library/utils/utf8ArrayToStr'; | |
| import { arrayToHex } from './hex'; | |
| import { ElementaryStreamTypes } from '../loader/fragment'; | |
| import { logger } from '../utils/logger'; | |
| import type { KeySystemIds } from './mediakeys-helper'; | |
| import type { DecryptData } from '../loader/level-key'; | |
| import type { PassthroughTrack, UserdataSample } from '../types/demuxer'; | |
| import type { ILogger } from '../utils/logger'; | |
| type BoxDataOrUndefined = Uint8Array<ArrayBuffer> | undefined; | |
| const UINT32_MAX = Math.pow(2, 32) - 1; | |
| const push = [].push; | |
| // We are using fixed track IDs for driving the MP4 remuxer | |
| // instead of following the TS PIDs. | |
| // There is no reason not to do this and some browsers/SourceBuffer-demuxers | |
| // may not like if there are TrackID "switches" | |
| // See https://github.com/video-dev/hls.js/issues/1331 | |
| // Here we are mapping our internal track types to constant MP4 track IDs | |
| // With MSE currently one can only have one track of each, and we are muxing | |
| // whatever video/audio rendition in them. | |
| export const RemuxerTrackIdConfig = { | |
| video: 1, | |
| audio: 2, | |
| id3: 3, | |
| text: 4, | |
| }; | |
| export function bin2str(data: Uint8Array): string { | |
| return String.fromCharCode.apply(null, data); | |
| } | |
| export function readUint16(buffer: Uint8Array, offset: number): number { | |
| const val = (buffer[offset] << 8) | buffer[offset + 1]; | |
| return val < 0 ? 65536 + val : val; | |
| } | |
| export function readUint32(buffer: Uint8Array, offset: number): number { | |
| const val = readSint32(buffer, offset); | |
| return val < 0 ? 4294967296 + val : val; | |
| } | |
| export function readUint64(buffer: Uint8Array, offset: number) { | |
| let result = readUint32(buffer, offset); | |
| result *= Math.pow(2, 32); | |
| result += readUint32(buffer, offset + 4); | |
| return result; | |
| } | |
| export function readSint32(buffer: Uint8Array, offset: number): number { | |
| return ( | |
| (buffer[offset] << 24) | | |
| (buffer[offset + 1] << 16) | | |
| (buffer[offset + 2] << 8) | | |
| buffer[offset + 3] | |
| ); | |
| } | |
| export function writeUint32(buffer: Uint8Array, offset: number, value: number) { | |
| buffer[offset] = value >> 24; | |
| buffer[offset + 1] = (value >> 16) & 0xff; | |
| buffer[offset + 2] = (value >> 8) & 0xff; | |
| buffer[offset + 3] = value & 0xff; | |
| } | |
| // Find "moof" box | |
| export function hasMoofData(data: Uint8Array): boolean { | |
| const end = data.byteLength; | |
| for (let i = 0; i < end; ) { | |
| const size = readUint32(data, i); | |
| if ( | |
| size > 8 && | |
| data[i + 4] === 0x6d && | |
| data[i + 5] === 0x6f && | |
| data[i + 6] === 0x6f && | |
| data[i + 7] === 0x66 | |
| ) { | |
| return true; | |
| } | |
| i = size > 1 ? i + size : end; | |
| } | |
| return false; | |
| } | |
| // Find the data for a box specified by its path | |
| export function findBox(data: Uint8Array, path: string[]): Uint8Array[] { | |
| const results = [] as Uint8Array[]; | |
| if (!path.length) { | |
| // short-circuit the search for empty paths | |
| return results; | |
| } | |
| const end = data.byteLength; | |
| for (let i = 0; i < end; ) { | |
| const size = readUint32(data, i); | |
| const type = bin2str(data.subarray(i + 4, i + 8)); | |
| const endbox = size > 1 ? i + size : end; | |
| if (type === path[0]) { | |
| if (path.length === 1) { | |
| // this is the end of the path and we've found the box we were | |
| // looking for | |
| results.push(data.subarray(i + 8, endbox)); | |
| } else { | |
| // recursively search for the next box along the path | |
| const subresults = findBox(data.subarray(i + 8, endbox), path.slice(1)); | |
| if (subresults.length) { | |
| push.apply(results, subresults); | |
| } | |
| } | |
| } | |
| i = endbox; | |
| } | |
| // we've finished searching all of data | |
| return results; | |
| } | |
| type SidxInfo = { | |
| earliestPresentationTime: number; | |
| timescale: number; | |
| version: number; | |
| referencesCount: number; | |
| references: any[]; | |
| }; | |
| function parseSegmentIndex(sidx: Uint8Array): SidxInfo | null { | |
| const references: any[] = []; | |
| const version = sidx[0]; | |
| // set initial offset, we skip the reference ID (not needed) | |
| let index = 8; | |
| const timescale = readUint32(sidx, index); | |
| index += 4; | |
| let earliestPresentationTime = 0; | |
| let firstOffset = 0; | |
| if (version === 0) { | |
| earliestPresentationTime = readUint32(sidx, index); | |
| firstOffset = readUint32(sidx, index + 4); | |
| index += 8; | |
| } else { | |
| earliestPresentationTime = readUint64(sidx, index); | |
| firstOffset = readUint64(sidx, index + 8); | |
| index += 16; | |
| } | |
| // skip reserved | |
| index += 2; | |
| let startByte = sidx.length + firstOffset; | |
| const referencesCount = readUint16(sidx, index); | |
| index += 2; | |
| for (let i = 0; i < referencesCount; i++) { | |
| let referenceIndex = index; | |
| const referenceInfo = readUint32(sidx, referenceIndex); | |
| referenceIndex += 4; | |
| const referenceSize = referenceInfo & 0x7fffffff; | |
| const referenceType = (referenceInfo & 0x80000000) >>> 31; | |
| if (referenceType === 1) { | |
| logger.warn('SIDX has hierarchical references (not supported)'); | |
| return null; | |
| } | |
| const subsegmentDuration = readUint32(sidx, referenceIndex); | |
| referenceIndex += 4; | |
| references.push({ | |
| referenceSize, | |
| subsegmentDuration, // unscaled | |
| info: { | |
| duration: subsegmentDuration / timescale, | |
| start: startByte, | |
| end: startByte + referenceSize - 1, | |
| }, | |
| }); | |
| startByte += referenceSize; | |
| // Skipping 1 bit for |startsWithSap|, 3 bits for |sapType|, and 28 bits | |
| // for |sapDelta|. | |
| referenceIndex += 4; | |
| // skip to next ref | |
| index = referenceIndex; | |
| } | |
| return { | |
| earliestPresentationTime, | |
| timescale, | |
| version, | |
| referencesCount, | |
| references, | |
| }; | |
| } | |
| /** | |
| * Parses an MP4 initialization segment and extracts stream type and | |
| * timescale values for any declared tracks. Timescale values indicate the | |
| * number of clock ticks per second to assume for time-based values | |
| * elsewhere in the MP4. | |
| * | |
| * To determine the start time of an MP4, you need two pieces of | |
| * information: the timescale unit and the earliest base media decode | |
| * time. Multiple timescales can be specified within an MP4 but the | |
| * base media decode time is always expressed in the timescale from | |
| * the media header box for the track: | |
| * ``` | |
| * moov > trak > mdia > mdhd.timescale | |
| * moov > trak > mdia > hdlr | |
| * ``` | |
| * @param initSegment the bytes of the init segment | |
| * @returns a hash of track type to timescale values or null if | |
| * the init segment is malformed. | |
| */ | |
| export interface InitDataTrack { | |
| timescale: number; | |
| id: number; | |
| codec: string; | |
| encrypted: boolean; | |
| supplemental: string | undefined; | |
| } | |
| type HdlrMetadata = 'meta'; | |
| type HdlrType = | |
| | ElementaryStreamTypes.AUDIO | |
| | ElementaryStreamTypes.VIDEO | |
| | HdlrMetadata; | |
| type StsdData = { | |
| codec: string; | |
| encrypted: boolean; | |
| supplemental: string | undefined; | |
| }; | |
| export interface InitData extends Array<any> { | |
| [index: number]: | |
| | { | |
| timescale: number; | |
| type: HdlrType; | |
| stsd: StsdData; | |
| default?: { | |
| duration: number; | |
| flags: number; | |
| }; | |
| } | |
| | undefined; | |
| audio?: InitDataTrack; | |
| video?: InitDataTrack; | |
| caption?: InitDataTrack; | |
| } | |
| export function parseInitSegment(initSegment: Uint8Array): InitData { | |
| const result: InitData = []; | |
| const traks = findBox(initSegment, ['moov', 'trak']); | |
| for (let i = 0; i < traks.length; i++) { | |
| const trak = traks[i]; | |
| const tkhd = findBox(trak, ['tkhd'])[0]; | |
| if (tkhd as any) { | |
| let version = tkhd[0]; | |
| const trackId = readUint32(tkhd, version === 0 ? 12 : 20); | |
| const mdhd = findBox(trak, ['mdia', 'mdhd'])[0]; | |
| if (mdhd as any) { | |
| version = mdhd[0]; | |
| const timescale = readUint32(mdhd, version === 0 ? 12 : 20); | |
| const hdlr = findBox(trak, ['mdia', 'hdlr'])[0]; | |
| if (hdlr as any) { | |
| const hdlrType = bin2str(hdlr.subarray(8, 12)); | |
| const type: HdlrType | undefined = { | |
| soun: ElementaryStreamTypes.AUDIO as const, | |
| vide: ElementaryStreamTypes.VIDEO as const, | |
| }[hdlrType]; | |
| // Parse codec details | |
| const stsdBox = findBox(trak, ['mdia', 'minf', 'stbl', 'stsd'])[0]; | |
| const stsd = parseStsd(stsdBox); | |
| if (type) { | |
| // Add 'audio', 'video', and 'audiovideo' track records that will map to SourceBuffers | |
| result[trackId] = { timescale, type, stsd }; | |
| result[type] = { timescale, id: trackId, ...stsd }; | |
| } else { | |
| // Add 'meta' and other track records | |
| result[trackId] = { | |
| timescale, | |
| type: hdlrType as HdlrType, | |
| stsd, | |
| }; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| const trex = findBox(initSegment, ['moov', 'mvex', 'trex']); | |
| trex.forEach((trex) => { | |
| const trackId = readUint32(trex, 4); | |
| const track = result[trackId]; | |
| if (track) { | |
| track.default = { | |
| duration: readUint32(trex, 12), | |
| flags: readUint32(trex, 20), | |
| }; | |
| } | |
| }); | |
| return result; | |
| } | |
| function parseStsd(stsd: Uint8Array): StsdData { | |
| const sampleEntries = stsd.subarray(8); | |
| const sampleEntriesEnd = sampleEntries.subarray(8 + 78); | |
| const fourCC = bin2str(sampleEntries.subarray(4, 8)); | |
| let codec = fourCC; | |
| let supplemental; | |
| const encrypted = fourCC === 'enca' || fourCC === 'encv'; | |
| if (encrypted) { | |
| const encBox = findBox(sampleEntries, [fourCC])[0]; | |
| const encBoxChildren = encBox.subarray(fourCC === 'enca' ? 28 : 78); | |
| const sinfs = findBox(encBoxChildren, ['sinf']); | |
| sinfs.forEach((sinf) => { | |
| const schm = findBox(sinf, ['schm'])[0]; | |
| if (schm as any) { | |
| const scheme = bin2str(schm.subarray(4, 8)); | |
| if (scheme === 'cbcs' || scheme === 'cenc') { | |
| const frma = findBox(sinf, ['frma'])[0]; | |
| if (frma as any) { | |
| // for encrypted content codec fourCC will be in frma | |
| codec = bin2str(frma); | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| const codecFourCC = codec; | |
| switch (codec) { | |
| case 'avc1': | |
| case 'avc2': | |
| case 'avc3': | |
| case 'avc4': { | |
| // extract profile + compatibility + level out of avcC box | |
| const avcCBox = findBox(sampleEntriesEnd, ['avcC'])[0]; | |
| if ((avcCBox as any) && avcCBox.length > 3) { | |
| codec += | |
| '.' + toHex(avcCBox[1]) + toHex(avcCBox[2]) + toHex(avcCBox[3]); | |
| supplemental = parseSupplementalDoViCodec( | |
| codecFourCC === 'avc1' ? 'dva1' : 'dvav', | |
| sampleEntriesEnd, | |
| ); | |
| } | |
| break; | |
| } | |
| case 'mp4a': { | |
| const codecBox = findBox(sampleEntries, [fourCC])[0]; | |
| const esdsBox = findBox(codecBox.subarray(28), ['esds'])[0]; | |
| if ((esdsBox as any) && esdsBox.length > 7) { | |
| let i = 4; | |
| // ES Descriptor tag | |
| if (esdsBox[i++] !== 0x03) { | |
| break; | |
| } | |
| i = skipBERInteger(esdsBox, i); | |
| i += 2; // skip es_id; | |
| const flags = esdsBox[i++]; | |
| if (flags & 0x80) { | |
| i += 2; // skip dependency es_id | |
| } | |
| if (flags & 0x40) { | |
| i += esdsBox[i++]; // skip URL | |
| } | |
| // Decoder config descriptor | |
| if (esdsBox[i++] !== 0x04) { | |
| break; | |
| } | |
| i = skipBERInteger(esdsBox, i); | |
| const objectType = esdsBox[i++]; | |
| if (objectType === 0x40) { | |
| codec += '.' + toHex(objectType); | |
| } else { | |
| break; | |
| } | |
| i += 12; | |
| // Decoder specific info | |
| if (esdsBox[i++] !== 0x05) { | |
| break; | |
| } | |
| i = skipBERInteger(esdsBox, i); | |
| const firstByte = esdsBox[i++]; | |
| let audioObjectType = (firstByte & 0xf8) >> 3; | |
| if (audioObjectType === 31) { | |
| audioObjectType += | |
| 1 + ((firstByte & 0x7) << 3) + ((esdsBox[i] & 0xe0) >> 5); | |
| } | |
| codec += '.' + audioObjectType; | |
| } | |
| break; | |
| } | |
| case 'hvc1': | |
| case 'hev1': { | |
| const hvcCBox = findBox(sampleEntriesEnd, ['hvcC'])[0]; | |
| if ((hvcCBox as any) && hvcCBox.length > 12) { | |
| const profileByte = hvcCBox[1]; | |
| const profileSpace = ['', 'A', 'B', 'C'][profileByte >> 6]; | |
| const generalProfileIdc = profileByte & 0x1f; | |
| const profileCompat = readUint32(hvcCBox, 2); | |
| const tierFlag = (profileByte & 0x20) >> 5 ? 'H' : 'L'; | |
| const levelIDC = hvcCBox[12]; | |
| const constraintIndicator = hvcCBox.subarray(6, 12); | |
| codec += '.' + profileSpace + generalProfileIdc; | |
| codec += | |
| '.' + reverse32BitInt(profileCompat).toString(16).toUpperCase(); | |
| codec += '.' + tierFlag + levelIDC; | |
| let constraintString = ''; | |
| for (let i = constraintIndicator.length; i--; ) { | |
| const byte = constraintIndicator[i]; | |
| if (byte || constraintString) { | |
| const encodedByte = byte.toString(16).toUpperCase(); | |
| constraintString = '.' + encodedByte + constraintString; | |
| } | |
| } | |
| codec += constraintString; | |
| } | |
| supplemental = parseSupplementalDoViCodec( | |
| codecFourCC == 'hev1' ? 'dvhe' : 'dvh1', | |
| sampleEntriesEnd, | |
| ); | |
| break; | |
| } | |
| case 'dvh1': | |
| case 'dvhe': | |
| case 'dvav': | |
| case 'dva1': | |
| case 'dav1': { | |
| codec = parseSupplementalDoViCodec(codec, sampleEntriesEnd) || codec; | |
| break; | |
| } | |
| case 'vp09': { | |
| const vpcCBox = findBox(sampleEntriesEnd, ['vpcC'])[0]; | |
| if ((vpcCBox as any) && vpcCBox.length > 6) { | |
| const profile = vpcCBox[4]; | |
| const level = vpcCBox[5]; | |
| const bitDepth = (vpcCBox[6] >> 4) & 0x0f; | |
| codec += | |
| '.' + | |
| addLeadingZero(profile) + | |
| '.' + | |
| addLeadingZero(level) + | |
| '.' + | |
| addLeadingZero(bitDepth); | |
| } | |
| break; | |
| } | |
| case 'av01': { | |
| const av1CBox = findBox(sampleEntriesEnd, ['av1C'])[0]; | |
| if ((av1CBox as any) && av1CBox.length > 2) { | |
| const profile = av1CBox[1] >>> 5; | |
| const level = av1CBox[1] & 0x1f; | |
| const tierFlag = av1CBox[2] >>> 7 ? 'H' : 'M'; | |
| const highBitDepth = (av1CBox[2] & 0x40) >> 6; | |
| const twelveBit = (av1CBox[2] & 0x20) >> 5; | |
| const bitDepth = | |
| profile === 2 && highBitDepth | |
| ? twelveBit | |
| ? 12 | |
| : 10 | |
| : highBitDepth | |
| ? 10 | |
| : 8; | |
| const monochrome = (av1CBox[2] & 0x10) >> 4; | |
| const chromaSubsamplingX = (av1CBox[2] & 0x08) >> 3; | |
| const chromaSubsamplingY = (av1CBox[2] & 0x04) >> 2; | |
| const chromaSamplePosition = av1CBox[2] & 0x03; | |
| // TODO: parse color_description_present_flag | |
| // default it to BT.709/limited range for now | |
| // more info https://aomediacodec.github.io/av1-isobmff/#av1codecconfigurationbox-syntax | |
| const colorPrimaries = 1; | |
| const transferCharacteristics = 1; | |
| const matrixCoefficients = 1; | |
| const videoFullRangeFlag = 0; | |
| codec += | |
| '.' + | |
| profile + | |
| '.' + | |
| addLeadingZero(level) + | |
| tierFlag + | |
| '.' + | |
| addLeadingZero(bitDepth) + | |
| '.' + | |
| monochrome + | |
| '.' + | |
| chromaSubsamplingX + | |
| chromaSubsamplingY + | |
| chromaSamplePosition + | |
| '.' + | |
| addLeadingZero(colorPrimaries) + | |
| '.' + | |
| addLeadingZero(transferCharacteristics) + | |
| '.' + | |
| addLeadingZero(matrixCoefficients) + | |
| '.' + | |
| videoFullRangeFlag; | |
| supplemental = parseSupplementalDoViCodec('dav1', sampleEntriesEnd); | |
| } | |
| break; | |
| } | |
| case 'ac-3': | |
| case 'ec-3': | |
| case 'alac': | |
| case 'fLaC': | |
| case 'Opus': | |
| default: | |
| break; | |
| } | |
| return { codec, encrypted, supplemental }; | |
| } | |
| function parseSupplementalDoViCodec( | |
| fourCC: string, | |
| sampleEntriesEnd: Uint8Array, | |
| ): string | undefined { | |
| const dvvCResult = findBox(sampleEntriesEnd, ['dvvC']); // used by DoVi Profile 8 to 10 | |
| const dvXCBox = dvvCResult.length | |
| ? dvvCResult[0] | |
| : findBox(sampleEntriesEnd, ['dvcC'])[0]; // used by DoVi Profiles up to 7 and 20 | |
| if (dvXCBox as any) { | |
| const doViProfile = (dvXCBox[2] >> 1) & 0x7f; | |
| const doViLevel = ((dvXCBox[2] << 5) & 0x20) | ((dvXCBox[3] >> 3) & 0x1f); | |
| return ( | |
| fourCC + | |
| '.' + | |
| addLeadingZero(doViProfile) + | |
| '.' + | |
| addLeadingZero(doViLevel) | |
| ); | |
| } | |
| } | |
| function reverse32BitInt(val: number) { | |
| let result = 0; | |
| for (let i = 0; i < 32; i++) { | |
| result |= ((val >> i) & 1) << (32 - 1 - i); | |
| } | |
| return result >>> 0; | |
| } | |
| function skipBERInteger(bytes: Uint8Array, i: number): number { | |
| const limit = i + 5; | |
| while (bytes[i++] & 0x80 && i < limit) { | |
| /* do nothing */ | |
| } | |
| return i; | |
| } | |
| function toHex(x: number): string { | |
| return ('0' + x.toString(16).toUpperCase()).slice(-2); | |
| } | |
| function addLeadingZero(num: number): string { | |
| return (num < 10 ? '0' : '') + num; | |
| } | |
| export function patchEncyptionData( | |
| initSegment: Uint8Array<ArrayBuffer> | undefined, | |
| decryptdata: DecryptData | null, | |
| ) { | |
| if (!initSegment || !decryptdata) { | |
| return; | |
| } | |
| const keyId = decryptdata.keyId; | |
| if (keyId && decryptdata.isCommonEncryption) { | |
| applyToTencBoxes(initSegment, (tenc, isAudio) => { | |
| // Look for default key id (keyID offset is always 8 within the tenc box): | |
| const tencKeyId = tenc.subarray(8, 24); | |
| if (!tencKeyId.some((b) => b !== 0)) { | |
| logger.log( | |
| `[eme] Patching keyId in 'enc${ | |
| isAudio ? 'a' : 'v' | |
| }>sinf>>tenc' box: ${arrayToHex(tencKeyId)} -> ${arrayToHex(keyId)}`, | |
| ); | |
| tenc.set(keyId, 8); | |
| } | |
| }); | |
| } | |
| } | |
| export function parseKeyIdsFromTenc( | |
| initSegment: Uint8Array<ArrayBuffer>, | |
| ): Uint8Array<ArrayBuffer>[] { | |
| const keyIds: Uint8Array<ArrayBuffer>[] = []; | |
| applyToTencBoxes(initSegment, (tenc) => keyIds.push(tenc.subarray(8, 24))); | |
| return keyIds; | |
| } | |
| function applyToTencBoxes( | |
| initSegment: Uint8Array<ArrayBuffer>, | |
| predicate: (tenc: Uint8Array<ArrayBuffer>, isAudio: boolean) => void, | |
| ) { | |
| const traks = findBox(initSegment, ['moov', 'trak']); | |
| traks.forEach((trak) => { | |
| const stsd = findBox(trak, [ | |
| 'mdia', | |
| 'minf', | |
| 'stbl', | |
| 'stsd', | |
| ])[0] as BoxDataOrUndefined; | |
| if (!stsd) return; | |
| const sampleEntries = stsd.subarray(8); | |
| let encBoxes = findBox(sampleEntries, ['enca']); | |
| const isAudio = encBoxes.length > 0; | |
| if (!isAudio) { | |
| encBoxes = findBox(sampleEntries, ['encv']); | |
| } | |
| encBoxes.forEach((enc) => { | |
| const encBoxChildren = isAudio ? enc.subarray(28) : enc.subarray(78); | |
| const sinfBoxes = findBox(encBoxChildren, ['sinf']); | |
| sinfBoxes.forEach((sinf) => { | |
| const tenc = parseSinf(sinf); | |
| if (tenc) { | |
| predicate(tenc, isAudio); | |
| } | |
| }); | |
| }); | |
| }); | |
| } | |
| export function parseSinf(sinf: Uint8Array): BoxDataOrUndefined { | |
| const schm = findBox(sinf, ['schm'])[0] as BoxDataOrUndefined; | |
| if (schm) { | |
| const scheme = bin2str(schm.subarray(4, 8)); | |
| if (scheme === 'cbcs' || scheme === 'cenc') { | |
| const tenc = findBox(sinf, ['schi', 'tenc'])[0] as BoxDataOrUndefined; | |
| if (tenc) { | |
| return tenc; | |
| } | |
| } else if (scheme === 'cbc2') { | |
| /* no-op */ | |
| } | |
| } | |
| } | |
| /* | |
| For Reference: | |
| aligned(8) class TrackFragmentHeaderBox | |
| extends FullBox(‘tfhd’, 0, tf_flags){ | |
| unsigned int(32) track_ID; | |
| // all the following are optional fields | |
| unsigned int(64) base_data_offset; | |
| unsigned int(32) sample_description_index; | |
| unsigned int(32) default_sample_duration; | |
| unsigned int(32) default_sample_size; | |
| unsigned int(32) default_sample_flags | |
| } | |
| */ | |
| export type TrackTimes = { | |
| duration: number; | |
| keyFrameIndex?: number; | |
| keyFrameStart?: number; | |
| start: number; | |
| sampleCount: number; | |
| timescale: number; | |
| type: HdlrType; | |
| }; | |
| export function getSampleData( | |
| data: Uint8Array, | |
| initData: InitData, | |
| logger: ILogger, | |
| ): Record<number, TrackTimes> { | |
| const tracks: Record<number, TrackTimes> = {}; | |
| const trafs = findBox(data, ['moof', 'traf']); | |
| for (let i = 0; i < trafs.length; i++) { | |
| const traf = trafs[i]; | |
| // There is only one tfhd & trun per traf | |
| // This is true for CMAF style content, and we should perhaps check the ftyp | |
| // and only look for a single trun then, but for ISOBMFF we should check | |
| // for multiple track runs. | |
| const tfhd = findBox(traf, ['tfhd'])[0]; | |
| // get the track id from the tfhd | |
| const id = readUint32(tfhd, 4); | |
| const track = initData[id]; | |
| if (!track) { | |
| continue; | |
| } | |
| (tracks[id] as any) ||= { | |
| start: NaN, | |
| duration: 0, | |
| sampleCount: 0, | |
| timescale: track.timescale, | |
| type: track.type, | |
| }; | |
| const trackTimes: TrackTimes = tracks[id]; | |
| // get start DTS | |
| const tfdt = findBox(traf, ['tfdt'])[0]; | |
| if (tfdt as any) { | |
| const version = tfdt[0]; | |
| let baseTime = readUint32(tfdt, 4); | |
| if (version === 1) { | |
| // If value is too large, assume signed 64-bit. Negative track fragment decode times are invalid, but they exist in the wild. | |
| // This prevents large values from being used for initPTS, which can cause playlist sync issues. | |
| // https://github.com/video-dev/hls.js/issues/5303 | |
| if (baseTime === UINT32_MAX) { | |
| logger.warn( | |
| `[mp4-demuxer]: Ignoring assumed invalid signed 64-bit track fragment decode time`, | |
| ); | |
| } else { | |
| baseTime *= UINT32_MAX + 1; | |
| baseTime += readUint32(tfdt, 8); | |
| } | |
| } | |
| if ( | |
| Number.isFinite(baseTime) && | |
| (!Number.isFinite(trackTimes.start) || baseTime < trackTimes.start) | |
| ) { | |
| trackTimes.start = baseTime; | |
| } | |
| } | |
| const trackDefault = track.default; | |
| const tfhdFlags = readUint32(tfhd, 0) | trackDefault?.flags!; | |
| let defaultSampleDuration: number = trackDefault?.duration || 0; | |
| if (tfhdFlags & 0x000008) { | |
| // 0x000008 indicates the presence of the default_sample_duration field | |
| if (tfhdFlags & 0x000002) { | |
| // 0x000002 indicates the presence of the sample_description_index field, which precedes default_sample_duration | |
| // If present, the default_sample_duration exists at byte offset 12 | |
| defaultSampleDuration = readUint32(tfhd, 12); | |
| } else { | |
| // Otherwise, the duration is at byte offset 8 | |
| defaultSampleDuration = readUint32(tfhd, 8); | |
| } | |
| } | |
| const truns = findBox(traf, ['trun']); | |
| let sampleDTS = trackTimes.start || 0; | |
| let rawDuration = 0; | |
| let sampleDuration = defaultSampleDuration; | |
| for (let j = 0; j < truns.length; j++) { | |
| const trun = truns[j]; | |
| const sampleCount = readUint32(trun, 4); | |
| const sampleIndex = trackTimes.sampleCount; | |
| trackTimes.sampleCount += sampleCount; | |
| // Get duration from samples | |
| const dataOffsetPresent = trun[3] & 0x01; | |
| const firstSampleFlagsPresent = trun[3] & 0x04; | |
| const sampleDurationPresent = trun[2] & 0x01; | |
| const sampleSizePresent = trun[2] & 0x02; | |
| const sampleFlagsPresent = trun[2] & 0x04; | |
| const sampleCompositionTimeOffsetPresent = trun[2] & 0x08; | |
| let offset = 8; | |
| let remaining = sampleCount; | |
| if (dataOffsetPresent) { | |
| offset += 4; | |
| } | |
| if (firstSampleFlagsPresent && sampleCount) { | |
| const isNonSyncSample = trun[offset + 1] & 0x01; | |
| if (!isNonSyncSample && trackTimes.keyFrameIndex === undefined) { | |
| trackTimes.keyFrameIndex = sampleIndex; | |
| } | |
| offset += 4; | |
| if (sampleDurationPresent) { | |
| sampleDuration = readUint32(trun, offset); | |
| offset += 4; | |
| } else { | |
| sampleDuration = defaultSampleDuration; | |
| } | |
| if (sampleSizePresent) { | |
| offset += 4; | |
| } | |
| if (sampleCompositionTimeOffsetPresent) { | |
| offset += 4; | |
| } | |
| sampleDTS += sampleDuration; | |
| rawDuration += sampleDuration; | |
| remaining--; | |
| } | |
| while (remaining--) { | |
| if (sampleDurationPresent) { | |
| sampleDuration = readUint32(trun, offset); | |
| offset += 4; | |
| } else { | |
| sampleDuration = defaultSampleDuration; | |
| } | |
| if (sampleSizePresent) { | |
| offset += 4; | |
| } | |
| if (sampleFlagsPresent) { | |
| const isNonSyncSample = trun[offset + 1] & 0x01; | |
| if (!isNonSyncSample) { | |
| if (trackTimes.keyFrameIndex === undefined) { | |
| trackTimes.keyFrameIndex = | |
| trackTimes.sampleCount - (remaining + 1); | |
| trackTimes.keyFrameStart = sampleDTS; | |
| } | |
| } | |
| offset += 4; | |
| } | |
| if (sampleCompositionTimeOffsetPresent) { | |
| offset += 4; | |
| } | |
| sampleDTS += sampleDuration; | |
| rawDuration += sampleDuration; | |
| } | |
| if (!rawDuration && defaultSampleDuration) { | |
| rawDuration += defaultSampleDuration * sampleCount; | |
| } | |
| } | |
| trackTimes.duration += rawDuration; | |
| } | |
| if (!Object.keys(tracks).some((trackId) => tracks[trackId].duration)) { | |
| // If duration samples are not available in the traf use sidx subsegment_duration | |
| let sidxMinStart = Infinity; | |
| let sidxMaxEnd = 0; | |
| const sidxs = findBox(data, ['sidx']); | |
| for (let i = 0; i < sidxs.length; i++) { | |
| const sidx = parseSegmentIndex(sidxs[i]); | |
| if (sidx?.references) { | |
| sidxMinStart = Math.min( | |
| sidxMinStart, | |
| sidx.earliestPresentationTime / sidx.timescale, | |
| ); | |
| const subSegmentDuration = sidx.references.reduce( | |
| (dur, ref) => dur + ref.info.duration || 0, | |
| 0, | |
| ); | |
| sidxMaxEnd = Math.max( | |
| sidxMaxEnd, | |
| subSegmentDuration + sidx.earliestPresentationTime / sidx.timescale, | |
| ); | |
| } | |
| } | |
| if (sidxMaxEnd && Number.isFinite(sidxMaxEnd)) { | |
| Object.keys(tracks).forEach((trackId) => { | |
| if (!tracks[trackId].duration) { | |
| tracks[trackId].duration = | |
| sidxMaxEnd * tracks[trackId].timescale - tracks[trackId].start; | |
| } | |
| }); | |
| } | |
| } | |
| return tracks; | |
| } | |
| // TODO: Check if the last moof+mdat pair is part of the valid range | |
| export function segmentValidRange( | |
| data: Uint8Array<ArrayBuffer>, | |
| ): SegmentedRange { | |
| const segmentedRange: SegmentedRange = { | |
| valid: null, | |
| remainder: null, | |
| }; | |
| const moofs = findBox(data, ['moof']); | |
| if (moofs.length < 2) { | |
| segmentedRange.remainder = data; | |
| return segmentedRange; | |
| } | |
| const last = moofs[moofs.length - 1]; | |
| // Offset by 8 bytes; findBox offsets the start by as much | |
| segmentedRange.valid = data.slice(0, last.byteOffset - 8); | |
| segmentedRange.remainder = data.slice(last.byteOffset - 8); | |
| return segmentedRange; | |
| } | |
| export interface SegmentedRange { | |
| valid: Uint8Array<ArrayBuffer> | null; | |
| remainder: Uint8Array<ArrayBuffer> | null; | |
| } | |
| export function appendUint8Array(data1: Uint8Array, data2: Uint8Array) { | |
| const temp = new Uint8Array(data1.length + data2.length); | |
| temp.set(data1); | |
| temp.set(data2, data1.length); | |
| return temp; | |
| } | |
| export interface IEmsgParsingData { | |
| schemeIdUri: string; | |
| value: string; | |
| timeScale: number; | |
| presentationTimeDelta?: number; | |
| presentationTime?: number; | |
| eventDuration: number; | |
| id: number; | |
| payload: Uint8Array; | |
| } | |
| export function parseSamples( | |
| timeOffset: number, | |
| track: PassthroughTrack, | |
| ): UserdataSample[] { | |
| const seiSamples = [] as UserdataSample[]; | |
| const videoData = track.samples; | |
| const timescale = track.timescale; | |
| const trackId = track.id; | |
| let isHEVCFlavor = false; | |
| const moofs = findBox(videoData, ['moof']); | |
| moofs.map((moof) => { | |
| const moofOffset = moof.byteOffset - 8; | |
| const trafs = findBox(moof, ['traf']); | |
| trafs.map((traf) => { | |
| // get the base media decode time from the tfdt | |
| const baseTime = findBox(traf, ['tfdt']).map((tfdt) => { | |
| const version = tfdt[0]; | |
| let result = readUint32(tfdt, 4); | |
| if (version === 1) { | |
| result *= Math.pow(2, 32); | |
| result += readUint32(tfdt, 8); | |
| } | |
| return result / timescale; | |
| })[0]; | |
| if ((baseTime as any) !== undefined) { | |
| timeOffset = baseTime; | |
| } | |
| return findBox(traf, ['tfhd']).map((tfhd) => { | |
| const id = readUint32(tfhd, 4); | |
| const tfhdFlags = readUint32(tfhd, 0) & 0xffffff; | |
| const baseDataOffsetPresent = (tfhdFlags & 0x000001) !== 0; | |
| const sampleDescriptionIndexPresent = (tfhdFlags & 0x000002) !== 0; | |
| const defaultSampleDurationPresent = (tfhdFlags & 0x000008) !== 0; | |
| let defaultSampleDuration = 0; | |
| const defaultSampleSizePresent = (tfhdFlags & 0x000010) !== 0; | |
| let defaultSampleSize = 0; | |
| const defaultSampleFlagsPresent = (tfhdFlags & 0x000020) !== 0; | |
| let tfhdOffset = 8; | |
| if (id === trackId) { | |
| if (baseDataOffsetPresent) { | |
| tfhdOffset += 8; | |
| } | |
| if (sampleDescriptionIndexPresent) { | |
| tfhdOffset += 4; | |
| } | |
| if (defaultSampleDurationPresent) { | |
| defaultSampleDuration = readUint32(tfhd, tfhdOffset); | |
| tfhdOffset += 4; | |
| } | |
| if (defaultSampleSizePresent) { | |
| defaultSampleSize = readUint32(tfhd, tfhdOffset); | |
| tfhdOffset += 4; | |
| } | |
| if (defaultSampleFlagsPresent) { | |
| tfhdOffset += 4; | |
| } | |
| if (track.type === 'video') { | |
| isHEVCFlavor = isHEVC(track.codec); | |
| } | |
| findBox(traf, ['trun']).map((trun) => { | |
| const version = trun[0]; | |
| const flags = readUint32(trun, 0) & 0xffffff; | |
| const dataOffsetPresent = (flags & 0x000001) !== 0; | |
| let dataOffset = 0; | |
| const firstSampleFlagsPresent = (flags & 0x000004) !== 0; | |
| const sampleDurationPresent = (flags & 0x000100) !== 0; | |
| let sampleDuration = 0; | |
| const sampleSizePresent = (flags & 0x000200) !== 0; | |
| let sampleSize = 0; | |
| const sampleFlagsPresent = (flags & 0x000400) !== 0; | |
| const sampleCompositionOffsetsPresent = (flags & 0x000800) !== 0; | |
| let compositionOffset = 0; | |
| const sampleCount = readUint32(trun, 4); | |
| let trunOffset = 8; // past version, flags, and sample count | |
| if (dataOffsetPresent) { | |
| dataOffset = readUint32(trun, trunOffset); | |
| trunOffset += 4; | |
| } | |
| if (firstSampleFlagsPresent) { | |
| trunOffset += 4; | |
| } | |
| let sampleOffset = dataOffset + moofOffset; | |
| for (let ix = 0; ix < sampleCount; ix++) { | |
| if (sampleDurationPresent) { | |
| sampleDuration = readUint32(trun, trunOffset); | |
| trunOffset += 4; | |
| } else { | |
| sampleDuration = defaultSampleDuration; | |
| } | |
| if (sampleSizePresent) { | |
| sampleSize = readUint32(trun, trunOffset); | |
| trunOffset += 4; | |
| } else { | |
| sampleSize = defaultSampleSize; | |
| } | |
| if (sampleFlagsPresent) { | |
| trunOffset += 4; | |
| } | |
| if (sampleCompositionOffsetsPresent) { | |
| if (version === 0) { | |
| compositionOffset = readUint32(trun, trunOffset); | |
| } else { | |
| compositionOffset = readSint32(trun, trunOffset); | |
| } | |
| trunOffset += 4; | |
| } | |
| if (track.type === ElementaryStreamTypes.VIDEO) { | |
| let naluTotalSize = 0; | |
| while (naluTotalSize < sampleSize) { | |
| const naluSize = readUint32(videoData, sampleOffset); | |
| sampleOffset += 4; | |
| if (isSEIMessage(isHEVCFlavor, videoData[sampleOffset])) { | |
| const data = videoData.subarray( | |
| sampleOffset, | |
| sampleOffset + naluSize, | |
| ); | |
| parseSEIMessageFromNALu( | |
| data, | |
| isHEVCFlavor ? 2 : 1, | |
| timeOffset + compositionOffset / timescale, | |
| seiSamples, | |
| ); | |
| } | |
| sampleOffset += naluSize; | |
| naluTotalSize += naluSize + 4; | |
| } | |
| } | |
| timeOffset += sampleDuration / timescale; | |
| } | |
| }); | |
| } | |
| }); | |
| }); | |
| }); | |
| return seiSamples; | |
| } | |
| export function isHEVC(codec: string | undefined) { | |
| if (!codec) { | |
| return false; | |
| } | |
| const baseCodec = codec.substring(0, 4); | |
| return ( | |
| baseCodec === 'hvc1' || | |
| baseCodec === 'hev1' || | |
| // Dolby Vision | |
| baseCodec === 'dvh1' || | |
| baseCodec === 'dvhe' | |
| ); | |
| } | |
| function isSEIMessage(isHEVCFlavor: boolean, naluHeader: number) { | |
| if (isHEVCFlavor) { | |
| const naluType = (naluHeader >> 1) & 0x3f; | |
| return naluType === 39 || naluType === 40; | |
| } else { | |
| const naluType = naluHeader & 0x1f; | |
| return naluType === 6; | |
| } | |
| } | |
| export function parseSEIMessageFromNALu( | |
| unescapedData: Uint8Array, | |
| headerSize: number, | |
| pts: number, | |
| samples: UserdataSample[], | |
| ) { | |
| const data = discardEPB(unescapedData); | |
| let seiPtr = 0; | |
| // skip nal header | |
| seiPtr += headerSize; | |
| let payloadType = 0; | |
| let payloadSize = 0; | |
| let b = 0; | |
| while (seiPtr < data.length) { | |
| payloadType = 0; | |
| do { | |
| if (seiPtr >= data.length) { | |
| break; | |
| } | |
| b = data[seiPtr++]; | |
| payloadType += b; | |
| } while (b === 0xff); | |
| // Parse payload size. | |
| payloadSize = 0; | |
| do { | |
| if (seiPtr >= data.length) { | |
| break; | |
| } | |
| b = data[seiPtr++]; | |
| payloadSize += b; | |
| } while (b === 0xff); | |
| const leftOver = data.length - seiPtr; | |
| // Create a variable to process the payload | |
| let payPtr = seiPtr; | |
| // Increment the seiPtr to the end of the payload | |
| if (payloadSize < leftOver) { | |
| seiPtr += payloadSize; | |
| } else if (payloadSize > leftOver) { | |
| // Some type of corruption has happened? | |
| logger.error( | |
| `Malformed SEI payload. ${payloadSize} is too small, only ${leftOver} bytes left to parse.`, | |
| ); | |
| // We might be able to parse some data, but let's be safe and ignore it. | |
| break; | |
| } | |
| if (payloadType === 4) { | |
| const countryCode = data[payPtr++]; | |
| if (countryCode === 181) { | |
| const providerCode = readUint16(data, payPtr); | |
| payPtr += 2; | |
| if (providerCode === 49) { | |
| const userStructure = readUint32(data, payPtr); | |
| payPtr += 4; | |
| if (userStructure === 0x47413934) { | |
| const userDataType = data[payPtr++]; | |
| // Raw CEA-608 bytes wrapped in CEA-708 packet | |
| if (userDataType === 3) { | |
| const firstByte = data[payPtr++]; | |
| const totalCCs = 0x1f & firstByte; | |
| const enabled = 0x40 & firstByte; | |
| const totalBytes = enabled ? 2 + totalCCs * 3 : 0; | |
| const byteArray = new Uint8Array(totalBytes); | |
| if (enabled) { | |
| byteArray[0] = firstByte; | |
| for (let i = 1; i < totalBytes; i++) { | |
| byteArray[i] = data[payPtr++]; | |
| } | |
| } | |
| samples.push({ | |
| type: userDataType, | |
| payloadType, | |
| pts, | |
| bytes: byteArray, | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| } else if (payloadType === 5) { | |
| if (payloadSize > 16) { | |
| const uuidStrArray: Array<string> = []; | |
| for (let i = 0; i < 16; i++) { | |
| const b = data[payPtr++].toString(16); | |
| uuidStrArray.push(b.length == 1 ? '0' + b : b); | |
| if (i === 3 || i === 5 || i === 7 || i === 9) { | |
| uuidStrArray.push('-'); | |
| } | |
| } | |
| const length = payloadSize - 16; | |
| const userDataBytes = new Uint8Array(length); | |
| for (let i = 0; i < length; i++) { | |
| userDataBytes[i] = data[payPtr++]; | |
| } | |
| samples.push({ | |
| payloadType, | |
| pts, | |
| uuid: uuidStrArray.join(''), | |
| userData: utf8ArrayToStr(userDataBytes), | |
| userDataBytes, | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * remove Emulation Prevention bytes from a RBSP | |
| */ | |
| export function discardEPB(data: Uint8Array): Uint8Array { | |
| const length = data.byteLength; | |
| const EPBPositions = [] as Array<number>; | |
| let i = 1; | |
| // Find all `Emulation Prevention Bytes` | |
| while (i < length - 2) { | |
| if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) { | |
| EPBPositions.push(i + 2); | |
| i += 2; | |
| } else { | |
| i++; | |
| } | |
| } | |
| // If no Emulation Prevention Bytes were found just return the original | |
| // array | |
| if (EPBPositions.length === 0) { | |
| return data; | |
| } | |
| // Create a new array to hold the NAL unit data | |
| const newLength = length - EPBPositions.length; | |
| const newData = new Uint8Array(newLength); | |
| let sourceIndex = 0; | |
| for (i = 0; i < newLength; sourceIndex++, i++) { | |
| if (sourceIndex === EPBPositions[0]) { | |
| // Skip this byte | |
| sourceIndex++; | |
| // Remove this position index | |
| EPBPositions.shift(); | |
| } | |
| newData[i] = data[sourceIndex]; | |
| } | |
| return newData; | |
| } | |
| export function parseEmsg(data: Uint8Array): IEmsgParsingData { | |
| const version = data[0]; | |
| let schemeIdUri: string = ''; | |
| let value: string = ''; | |
| let timeScale: number = 0; | |
| let presentationTimeDelta: number = 0; | |
| let presentationTime: number = 0; | |
| let eventDuration: number = 0; | |
| let id: number = 0; | |
| let offset: number = 0; | |
| if (version === 0) { | |
| while (bin2str(data.subarray(offset, offset + 1)) !== '\0') { | |
| schemeIdUri += bin2str(data.subarray(offset, offset + 1)); | |
| offset += 1; | |
| } | |
| schemeIdUri += bin2str(data.subarray(offset, offset + 1)); | |
| offset += 1; | |
| while (bin2str(data.subarray(offset, offset + 1)) !== '\0') { | |
| value += bin2str(data.subarray(offset, offset + 1)); | |
| offset += 1; | |
| } | |
| value += bin2str(data.subarray(offset, offset + 1)); | |
| offset += 1; | |
| timeScale = readUint32(data, 12); | |
| presentationTimeDelta = readUint32(data, 16); | |
| eventDuration = readUint32(data, 20); | |
| id = readUint32(data, 24); | |
| offset = 28; | |
| } else if (version === 1) { | |
| offset += 4; | |
| timeScale = readUint32(data, offset); | |
| offset += 4; | |
| const leftPresentationTime = readUint32(data, offset); | |
| offset += 4; | |
| const rightPresentationTime = readUint32(data, offset); | |
| offset += 4; | |
| presentationTime = 2 ** 32 * leftPresentationTime + rightPresentationTime; | |
| if (!Number.isSafeInteger(presentationTime)) { | |
| presentationTime = Number.MAX_SAFE_INTEGER; | |
| logger.warn( | |
| 'Presentation time exceeds safe integer limit and wrapped to max safe integer in parsing emsg box', | |
| ); | |
| } | |
| eventDuration = readUint32(data, offset); | |
| offset += 4; | |
| id = readUint32(data, offset); | |
| offset += 4; | |
| while (bin2str(data.subarray(offset, offset + 1)) !== '\0') { | |
| schemeIdUri += bin2str(data.subarray(offset, offset + 1)); | |
| offset += 1; | |
| } | |
| schemeIdUri += bin2str(data.subarray(offset, offset + 1)); | |
| offset += 1; | |
| while (bin2str(data.subarray(offset, offset + 1)) !== '\0') { | |
| value += bin2str(data.subarray(offset, offset + 1)); | |
| offset += 1; | |
| } | |
| value += bin2str(data.subarray(offset, offset + 1)); | |
| offset += 1; | |
| } | |
| const payload = data.subarray(offset, data.byteLength); | |
| return { | |
| schemeIdUri, | |
| value, | |
| timeScale, | |
| presentationTime, | |
| presentationTimeDelta, | |
| eventDuration, | |
| id, | |
| payload, | |
| }; | |
| } | |
| export function mp4Box(type: ArrayLike<number>, ...payload: Uint8Array[]) { | |
| const len = payload.length; | |
| let size = 8; | |
| let i = len; | |
| while (i--) { | |
| size += payload[i].byteLength; | |
| } | |
| const result = new Uint8Array(size); | |
| result[0] = (size >> 24) & 0xff; | |
| result[1] = (size >> 16) & 0xff; | |
| result[2] = (size >> 8) & 0xff; | |
| result[3] = size & 0xff; | |
| result.set(type, 4); | |
| for (i = 0, size = 8; i < len; i++) { | |
| result.set(payload[i], size); | |
| size += payload[i].byteLength; | |
| } | |
| return result; | |
| } | |
| export function mp4pssh( | |
| systemId: Uint8Array, | |
| keyids: Array<Uint8Array> | null, | |
| data: Uint8Array, | |
| ) { | |
| if (systemId.byteLength !== 16) { | |
| throw new RangeError('Invalid system id'); | |
| } | |
| let version; | |
| let kids; | |
| if (keyids) { | |
| version = 1; | |
| kids = new Uint8Array(keyids.length * 16); | |
| for (let ix = 0; ix < keyids.length; ix++) { | |
| const k = keyids[ix]; // uint8array | |
| if (k.byteLength !== 16) { | |
| throw new RangeError('Invalid key'); | |
| } | |
| kids.set(k, ix * 16); | |
| } | |
| } else { | |
| version = 0; | |
| kids = new Uint8Array(); | |
| } | |
| let kidCount; | |
| if (version > 0) { | |
| kidCount = new Uint8Array(4); | |
| if (keyids!.length > 0) { | |
| new DataView(kidCount.buffer).setUint32(0, keyids!.length, false); | |
| } | |
| } else { | |
| kidCount = new Uint8Array(); | |
| } | |
| const dataSize = new Uint8Array(4); | |
| if (data.byteLength > 0) { | |
| new DataView(dataSize.buffer).setUint32(0, data.byteLength, false); | |
| } | |
| return mp4Box( | |
| [112, 115, 115, 104], | |
| new Uint8Array([ | |
| version, | |
| 0x00, | |
| 0x00, | |
| 0x00, // Flags | |
| ]), | |
| systemId, // 16 bytes | |
| kidCount, | |
| kids, | |
| dataSize, | |
| data, | |
| ); | |
| } | |
| export type PsshData = { | |
| version: 0 | 1; | |
| systemId: KeySystemIds; | |
| kids: null | Uint8Array<ArrayBuffer>[]; | |
| data: null | Uint8Array<ArrayBuffer>; | |
| offset: number; | |
| size: number; | |
| }; | |
| export type PsshInvalidResult = { | |
| systemId?: undefined; | |
| kids?: undefined; | |
| offset: number; | |
| size: number; | |
| }; | |
| export function parseMultiPssh( | |
| initData: ArrayBuffer, | |
| ): (PsshData | PsshInvalidResult)[] { | |
| const results: (PsshData | PsshInvalidResult)[] = []; | |
| if (initData instanceof ArrayBuffer) { | |
| const length = initData.byteLength; | |
| let offset = 0; | |
| while (offset + 32 < length) { | |
| const view = new DataView(initData, offset); | |
| const pssh = parsePssh(view); | |
| results.push(pssh); | |
| offset += pssh.size; | |
| } | |
| } | |
| return results; | |
| } | |
| function parsePssh(view: DataView<ArrayBuffer>): PsshData | PsshInvalidResult { | |
| const size = view.getUint32(0); | |
| const offset = view.byteOffset; | |
| const length = view.byteLength; | |
| if (length < size) { | |
| return { | |
| offset, | |
| size: length, | |
| }; | |
| } | |
| const type = view.getUint32(4); | |
| if (type !== 0x70737368) { | |
| return { offset, size }; | |
| } | |
| const version = view.getUint32(8) >>> 24; | |
| if (version !== 0 && version !== 1) { | |
| return { offset, size }; | |
| } | |
| const buffer = view.buffer; | |
| const systemId = arrayToHex( | |
| new Uint8Array(buffer, offset + 12, 16), | |
| ) as KeySystemIds; | |
| let kids: null | Uint8Array<ArrayBuffer>[] = null; | |
| let data: null | Uint8Array<ArrayBuffer> = null; | |
| let dataSizeOffset = 0; | |
| if (version === 0) { | |
| dataSizeOffset = 28; | |
| } else { | |
| const kidCounts = view.getUint32(28); | |
| if (!kidCounts || length < 32 + kidCounts * 16) { | |
| return { offset, size }; | |
| } | |
| kids = []; | |
| for (let i = 0; i < kidCounts; i++) { | |
| kids.push(new Uint8Array(buffer, offset + 32 + i * 16, 16)); | |
| } | |
| dataSizeOffset = 32 + kidCounts * 16; | |
| } | |
| if (!dataSizeOffset) { | |
| return { offset, size }; | |
| } | |
| const dataSizeOrKidCount = view.getUint32(dataSizeOffset); | |
| if (size - 32 < dataSizeOrKidCount) { | |
| return { offset, size }; | |
| } | |
| data = new Uint8Array( | |
| buffer, | |
| offset + dataSizeOffset + 4, | |
| dataSizeOrKidCount, | |
| ); | |
| return { | |
| version, | |
| systemId, | |
| kids, | |
| data, | |
| offset, | |
| size, | |
| }; | |
| } | |
Xet Storage Details
- Size:
- 43.5 kB
- Xet hash:
- 1bb859ae3363423ce099472c0e4c005ca0e077348bf2a09538263e8be24e76d6
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.