Buckets:
dieumercimvemba/mediastore / scripts /whatsapp-bridge /node_modules /music-metadata /lib /mp4 /AtomToken.js
| import * as Token from 'token-types'; | |
| import initDebug from 'debug'; | |
| import { FourCcToken } from '../common/FourCC.js'; | |
| import { makeUnexpectedFileContentError } from '../ParseError.js'; | |
| import * as util from '../common/Util.js'; | |
| import { FieldDecodingError } from '../ParseError.js'; | |
| const debug = initDebug('music-metadata:parser:MP4:atom'); | |
| export class Mp4ContentError extends makeUnexpectedFileContentError('MP4') { | |
| } | |
| export const Header = { | |
| len: 8, | |
| get: (buf, off) => { | |
| const length = Token.UINT32_BE.get(buf, off); | |
| if (length < 0) | |
| throw new Mp4ContentError('Invalid atom header length'); | |
| return { | |
| length: BigInt(length), | |
| name: new Token.StringType(4, 'latin1').get(buf, off + 4) | |
| }; | |
| }, | |
| put: (buf, off, hdr) => { | |
| Token.UINT32_BE.put(buf, off, Number(hdr.length)); | |
| return FourCcToken.put(buf, off + 4, hdr.name); | |
| } | |
| }; | |
| /** | |
| * Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap1/qtff1.html#//apple_ref/doc/uid/TP40000939-CH203-38190 | |
| */ | |
| export const ExtendedSize = Token.UINT64_BE; | |
| export const ftyp = { | |
| len: 4, | |
| get: (buf, off) => { | |
| return { | |
| type: new Token.StringType(4, 'ascii').get(buf, off) | |
| }; | |
| } | |
| }; | |
| /** | |
| * Token: Movie Header Atom | |
| */ | |
| export const mhdr = { | |
| len: 8, | |
| get: (buf, off) => { | |
| return { | |
| version: Token.UINT8.get(buf, off), | |
| flags: Token.UINT24_BE.get(buf, off + 1), | |
| nextItemID: Token.UINT32_BE.get(buf, off + 4) | |
| }; | |
| } | |
| }; | |
| /** | |
| * Base class for 'fixed' length atoms. | |
| * In some cases these atoms are longer then the sum of the described fields. | |
| * Issue: https://github.com/Borewit/music-metadata/issues/120 | |
| */ | |
| export class FixedLengthAtom { | |
| /** | |
| * | |
| * @param {number} len Length as specified in the size field | |
| * @param {number} expLen Total length of sum of specified fields in the standard | |
| * @param atomId Atom ID | |
| */ | |
| constructor(len, expLen, atomId) { | |
| if (len < expLen) { | |
| throw new Mp4ContentError(`Atom ${atomId} expected to be ${expLen}, but specifies ${len} bytes long.`); | |
| } | |
| if (len > expLen) { | |
| debug(`Warning: atom ${atomId} expected to be ${expLen}, but was actually ${len} bytes long.`); | |
| } | |
| this.len = len; | |
| } | |
| } | |
| /** | |
| * Timestamp stored in seconds since Mac Epoch (1 January 1904) | |
| */ | |
| const SecondsSinceMacEpoch = { | |
| len: 4, | |
| get: (buf, off) => { | |
| const secondsSinceUnixEpoch = Token.UINT32_BE.get(buf, off) - 2082844800; | |
| return new Date(secondsSinceUnixEpoch * 1000); | |
| } | |
| }; | |
| const SecondsSinceMacEpoch64 = { | |
| len: 8, | |
| get: (buf, off) => { | |
| const secondsSinceUnixEpoch = Number(Token.UINT64_BE.get(buf, off)) - 2082844800; | |
| return new Date(secondsSinceUnixEpoch * 1000); | |
| } | |
| }; | |
| /** | |
| * Token: Media Header Atom | |
| * Ref: | |
| * - https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-SW34 | |
| * - https://wiki.multimedia.cx/index.php/QuickTime_container#mdhd | |
| */ | |
| export class MdhdAtom extends FixedLengthAtom { | |
| constructor(len) { | |
| super(len, 24, 'mdhd'); | |
| } | |
| get(buf, off) { | |
| const version = Token.UINT8.get(buf, off + 0); | |
| const flags = Token.UINT24_BE.get(buf, off + 1); | |
| switch (version) { | |
| case 0: | |
| // Version 0: 32-bit fields | |
| return { | |
| version, | |
| flags, | |
| creationTime: SecondsSinceMacEpoch.get(buf, off + 4), | |
| modificationTime: SecondsSinceMacEpoch.get(buf, off + 8), | |
| timeScale: Token.UINT32_BE.get(buf, off + 12), | |
| duration: Token.UINT32_BE.get(buf, off + 16), | |
| language: Token.UINT16_BE.get(buf, off + 20), | |
| quality: Token.UINT16_BE.get(buf, off + 22) | |
| }; | |
| case 1: | |
| return { | |
| version, | |
| flags, | |
| creationTime: SecondsSinceMacEpoch64.get(buf, off + 4), | |
| modificationTime: SecondsSinceMacEpoch64.get(buf, off + 12), | |
| timeScale: Token.UINT32_BE.get(buf, off + 20), | |
| duration: Number(Token.UINT64_BE.get(buf, off + 24)), | |
| language: Token.UINT16_BE.get(buf, off + 32), | |
| quality: Token.UINT16_BE.get(buf, off + 34) | |
| }; | |
| default: | |
| throw new FieldDecodingError('Invalid mdhd version header'); | |
| } | |
| } | |
| } | |
| /** | |
| * Token: Movie Header Atom | |
| */ | |
| export class MvhdAtom extends FixedLengthAtom { | |
| constructor(len) { | |
| super(len, 100, 'mvhd'); | |
| } | |
| get(buf, off) { | |
| const version = Token.UINT8.get(buf, off); | |
| const flags = Token.UINT24_BE.get(buf, off + 1); | |
| if (version === 1) { | |
| // Version 1: 64-bit creation/modification times and duration | |
| return { | |
| version, | |
| flags, | |
| creationTime: SecondsSinceMacEpoch64.get(buf, off + 4), | |
| modificationTime: SecondsSinceMacEpoch64.get(buf, off + 12), | |
| timeScale: Token.UINT32_BE.get(buf, off + 20), | |
| duration: Number(Token.UINT64_BE.get(buf, off + 24)), | |
| preferredRate: Token.UINT32_BE.get(buf, off + 32), | |
| preferredVolume: Token.UINT16_BE.get(buf, off + 36), | |
| // ignore reserved: 10 bytes | |
| // ignore matrix structure: 36 bytes | |
| previewTime: Token.UINT32_BE.get(buf, off + 84), | |
| previewDuration: Token.UINT32_BE.get(buf, off + 88), | |
| posterTime: Token.UINT32_BE.get(buf, off + 92), | |
| selectionTime: Token.UINT32_BE.get(buf, off + 96), | |
| selectionDuration: Token.UINT32_BE.get(buf, off + 100), | |
| currentTime: Token.UINT32_BE.get(buf, off + 104), | |
| nextTrackID: Token.UINT32_BE.get(buf, off + 108) | |
| }; | |
| } | |
| // Version 0: 32-bit fields | |
| return { | |
| version, | |
| flags, | |
| creationTime: SecondsSinceMacEpoch.get(buf, off + 4), | |
| modificationTime: SecondsSinceMacEpoch.get(buf, off + 8), | |
| timeScale: Token.UINT32_BE.get(buf, off + 12), | |
| duration: Token.UINT32_BE.get(buf, off + 16), | |
| preferredRate: Token.UINT32_BE.get(buf, off + 20), | |
| preferredVolume: Token.UINT16_BE.get(buf, off + 24), | |
| // ignore reserved: 10 bytes | |
| // ignore matrix structure: 36 bytes | |
| previewTime: Token.UINT32_BE.get(buf, off + 72), | |
| previewDuration: Token.UINT32_BE.get(buf, off + 76), | |
| posterTime: Token.UINT32_BE.get(buf, off + 80), | |
| selectionTime: Token.UINT32_BE.get(buf, off + 84), | |
| selectionDuration: Token.UINT32_BE.get(buf, off + 88), | |
| currentTime: Token.UINT32_BE.get(buf, off + 92), | |
| nextTrackID: Token.UINT32_BE.get(buf, off + 96) | |
| }; | |
| } | |
| } | |
| /** | |
| * Data Atom Structure | |
| */ | |
| export class DataAtom { | |
| constructor(len) { | |
| this.len = len; | |
| } | |
| get(buf, off) { | |
| return { | |
| type: { | |
| set: Token.UINT8.get(buf, off + 0), | |
| type: Token.UINT24_BE.get(buf, off + 1) | |
| }, | |
| locale: Token.UINT24_BE.get(buf, off + 4), | |
| value: new Token.Uint8ArrayType(this.len - 8).get(buf, off + 8) | |
| }; | |
| } | |
| } | |
| /** | |
| * Data Atom Structure | |
| * Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW31 | |
| */ | |
| export class NameAtom { | |
| constructor(len) { | |
| this.len = len; | |
| } | |
| get(buf, off) { | |
| return { | |
| version: Token.UINT8.get(buf, off), | |
| flags: Token.UINT24_BE.get(buf, off + 1), | |
| name: new Token.StringType(this.len - 4, 'utf-8').get(buf, off + 4) | |
| }; | |
| } | |
| } | |
| /** | |
| * Track Header Atoms structure (`tkhd`) | |
| * Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25550 | |
| */ | |
| export class TrackHeaderAtom { | |
| constructor(len) { | |
| this.len = len; | |
| } | |
| get(buf, off) { | |
| return { | |
| version: Token.UINT8.get(buf, off), | |
| flags: Token.UINT24_BE.get(buf, off + 1), | |
| creationTime: SecondsSinceMacEpoch.get(buf, off + 4), | |
| modificationTime: SecondsSinceMacEpoch.get(buf, off + 8), | |
| trackId: Token.UINT32_BE.get(buf, off + 12), | |
| // reserved 4 bytes | |
| duration: Token.UINT32_BE.get(buf, off + 20), | |
| layer: Token.UINT16_BE.get(buf, off + 24), | |
| alternateGroup: Token.UINT16_BE.get(buf, off + 26), | |
| volume: Token.UINT16_BE.get(buf, off + 28) // ToDo: fixed point | |
| // ToDo: add remaining fields | |
| }; | |
| } | |
| } | |
| /** | |
| * Atom: Sample Description Atom ('stsd') | |
| * Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25691 | |
| */ | |
| const stsdHeader = { | |
| len: 8, | |
| get: (buf, off) => { | |
| return { | |
| version: Token.UINT8.get(buf, off), | |
| flags: Token.UINT24_BE.get(buf, off + 1), | |
| numberOfEntries: Token.UINT32_BE.get(buf, off + 4) | |
| }; | |
| } | |
| }; | |
| /** | |
| * Atom: Sample Description Atom ('stsd') | |
| * Ref: https://developer.apple.com/documentation/quicktime-file-format/sample_description_atom | |
| */ | |
| class SampleDescriptionTable { | |
| constructor(len) { | |
| this.len = len; | |
| } | |
| get(buf, off) { | |
| const descrLen = this.len - 12; | |
| return { | |
| dataFormat: FourCcToken.get(buf, off), | |
| dataReferenceIndex: Token.UINT16_BE.get(buf, off + 10), | |
| description: descrLen > 0 ? new Token.Uint8ArrayType(descrLen).get(buf, off + 12) : undefined | |
| }; | |
| } | |
| } | |
| /** | |
| * Atom: Sample-description Atom ('stsd') | |
| * Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25691 | |
| */ | |
| export class StsdAtom { | |
| constructor(len) { | |
| this.len = len; | |
| } | |
| get(buf, off) { | |
| const header = stsdHeader.get(buf, off); | |
| off += stsdHeader.len; | |
| const table = []; | |
| for (let n = 0; n < header.numberOfEntries; ++n) { | |
| const size = Token.UINT32_BE.get(buf, off); // Sample description size | |
| off += Token.UINT32_BE.len; | |
| table.push(new SampleDescriptionTable(size - Token.UINT32_BE.len).get(buf, off)); | |
| off += size; | |
| } | |
| return { | |
| header, | |
| table | |
| }; | |
| } | |
| } | |
| /** | |
| * Common Sound Sample Description (version & revision) | |
| * Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html#//apple_ref/doc/uid/TP40000939-CH205-57317 | |
| */ | |
| export const SoundSampleDescriptionVersion = { | |
| len: 8, | |
| get(buf, off) { | |
| return { | |
| version: Token.INT16_BE.get(buf, off), | |
| revision: Token.INT16_BE.get(buf, off + 2), | |
| vendor: Token.INT32_BE.get(buf, off + 4) | |
| }; | |
| } | |
| }; | |
| /** | |
| * Sound Sample Description (Version 0) | |
| * Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html#//apple_ref/doc/uid/TP40000939-CH205-130736 | |
| */ | |
| export const SoundSampleDescriptionV0 = { | |
| len: 12, | |
| get(buf, off) { | |
| return { | |
| numAudioChannels: Token.INT16_BE.get(buf, off + 0), | |
| sampleSize: Token.INT16_BE.get(buf, off + 2), | |
| compressionId: Token.INT16_BE.get(buf, off + 4), | |
| packetSize: Token.INT16_BE.get(buf, off + 6), | |
| sampleRate: Token.UINT16_BE.get(buf, off + 8) + Token.UINT16_BE.get(buf, off + 10) / 10000 | |
| }; | |
| } | |
| }; | |
| class SimpleTableAtom { | |
| constructor(len, token) { | |
| this.len = len; | |
| this.token = token; | |
| } | |
| get(buf, off) { | |
| const nrOfEntries = Token.INT32_BE.get(buf, off + 4); | |
| return { | |
| version: Token.INT8.get(buf, off + 0), | |
| flags: Token.INT24_BE.get(buf, off + 1), | |
| numberOfEntries: nrOfEntries, | |
| entries: readTokenTable(buf, this.token, off + 8, this.len - 8, nrOfEntries) | |
| }; | |
| } | |
| } | |
| export const TimeToSampleToken = { | |
| len: 8, | |
| get(buf, off) { | |
| return { | |
| count: Token.INT32_BE.get(buf, off + 0), | |
| duration: Token.INT32_BE.get(buf, off + 4) | |
| }; | |
| } | |
| }; | |
| /** | |
| * Time-to-sample('stts') atom. | |
| * Store duration information for a media’s samples. | |
| * Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25696 | |
| */ | |
| export class SttsAtom extends SimpleTableAtom { | |
| constructor(len) { | |
| super(len, TimeToSampleToken); | |
| } | |
| } | |
| export const SampleToChunkToken = { | |
| len: 12, | |
| get(buf, off) { | |
| return { | |
| firstChunk: Token.INT32_BE.get(buf, off), | |
| samplesPerChunk: Token.INT32_BE.get(buf, off + 4), | |
| sampleDescriptionId: Token.INT32_BE.get(buf, off + 8) | |
| }; | |
| } | |
| }; | |
| /** | |
| * Sample-to-Chunk ('stsc') atom interface | |
| * Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25706 | |
| */ | |
| export class StscAtom extends SimpleTableAtom { | |
| constructor(len) { | |
| super(len, SampleToChunkToken); | |
| } | |
| } | |
| /** | |
| * Sample-size ('stsz') atom | |
| * Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25710 | |
| */ | |
| export class StszAtom { | |
| constructor(len) { | |
| this.len = len; | |
| } | |
| get(buf, off) { | |
| const nrOfEntries = Token.INT32_BE.get(buf, off + 8); | |
| return { | |
| version: Token.INT8.get(buf, off), | |
| flags: Token.INT24_BE.get(buf, off + 1), | |
| sampleSize: Token.INT32_BE.get(buf, off + 4), | |
| numberOfEntries: nrOfEntries, | |
| entries: readTokenTable(buf, Token.INT32_BE, off + 12, this.len - 12, nrOfEntries) | |
| }; | |
| } | |
| } | |
| /** | |
| * Chunk offset atom, 'stco' | |
| * Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25715 | |
| */ | |
| export class StcoAtom extends SimpleTableAtom { | |
| constructor(len) { | |
| super(len, Token.INT32_BE); | |
| this.len = len; | |
| } | |
| } | |
| /** | |
| * Token used to decode text-track from 'mdat' atom (raw data stream) | |
| */ | |
| export class ChapterText { | |
| constructor(len) { | |
| this.len = len; | |
| } | |
| get(buf, off) { | |
| const titleLen = Token.INT16_BE.get(buf, off + 0); | |
| const str = new Token.StringType(titleLen, 'utf-8'); | |
| return str.get(buf, off + 2); | |
| } | |
| } | |
| function readTokenTable(buf, token, off, remainingLen, numberOfEntries) { | |
| debug(`remainingLen=${remainingLen}, numberOfEntries=${numberOfEntries} * token-len=${token.len}`); | |
| if (remainingLen === 0) | |
| return []; | |
| if (remainingLen !== numberOfEntries * token.len) | |
| throw new Mp4ContentError('mismatch number-of-entries with remaining atom-length'); | |
| const entries = []; | |
| // parse offset-table | |
| for (let n = 0; n < numberOfEntries; ++n) { | |
| entries.push(token.get(buf, off)); | |
| off += token.len; | |
| } | |
| return entries; | |
| } | |
| /** | |
| * Sample-size ('tfhd') TrackFragmentHeaderBox | |
| */ | |
| export class TrackFragmentHeaderBox { | |
| constructor(len) { | |
| this.len = len; | |
| } | |
| get(buf, off) { | |
| const flagOffset = off + 1; | |
| const header = { | |
| version: Token.INT8.get(buf, off), | |
| flags: { | |
| baseDataOffsetPresent: util.getBit(buf, flagOffset + 2, 0), | |
| sampleDescriptionIndexPresent: util.getBit(buf, flagOffset + 2, 1), | |
| defaultSampleDurationPresent: util.getBit(buf, flagOffset + 2, 3), | |
| defaultSampleSizePresent: util.getBit(buf, flagOffset + 2, 4), | |
| defaultSampleFlagsPresent: util.getBit(buf, flagOffset + 2, 5), | |
| defaultDurationIsEmpty: util.getBit(buf, flagOffset, 0), | |
| defaultBaseIsMoof: util.getBit(buf, flagOffset, 1) | |
| }, | |
| trackId: Token.UINT32_BE.get(buf, 4) | |
| }; | |
| let dynOffset = 8; | |
| if (header.flags.baseDataOffsetPresent) { | |
| header.baseDataOffset = Token.UINT64_BE.get(buf, dynOffset); | |
| dynOffset += 8; | |
| } | |
| if (header.flags.sampleDescriptionIndexPresent) { | |
| header.sampleDescriptionIndex = Token.UINT32_BE.get(buf, dynOffset); | |
| dynOffset += 4; | |
| } | |
| if (header.flags.defaultSampleDurationPresent) { | |
| header.defaultSampleDuration = Token.UINT32_BE.get(buf, dynOffset); | |
| dynOffset += 4; | |
| } | |
| if (header.flags.defaultSampleSizePresent) { | |
| header.defaultSampleSize = Token.UINT32_BE.get(buf, dynOffset); | |
| dynOffset += 4; | |
| } | |
| if (header.flags.defaultSampleFlagsPresent) { | |
| header.defaultSampleFlags = Token.UINT32_BE.get(buf, dynOffset); | |
| } | |
| return header; | |
| } | |
| } | |
| /** | |
| * Sample-size ('trun') TrackRunBox | |
| */ | |
| export class TrackRunBox { | |
| constructor(len) { | |
| this.len = len; | |
| } | |
| get(buf, off) { | |
| const flagOffset = off + 1; | |
| const trun = { | |
| version: Token.INT8.get(buf, off), | |
| flags: { | |
| dataOffsetPresent: util.getBit(buf, flagOffset + 2, 0), | |
| firstSampleFlagsPresent: util.getBit(buf, flagOffset + 2, 2), | |
| sampleDurationPresent: util.getBit(buf, flagOffset + 1, 0), | |
| sampleSizePresent: util.getBit(buf, flagOffset + 1, 1), | |
| sampleFlagsPresent: util.getBit(buf, flagOffset + 1, 2), | |
| sampleCompositionTimeOffsetsPresent: util.getBit(buf, flagOffset + 1, 3) | |
| }, | |
| sampleCount: Token.UINT32_BE.get(buf, off + 4), | |
| samples: [] | |
| }; | |
| let dynOffset = off + 8; | |
| if (trun.flags.dataOffsetPresent) { | |
| trun.dataOffset = Token.UINT32_BE.get(buf, dynOffset); | |
| dynOffset += 4; | |
| } | |
| if (trun.flags.firstSampleFlagsPresent) { | |
| trun.firstSampleFlags = Token.UINT32_BE.get(buf, dynOffset); | |
| dynOffset += 4; | |
| } | |
| for (let n = 0; n < trun.sampleCount; ++n) { | |
| if (dynOffset >= this.len) { | |
| debug("TrackRunBox size mismatch"); | |
| break; | |
| } | |
| const sample = {}; | |
| if (trun.flags.sampleDurationPresent) { | |
| sample.sampleDuration = Token.UINT32_BE.get(buf, dynOffset); | |
| dynOffset += 4; | |
| } | |
| if (trun.flags.sampleSizePresent) { | |
| sample.sampleSize = Token.UINT32_BE.get(buf, dynOffset); | |
| dynOffset += 4; | |
| } | |
| if (trun.flags.sampleFlagsPresent) { | |
| sample.sampleFlags = Token.UINT32_BE.get(buf, dynOffset); | |
| dynOffset += 4; | |
| } | |
| if (trun.flags.sampleCompositionTimeOffsetsPresent) { | |
| sample.sampleCompositionTimeOffset = Token.UINT32_BE.get(buf, dynOffset); | |
| dynOffset += 4; | |
| } | |
| trun.samples.push(sample); | |
| } | |
| return trun; | |
| } | |
| } | |
| /** | |
| * HandlerBox (`hdlr`) | |
| */ | |
| export class HandlerBox { | |
| constructor(len) { | |
| this.len = len; | |
| } | |
| get(buf, off) { | |
| const _flagOffset = off + 1; | |
| const charTypeToken = new Token.StringType(4, 'utf-8'); | |
| return { | |
| version: Token.INT8.get(buf, off), | |
| flags: Token.UINT24_BE.get(buf, off + 1), | |
| componentType: charTypeToken.get(buf, off + 4), | |
| handlerType: charTypeToken.get(buf, off + 8), | |
| componentName: new Token.StringType(this.len - 28, 'utf-8').get(buf, off + 28), | |
| }; | |
| } | |
| } | |
| /** | |
| * Chapter Track Reference Box (`chap`) | |
| */ | |
| export class ChapterTrackReferenceBox { | |
| constructor(len) { | |
| this.len = len; | |
| } | |
| get(buf, off) { | |
| let dynOffset = 0; | |
| const trackIds = []; | |
| while (dynOffset < this.len) { | |
| trackIds.push(Token.UINT32_BE.get(buf, off + dynOffset)); | |
| dynOffset += 4; | |
| } | |
| return trackIds; | |
| } | |
| } | |
Xet Storage Details
- Size:
- 20.4 kB
- Xet hash:
- 2defe49016c71528202f99724ce64b346973e8f060fb0e317f2275a24f538057
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.