Skip to content

Instantly share code, notes, and snippets.

@NSExceptional
Created April 21, 2025 21:37
Show Gist options
  • Save NSExceptional/17e38f3b0818f5330bbc9ee444157768 to your computer and use it in GitHub Desktop.
Save NSExceptional/17e38f3b0818f5330bbc9ee444157768 to your computer and use it in GitHub Desktop.
A small class for parsing MP4 file headers, with the ability to check whether the given file has the hvc1 tag. All in-process.
/*
* mp4.ts
* media
*
* Created by Tanner Bennett on 2025-04-19
* Copyright © 2025 Tanner Bennett. All rights reserved.
*/
type Atom = {
size: number;
type: string;
offset: number;
};
type TopLevelMP4Atoms =
'ftyp' |
'moov' |
'mdat' |
'free' |
'skip' |
'udta' ;
type SeekOptions = {
reset?: boolean;
uptoOffset?: number;
};
export default class MP4 {
constructor(private readonly file: string) {
this.handle = Deno.openSync(this.file, { read: true });
}
private handle: Deno.FsFile
private get offset(): number {
return this.handle.seekSync(0, Deno.SeekMode.Current);
}
private reset(): void {
this.handle.seekSync(0, Deno.SeekMode.Start);
}
public close(): void {
this.handle.close();
}
private readBytes(size: number): Uint8Array {
const buffer = new Uint8Array(size);
const bytesRead = this.handle.readSync(buffer);
if (bytesRead === null) {
throw new Error('End of file reached');
}
return buffer;
}
private readAtomHeader(): Atom {
const offset = this.offset;
const header = this.readBytes(8);
const size = new DataView(header.buffer).getUint32(0);
const type = new TextDecoder().decode(header.subarray(4, 8));
return { size, type, offset };
}
/**
* Seeks up to the end of the specified atom header.
*
* If the atom has children or data, the reader will be positioned
* at the start of the next child or data, so that you can immediately
* read the next atom header or start reading the data.
*/
public seekAtom(type: string, options?: SeekOptions): Atom | null {
const { reset, uptoOffset } = options || {};
if (reset) {
this.reset();
}
const shouldLoop = (): boolean => {
if (uptoOffset) {
return this.offset < uptoOffset;
}
return true;
};
while (shouldLoop()) {
const { size, type: nextType } = this.readAtomHeader();
if (nextType === type) {
return { type, size, offset: this.offset - 8 };
}
// Seek past the current atom (-8 is for the header we already read)
this.handle.seekSync(size - 8, Deno.SeekMode.Current);
if (size === 0) {
break; // End of file
}
}
return null;
}
/** Traverse a branch of the atom tree. Assumes each atom immediately contains child atoms. */
public traverseAtoms(branch: string[]): Atom | null {
if (branch.length === 0) {
return null;
}
if (branch.length === 1) {
return this.seekAtom(branch[0]);
}
const first = branch.shift()!;
let currentAtom = this.seekAtom(first);
// Logging
if (!currentAtom) {
console.log(`Could not find first atom in branch:\n${branch.join(' > ')}`);
}
for (const type of branch) {
// No need to check for null at any point here, it's fine
// if we come across null right away and keep looping,
// `seekAtomWithinAtom` will no-op each time in that case
const childAtom = this.seekAtomWithinAtom(currentAtom, type);
currentAtom = childAtom;
// Logging
if (!currentAtom) {
console.log(`Could not find atom '${type}' in branch:\n${branch.join(' > ')}`);
break;
}
}
return currentAtom;
}
/** Scan all atoms at the current depth for the given type, up to the end of the parent atom */
public seekAtomWithinAtom(parent: Atom | null, type: string): Atom | null {
if (!parent) {
return null;
}
const { size, offset } = parent;
const endOffset = offset + size;
return this.seekAtom(type, { uptoOffset: endOffset });
}
/** This tag is stored as an atom inside moov > trak > mdia > minf > stbl > stsd */
public get hvc1(): boolean {
const stsd = this.traverseAtoms(['moov', 'trak', 'mdia', 'minf', 'stbl', 'stsd']);
if (!stsd) {
return false;
}
// Can't just put hvc1 at the end of the list above, because
// stsd is the first and only atom in this list that doesn't
// immediately contain other atoms, so we need to seek past
// some metadata before we can read the next atom
// Skip version/flags (4 bytes)
this.handle.seekSync(4, Deno.SeekMode.Current);
// Read the number of entries in the stsd atom
const stsdHeader = this.readBytes(4);
const numEntries = new DataView(stsdHeader.buffer).getUint32(0);
if (numEntries === 0) {
return false;
}
const hvc1 = this.seekAtomWithinAtom(stsd, 'hvc1');
return !!hvc1;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment