Buckets:
dieumercimvemba/mediastore / scripts /whatsapp-bridge /node_modules /music-metadata /lib /mp4 /MP4Parser.js
| import initDebug from 'debug'; | |
| import * as Token from 'token-types'; | |
| import { BasicParser } from '../common/BasicParser.js'; | |
| import { Genres } from '../id3v1/ID3v1Parser.js'; | |
| import { Atom } from './Atom.js'; | |
| import * as AtomToken from './AtomToken.js'; | |
| import { ChapterTrackReferenceBox, Mp4ContentError } from './AtomToken.js'; | |
| import { TrackType } from '../type.js'; | |
| import { uint8ArrayToHex } from 'uint8array-extras'; | |
| import { textDecode } from '@borewit/text-codec'; | |
| const debug = initDebug('music-metadata:parser:MP4'); | |
| const tagFormat = 'iTunes'; | |
| const encoderDict = { | |
| raw: { | |
| lossy: false, | |
| format: 'raw' | |
| }, | |
| MAC3: { | |
| lossy: true, | |
| format: 'MACE 3:1' | |
| }, | |
| MAC6: { | |
| lossy: true, | |
| format: 'MACE 6:1' | |
| }, | |
| ima4: { | |
| lossy: true, | |
| format: 'IMA 4:1' | |
| }, | |
| ulaw: { | |
| lossy: true, | |
| format: 'uLaw 2:1' | |
| }, | |
| alaw: { | |
| lossy: true, | |
| format: 'uLaw 2:1' | |
| }, | |
| Qclp: { | |
| lossy: true, | |
| format: 'QUALCOMM PureVoice' | |
| }, | |
| '.mp3': { | |
| lossy: true, | |
| format: 'MPEG-1 layer 3' | |
| }, | |
| alac: { | |
| lossy: false, | |
| format: 'ALAC' | |
| }, | |
| 'ac-3': { | |
| lossy: true, | |
| format: 'AC-3' | |
| }, | |
| mp4a: { | |
| lossy: true, | |
| format: 'MPEG-4/AAC' | |
| }, | |
| mp4s: { | |
| lossy: true, | |
| format: 'MP4S' | |
| }, | |
| // Closed Captioning Media, https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html#//apple_ref/doc/uid/TP40000939-CH205-SW87 | |
| c608: { | |
| lossy: true, | |
| format: 'CEA-608' | |
| }, | |
| c708: { | |
| lossy: true, | |
| format: 'CEA-708' | |
| } | |
| }; | |
| function distinct(value, index, self) { | |
| return self.indexOf(value) === index; | |
| } | |
| /* | |
| * Parser for the MP4 (MPEG-4 Part 14) container format | |
| * Standard: ISO/IEC 14496-14 | |
| * supporting: | |
| * - QuickTime container | |
| * - MP4 File Format | |
| * - 3GPP file format | |
| * - 3GPP2 file format | |
| * | |
| * MPEG-4 Audio / Part 3 (.m4a)& MPEG 4 Video (m4v, mp4) extension. | |
| * Support for Apple iTunes tags as found in a M4A/M4V files. | |
| * Ref: | |
| * https://en.wikipedia.org/wiki/ISO_base_media_file_format | |
| * https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html | |
| * http://atomicparsley.sourceforge.net/mpeg-4files.html | |
| * https://github.com/sergiomb2/libmp4v2/wiki/iTunesMetadata | |
| * https://wiki.multimedia.cx/index.php/QuickTime_container | |
| */ | |
| export class MP4Parser extends BasicParser { | |
| constructor() { | |
| super(...arguments); | |
| this.tracks = new Map(); | |
| this.hasVideoTrack = false; | |
| this.hasAudioTrack = true; | |
| this.atomParsers = { | |
| /** | |
| * Parse movie header (mvhd) atom | |
| * Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-56313 | |
| */ | |
| mvhd: async (len) => { | |
| const mvhd = await this.tokenizer.readToken(new AtomToken.MvhdAtom(len)); | |
| this.metadata.setFormat('creationTime', mvhd.creationTime); | |
| this.metadata.setFormat('modificationTime', mvhd.modificationTime); | |
| }, | |
| chap: async (len) => { | |
| const td = this.getTrackDescription(); | |
| const trackIds = []; | |
| while (len >= Token.UINT32_BE.len) { | |
| trackIds.push(await this.tokenizer.readNumber(Token.UINT32_BE)); | |
| len -= Token.UINT32_BE.len; | |
| } | |
| td.chapterList = trackIds; | |
| }, | |
| /** | |
| * Parse mdat atom. | |
| * Will scan for chapters | |
| */ | |
| mdat: async (len) => { | |
| if (this.options.includeChapters) { | |
| const trackWithChapters = [...this.tracks.values()].filter(track => track.chapterList); | |
| if (trackWithChapters.length === 1) { | |
| const chapterTrackIds = trackWithChapters[0].chapterList; | |
| const chapterTracks = [...this.tracks.values()].filter(track => chapterTrackIds.indexOf(track.header.trackId) !== -1); | |
| if (chapterTracks.length === 1) { | |
| return this.parseChapterTrack(chapterTracks[0], trackWithChapters[0], len); | |
| } | |
| } | |
| } | |
| await this.tokenizer.ignore(len); | |
| }, | |
| ftyp: async (len) => { | |
| const types = []; | |
| while (len > 0) { | |
| const ftype = await this.tokenizer.readToken(AtomToken.ftyp); | |
| len -= AtomToken.ftyp.len; | |
| const value = ftype.type.replace(/\W/g, ''); | |
| if (value.length > 0) { | |
| types.push(value); // unshift for backward compatibility | |
| } | |
| } | |
| debug(`ftyp: ${types.join('/')}`); | |
| const x = types.filter(distinct).join('/'); | |
| this.metadata.setFormat('container', x); | |
| }, | |
| /** | |
| * Parse sample description atom | |
| */ | |
| stsd: async (len) => { | |
| const stsd = await this.tokenizer.readToken(new AtomToken.StsdAtom(len)); | |
| const trackDescription = this.getTrackDescription(); | |
| trackDescription.soundSampleDescription = stsd.table.map(dfEntry => this.parseSoundSampleDescription(dfEntry)); | |
| }, | |
| /** | |
| * Parse sample-sizes atom ('stsz') | |
| */ | |
| stsz: async (len) => { | |
| const stsz = await this.tokenizer.readToken(new AtomToken.StszAtom(len)); | |
| const td = this.getTrackDescription(); | |
| td.sampleSize = stsz.sampleSize; | |
| td.sampleSizeTable = stsz.entries; | |
| }, | |
| date: async (len) => { | |
| const date = await this.tokenizer.readToken(new Token.StringType(len, 'utf-8')); | |
| await this.addTag('date', date); | |
| } | |
| }; | |
| } | |
| static read_BE_Integer(array, signed) { | |
| const integerType = (signed ? 'INT' : 'UINT') + array.length * 8 + (array.length > 1 ? '_BE' : ''); | |
| const token = Token[integerType]; | |
| if (!token) { | |
| throw new Mp4ContentError(`Token for integer type not found: "${integerType}"`); | |
| } | |
| return Number(token.get(array, 0)); | |
| } | |
| async parse() { | |
| this.hasVideoTrack = false; | |
| this.hasAudioTrack = true; | |
| this.tracks.clear(); | |
| let remainingFileSize = this.tokenizer.fileInfo.size || 0; | |
| while (!this.tokenizer.fileInfo.size || remainingFileSize > 0) { | |
| try { | |
| const token = await this.tokenizer.peekToken(AtomToken.Header); | |
| if (token.name === '\0\0\0\0') { | |
| const errMsg = `Error at offset=${this.tokenizer.position}: box.id=0`; | |
| debug(errMsg); | |
| this.addWarning(errMsg); | |
| break; | |
| } | |
| } | |
| catch (error) { | |
| if (error instanceof Error) { | |
| const errMsg = `Error at offset=${this.tokenizer.position}: ${error.message}`; | |
| debug(errMsg); | |
| this.addWarning(errMsg); | |
| } | |
| else | |
| throw error; | |
| break; | |
| } | |
| const rootAtom = await Atom.readAtom(this.tokenizer, (atom, remaining) => this.handleAtom(atom, remaining), null, remainingFileSize); | |
| remainingFileSize -= rootAtom.header.length === BigInt(0) ? remainingFileSize : Number(rootAtom.header.length); | |
| } | |
| // Post process metadata | |
| const formatList = []; | |
| this.tracks.forEach(track => { | |
| const trackFormats = []; | |
| track.soundSampleDescription.forEach(ssd => { | |
| const streamInfo = {}; | |
| const encoderInfo = encoderDict[ssd.dataFormat]; | |
| if (encoderInfo) { | |
| trackFormats.push(encoderInfo.format); | |
| streamInfo.codecName = encoderInfo.format; | |
| } | |
| else { | |
| streamInfo.codecName = `<${ssd.dataFormat}>`; | |
| } | |
| if (ssd.description) { | |
| const { description } = ssd; | |
| if (description.sampleRate > 0) { | |
| streamInfo.type = TrackType.audio; | |
| streamInfo.audio = { | |
| samplingFrequency: description.sampleRate, | |
| bitDepth: description.sampleSize, | |
| channels: description.numAudioChannels | |
| }; | |
| } | |
| } | |
| this.metadata.addStreamInfo(streamInfo); | |
| }); | |
| if (trackFormats.length >= 1) { | |
| formatList.push(trackFormats.join('/')); | |
| } | |
| }); | |
| if (formatList.length > 0) { | |
| this.metadata.setFormat('codec', formatList.filter(distinct).join('+')); | |
| } | |
| const audioTracks = [...this.tracks.values()].filter(track => { | |
| return track.soundSampleDescription.length >= 1 && track.soundSampleDescription[0].description && track.soundSampleDescription[0].description.numAudioChannels > 0; | |
| }); | |
| // Calculate duration and bitrate of audio tracks | |
| for (const audioTrack of audioTracks) { | |
| if (audioTrack.media.header && audioTrack.media.header.timeScale > 0) { | |
| audioTrack.sampleRate = audioTrack.media.header.timeScale; | |
| if (audioTrack.media.header.duration > 0) { | |
| debug('Using duration defined on audio track'); | |
| audioTrack.samples = audioTrack.media.header.duration; | |
| audioTrack.duration = audioTrack.samples / audioTrack.sampleRate; | |
| } | |
| if (audioTrack.fragments.length > 0) { | |
| debug('Calculate duration defined in track fragments'); | |
| let totalTimeUnits = 0; | |
| audioTrack.sizeInBytes = 0; | |
| for (const fragment of audioTrack.fragments) { | |
| for (const sample of fragment.trackRun.samples) { | |
| const dur = sample.sampleDuration ?? fragment.header.defaultSampleDuration ?? 0; | |
| const size = sample.sampleSize ?? fragment.header.defaultSampleSize ?? 0; | |
| if (dur === 0) { | |
| throw new Error("Missing sampleDuration and no defaultSampleDuration in track fragment header"); | |
| } | |
| if (size === 0) { | |
| throw new Error("Missing sampleSize and no defaultSampleSize in track fragment header"); | |
| } | |
| totalTimeUnits += dur; | |
| audioTrack.sizeInBytes += size; | |
| } | |
| } | |
| if (!audioTrack.samples) { | |
| audioTrack.samples = totalTimeUnits; | |
| } | |
| if (!audioTrack.duration) { | |
| audioTrack.duration = totalTimeUnits / audioTrack.sampleRate; | |
| } | |
| } | |
| else if (audioTrack.sampleSizeTable.length > 0) { | |
| audioTrack.sizeInBytes = audioTrack.sampleSizeTable.reduce((sum, n) => sum + n, 0); | |
| } | |
| } | |
| const ssd = audioTrack.soundSampleDescription[0]; | |
| if (ssd.description && audioTrack.media.header) { | |
| this.metadata.setFormat('sampleRate', ssd.description.sampleRate); | |
| this.metadata.setFormat('bitsPerSample', ssd.description.sampleSize); | |
| this.metadata.setFormat('numberOfChannels', ssd.description.numAudioChannels); | |
| if (audioTrack.media.header.timeScale === 0 && audioTrack.timeToSampleTable.length > 0) { | |
| const totalSampleSize = audioTrack.timeToSampleTable | |
| .map(ttstEntry => ttstEntry.count * ttstEntry.duration) | |
| .reduce((total, sampleSize) => total + sampleSize); | |
| audioTrack.duration = totalSampleSize / ssd.description.sampleRate; | |
| } | |
| } | |
| const encoderInfo = encoderDict[ssd.dataFormat]; | |
| if (encoderInfo) { | |
| this.metadata.setFormat('lossless', !encoderInfo.lossy); | |
| } | |
| } | |
| if (audioTracks.length >= 1) { | |
| const firstAudioTrack = audioTracks[0]; | |
| if (firstAudioTrack.duration) { | |
| this.metadata.setFormat('duration', firstAudioTrack.duration); | |
| if (firstAudioTrack.sizeInBytes) { | |
| this.metadata.setFormat('bitrate', 8 * firstAudioTrack.sizeInBytes / firstAudioTrack.duration); | |
| } | |
| } | |
| } | |
| this.metadata.setFormat('hasAudio', this.hasAudioTrack); | |
| this.metadata.setFormat('hasVideo', this.hasVideoTrack); | |
| } | |
| async handleAtom(atom, remaining) { | |
| if (atom.parent) { | |
| switch (atom.parent.header.name) { | |
| case 'ilst': | |
| case '<id>': | |
| return this.parseMetadataItemData(atom); | |
| case 'moov': | |
| switch (atom.header.name) { | |
| case 'trak': | |
| return this.parseTrackBox(atom); | |
| case 'udta': | |
| return this.parseTrackBox(atom); | |
| } | |
| break; | |
| case 'moof': | |
| switch (atom.header.name) { | |
| case 'traf': | |
| return this.parseTrackFragmentBox(atom); | |
| } | |
| } | |
| } | |
| // const payloadLength = atom.getPayloadLength(remaining); | |
| if (this.atomParsers[atom.header.name]) { | |
| return this.atomParsers[atom.header.name](remaining); | |
| } | |
| debug(`No parser for atom path=${atom.atomPath}, payload-len=${remaining}, ignoring atom`); | |
| await this.tokenizer.ignore(remaining); | |
| } | |
| getTrackDescription() { | |
| // ToDo: pick the right track, not the last track!!!! | |
| const tracks = [...this.tracks.values()]; | |
| return tracks[tracks.length - 1]; | |
| } | |
| async addTag(id, value) { | |
| await this.metadata.addTag(tagFormat, id, value); | |
| } | |
| addWarning(message) { | |
| debug(`Warning: ${message}`); | |
| this.metadata.addWarning(message); | |
| } | |
| /** | |
| * Parse data of Meta-item-list-atom (item of 'ilst' atom) | |
| * @param metaAtom | |
| * Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW8 | |
| */ | |
| parseMetadataItemData(metaAtom) { | |
| let tagKey = metaAtom.header.name; | |
| return metaAtom.readAtoms(this.tokenizer, async (child, remaining) => { | |
| const payLoadLength = child.getPayloadLength(remaining); | |
| switch (child.header.name) { | |
| case 'data': // value atom | |
| return this.parseValueAtom(tagKey, child); | |
| case 'name': // name atom (optional) | |
| case 'mean': | |
| case 'rate': { | |
| const name = await this.tokenizer.readToken(new AtomToken.NameAtom(payLoadLength)); | |
| tagKey += `:${name.name}`; | |
| break; | |
| } | |
| default: { | |
| const uint8Array = await this.tokenizer.readToken(new Token.Uint8ArrayType(payLoadLength)); | |
| this.addWarning(`Unsupported meta-item: ${tagKey}[${child.header.name}] => value=${uint8ArrayToHex(uint8Array)} ascii=${textDecode(uint8Array, 'ascii')}`); | |
| } | |
| } | |
| }, metaAtom.getPayloadLength(0)); | |
| } | |
| async parseValueAtom(tagKey, metaAtom) { | |
| const dataAtom = await this.tokenizer.readToken(new AtomToken.DataAtom(Number(metaAtom.header.length) - AtomToken.Header.len)); | |
| if (dataAtom.type.set !== 0) { | |
| throw new Mp4ContentError(`Unsupported type-set != 0: ${dataAtom.type.set}`); | |
| } | |
| // Use well-known-type table | |
| // Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW35 | |
| switch (dataAtom.type.type) { | |
| case 0: // reserved: Reserved for use where no type needs to be indicated | |
| switch (tagKey) { | |
| case 'trkn': | |
| case 'disk': { | |
| const num = Token.UINT8.get(dataAtom.value, 3); | |
| const of = Token.UINT8.get(dataAtom.value, 5); | |
| // console.log(" %s[data] = %s/%s", tagKey, num, of); | |
| await this.addTag(tagKey, `${num}/${of}`); | |
| break; | |
| } | |
| case 'gnre': { | |
| const genreInt = Token.UINT8.get(dataAtom.value, 1); | |
| const genreStr = Genres[genreInt - 1]; | |
| // console.log(" %s[data] = %s", tagKey, genreStr); | |
| await this.addTag(tagKey, genreStr); | |
| break; | |
| } | |
| case 'rate': { | |
| const rate = textDecode(dataAtom.value, 'ascii'); | |
| await this.addTag(tagKey, rate); | |
| break; | |
| } | |
| default: | |
| debug(`unknown proprietary value type for: ${metaAtom.atomPath}`); | |
| } | |
| break; | |
| case 1: // UTF-8: Without any count or NULL terminator | |
| case 18: // Unknown: Found in m4b in combination with a '©gen' tag | |
| await this.addTag(tagKey, textDecode(dataAtom.value)); | |
| break; | |
| case 13: // JPEG | |
| if (this.options.skipCovers) | |
| break; | |
| await this.addTag(tagKey, { | |
| format: 'image/jpeg', | |
| data: Uint8Array.from(dataAtom.value) | |
| }); | |
| break; | |
| case 14: // PNG | |
| if (this.options.skipCovers) | |
| break; | |
| await this.addTag(tagKey, { | |
| format: 'image/png', | |
| data: Uint8Array.from(dataAtom.value) | |
| }); | |
| break; | |
| case 21: // BE Signed Integer | |
| await this.addTag(tagKey, MP4Parser.read_BE_Integer(dataAtom.value, true)); | |
| break; | |
| case 22: // BE Unsigned Integer | |
| await this.addTag(tagKey, MP4Parser.read_BE_Integer(dataAtom.value, false)); | |
| break; | |
| case 65: // An 8-bit signed integer | |
| await this.addTag(tagKey, Token.UINT8.get(dataAtom.value, 0)); | |
| break; | |
| case 66: // A big-endian 16-bit signed integer | |
| await this.addTag(tagKey, Token.UINT16_BE.get(dataAtom.value, 0)); | |
| break; | |
| case 67: // A big-endian 32-bit signed integer | |
| await this.addTag(tagKey, Token.UINT32_BE.get(dataAtom.value, 0)); | |
| break; | |
| default: | |
| this.addWarning(`atom key=${tagKey}, has unknown well-known-type (data-type): ${dataAtom.type.type}`); | |
| } | |
| } | |
| async parseTrackBox(trakBox) { | |
| // @ts-expect-error | |
| const track = { | |
| media: {}, | |
| fragments: [] | |
| }; | |
| await trakBox.readAtoms(this.tokenizer, async (child, remaining) => { | |
| const payLoadLength = child.getPayloadLength(remaining); | |
| switch (child.header.name) { | |
| case 'chap': { | |
| const chap = await this.tokenizer.readToken(new ChapterTrackReferenceBox(remaining)); | |
| track.chapterList = chap; | |
| break; | |
| } | |
| case 'tkhd': // TrackHeaderBox | |
| track.header = await this.tokenizer.readToken(new AtomToken.TrackHeaderAtom(payLoadLength)); | |
| break; | |
| case 'hdlr': // TrackHeaderBox | |
| track.handler = await this.tokenizer.readToken(new AtomToken.HandlerBox(payLoadLength)); | |
| track.isAudio = () => track.handler.handlerType === 'audi' || track.handler.handlerType === 'soun'; | |
| track.isVideo = () => track.handler.handlerType === 'vide'; | |
| if (track.isAudio()) { | |
| this.hasAudioTrack = true; | |
| } | |
| else if (track.isVideo()) { | |
| this.hasVideoTrack = true; | |
| } | |
| break; | |
| case 'mdhd': { // Parse media header (mdhd) box | |
| const mdhd_data = await this.tokenizer.readToken(new AtomToken.MdhdAtom(payLoadLength)); | |
| track.media.header = mdhd_data; | |
| break; | |
| } | |
| case 'stco': { | |
| const stco = await this.tokenizer.readToken(new AtomToken.StcoAtom(payLoadLength)); | |
| track.chunkOffsetTable = stco.entries; // remember chunk offsets | |
| break; | |
| } | |
| case 'stsc': { // sample-to-Chunk box | |
| const stsc = await this.tokenizer.readToken(new AtomToken.StscAtom(payLoadLength)); | |
| track.sampleToChunkTable = stsc.entries; | |
| break; | |
| } | |
| case 'stsd': { // sample description box | |
| const stsd = await this.tokenizer.readToken(new AtomToken.StsdAtom(payLoadLength)); | |
| track.soundSampleDescription = stsd.table.map(dfEntry => this.parseSoundSampleDescription(dfEntry)); | |
| break; | |
| } | |
| case 'stts': { // time-to-sample table | |
| const stts = await this.tokenizer.readToken(new AtomToken.SttsAtom(payLoadLength)); | |
| track.timeToSampleTable = stts.entries; | |
| break; | |
| } | |
| case 'stsz': { | |
| const stsz = await this.tokenizer.readToken(new AtomToken.StszAtom(payLoadLength)); | |
| track.sampleSize = stsz.sampleSize; | |
| track.sampleSizeTable = stsz.entries; | |
| break; | |
| } | |
| case 'dinf': | |
| case 'vmhd': | |
| case 'smhd': | |
| debug(`Ignoring: ${child.header.name}`); | |
| await this.tokenizer.ignore(payLoadLength); | |
| break; | |
| default: { | |
| debug(`Unexpected track box: ${child.header.name}`); | |
| await this.tokenizer.ignore(payLoadLength); | |
| } | |
| } | |
| }, trakBox.getPayloadLength(0)); | |
| // Register track | |
| this.tracks.set(track.header.trackId, track); | |
| } | |
| parseTrackFragmentBox(trafBox) { | |
| let tfhd; | |
| return trafBox.readAtoms(this.tokenizer, async (child, remaining) => { | |
| const payLoadLength = child.getPayloadLength(remaining); | |
| switch (child.header.name) { | |
| case 'tfhd': { // TrackFragmentHeaderBox | |
| const fragmentHeaderBox = new AtomToken.TrackFragmentHeaderBox(child.getPayloadLength(remaining)); | |
| tfhd = await this.tokenizer.readToken(fragmentHeaderBox); | |
| break; | |
| } | |
| case 'tfdt': // TrackFragmentBaseMediaDecodeTimeBo | |
| await this.tokenizer.ignore(payLoadLength); | |
| break; | |
| case 'trun': { // TrackRunBox | |
| const trackRunBox = new AtomToken.TrackRunBox(payLoadLength); | |
| const trun = await this.tokenizer.readToken(trackRunBox); | |
| if (tfhd) { | |
| const track = this.tracks.get(tfhd.trackId); | |
| track?.fragments.push({ header: tfhd, trackRun: trun }); | |
| } | |
| break; | |
| } | |
| default: { | |
| debug(`Unexpected box: ${child.header.name}`); | |
| await this.tokenizer.ignore(payLoadLength); | |
| } | |
| } | |
| }, trafBox.getPayloadLength(0)); | |
| } | |
| /** | |
| * @param sampleDescription | |
| * Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html#//apple_ref/doc/uid/TP40000939-CH205-128916 | |
| */ | |
| parseSoundSampleDescription(sampleDescription) { | |
| const ssd = { | |
| dataFormat: sampleDescription.dataFormat, | |
| dataReferenceIndex: sampleDescription.dataReferenceIndex | |
| }; | |
| let offset = 0; | |
| if (sampleDescription.description) { | |
| const version = AtomToken.SoundSampleDescriptionVersion.get(sampleDescription.description, offset); | |
| offset += AtomToken.SoundSampleDescriptionVersion.len; | |
| if (version.version === 0 || version.version === 1) { | |
| // Sound Sample Description (Version 0) | |
| ssd.description = AtomToken.SoundSampleDescriptionV0.get(sampleDescription.description, offset); | |
| } | |
| else { | |
| debug(`Warning: sound-sample-description ${version} not implemented`); | |
| } | |
| } | |
| return ssd; | |
| } | |
| async parseChapterTrack(chapterTrack, track, len) { | |
| if (!chapterTrack.sampleSize) { | |
| if (chapterTrack.chunkOffsetTable.length !== chapterTrack.sampleSizeTable.length) | |
| throw new Error('Expected equal chunk-offset-table & sample-size-table length.'); | |
| } | |
| const chapters = []; | |
| for (let i = 0; i < chapterTrack.chunkOffsetTable.length && len > 0; ++i) { | |
| const start = chapterTrack.timeToSampleTable | |
| .slice(0, i) | |
| .reduce((acc, cur) => acc + cur.duration, 0); | |
| const chunkOffset = chapterTrack.chunkOffsetTable[i]; | |
| const nextChunkLen = chunkOffset - this.tokenizer.position; | |
| const sampleSize = chapterTrack.sampleSize > 0 ? chapterTrack.sampleSize : chapterTrack.sampleSizeTable[i]; | |
| len -= nextChunkLen + sampleSize; | |
| if (len < 0) | |
| throw new Mp4ContentError('Chapter chunk exceeding token length'); | |
| await this.tokenizer.ignore(nextChunkLen); | |
| const title = await this.tokenizer.readToken(new AtomToken.ChapterText(sampleSize)); | |
| debug(`Chapter ${i + 1}: ${title}`); | |
| const chapter = { | |
| title, | |
| timeScale: chapterTrack.media.header ? chapterTrack.media.header.timeScale : 0, | |
| start, | |
| sampleOffset: this.findSampleOffset(track, this.tokenizer.position) | |
| }; | |
| debug(`Chapter title=${chapter.title}, offset=${chapter.sampleOffset}/${track.header.duration}`); // ToDo, use media duration if required!!! | |
| chapters.push(chapter); | |
| } | |
| this.metadata.setFormat('chapters', chapters); | |
| await this.tokenizer.ignore(len); | |
| } | |
| findSampleOffset(track, chapterOffset) { | |
| let chunkIndex = 0; | |
| while (chunkIndex < track.chunkOffsetTable.length && track.chunkOffsetTable[chunkIndex] < chapterOffset) { | |
| ++chunkIndex; | |
| } | |
| return this.getChunkDuration(chunkIndex + 1, track); | |
| } | |
| getChunkDuration(chunkId, track) { | |
| let ttsi = 0; | |
| let ttsc = track.timeToSampleTable[ttsi].count; | |
| let ttsd = track.timeToSampleTable[ttsi].duration; | |
| let curChunkId = 1; | |
| let samplesPerChunk = this.getSamplesPerChunk(curChunkId, track.sampleToChunkTable); | |
| let totalDuration = 0; | |
| while (curChunkId < chunkId) { | |
| const nrOfSamples = Math.min(ttsc, samplesPerChunk); | |
| totalDuration += nrOfSamples * ttsd; | |
| ttsc -= nrOfSamples; | |
| samplesPerChunk -= nrOfSamples; | |
| if (samplesPerChunk === 0) { | |
| ++curChunkId; | |
| samplesPerChunk = this.getSamplesPerChunk(curChunkId, track.sampleToChunkTable); | |
| } | |
| else { | |
| ++ttsi; | |
| ttsc = track.timeToSampleTable[ttsi].count; | |
| ttsd = track.timeToSampleTable[ttsi].duration; | |
| } | |
| } | |
| return totalDuration; | |
| } | |
| getSamplesPerChunk(chunkId, stcTable) { | |
| for (let i = 0; i < stcTable.length - 1; ++i) { | |
| if (chunkId >= stcTable[i].firstChunk && chunkId < stcTable[i + 1].firstChunk) { | |
| return stcTable[i].samplesPerChunk; | |
| } | |
| } | |
| return stcTable[stcTable.length - 1].samplesPerChunk; | |
| } | |
| } | |
Xet Storage Details
- Size:
- 29.1 kB
- Xet hash:
- 33000db2509d5fd5b9d9975e6371455dd3b4dbbc5ecd09401e57043bb1f1ce03
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.