Instantly share code, notes, and snippets.
CreatedApril 21, 2025 21:37
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
| /* | |
| * mp4.ts | |
| * media | |
| * | |
| * Created by Tanner Bennett on 2025-04-19 | |
| * Copyright © 2025 Tanner Bennett. All rights reserved. | |
| */ | |
| typeAtom={ | |
| size:number; | |
| type:string; | |
| offset:number; | |
| }; | |
| typeTopLevelMP4Atoms= | |
| 'ftyp'| | |
| 'moov'| | |
| 'mdat'| | |
| 'free'| | |
| 'skip'| | |
| 'udta'; | |
| typeSeekOptions={ | |
| reset?:boolean; | |
| uptoOffset?:number; | |
| }; | |
| exportdefaultclassMP4{ | |
| constructor(privatereadonlyfile:string){ | |
| this.handle=Deno.openSync(this.file,{read:true}); | |
| } | |
| privatehandle:Deno.FsFile | |
| privategetoffset():number{ | |
| returnthis.handle.seekSync(0,Deno.SeekMode.Current); | |
| } | |
| privatereset():void{ | |
| this.handle.seekSync(0,Deno.SeekMode.Start); | |
| } | |
| publicclose():void{ | |
| this.handle.close(); | |
| } | |
| privatereadBytes(size:number):Uint8Array{ | |
| constbuffer=newUint8Array(size); | |
| constbytesRead=this.handle.readSync(buffer); | |
| if(bytesRead===null){ | |
| thrownewError('End of file reached'); | |
| } | |
| returnbuffer; | |
| } | |
| privatereadAtomHeader():Atom{ | |
| constoffset=this.offset; | |
| constheader=this.readBytes(8); | |
| constsize=newDataView(header.buffer).getUint32(0); | |
| consttype=newTextDecoder().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. | |
| */ | |
| publicseekAtom(type:string,options?:SeekOptions):Atom|null{ | |
| const{ reset, uptoOffset}=options||{}; | |
| if(reset){ | |
| this.reset(); | |
| } | |
| constshouldLoop=():boolean=>{ | |
| if(uptoOffset){ | |
| returnthis.offset<uptoOffset; | |
| } | |
| returntrue; | |
| }; | |
| 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 | |
| } | |
| } | |
| returnnull; | |
| } | |
| /** Traverse a branch of the atom tree. Assumes each atom immediately contains child atoms. */ | |
| publictraverseAtoms(branch:string[]):Atom|null{ | |
| if(branch.length===0){ | |
| returnnull; | |
| } | |
| if(branch.length===1){ | |
| returnthis.seekAtom(branch[0]); | |
| } | |
| constfirst=branch.shift()!; | |
| letcurrentAtom=this.seekAtom(first); | |
| // Logging | |
| if(!currentAtom){ | |
| console.log(`Could not find first atom in branch:\n${branch.join(' > ')}`); | |
| } | |
| for(consttypeofbranch){ | |
| // 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 | |
| constchildAtom=this.seekAtomWithinAtom(currentAtom,type); | |
| currentAtom=childAtom; | |
| // Logging | |
| if(!currentAtom){ | |
| console.log(`Could not find atom '${type}' in branch:\n${branch.join(' > ')}`); | |
| break; | |
| } | |
| } | |
| returncurrentAtom; | |
| } | |
| /** Scan all atoms at the current depth for the given type, up to the end of the parent atom */ | |
| publicseekAtomWithinAtom(parent:Atom|null,type:string):Atom|null{ | |
| if(!parent){ | |
| returnnull; | |
| } | |
| const{ size, offset}=parent; | |
| constendOffset=offset+size; | |
| returnthis.seekAtom(type,{uptoOffset:endOffset}); | |
| } | |
| /** This tag is stored as an atom inside moov > trak > mdia > minf > stbl > stsd */ | |
| publicgethvc1():boolean{ | |
| conststsd=this.traverseAtoms(['moov','trak','mdia','minf','stbl','stsd']); | |
| if(!stsd){ | |
| returnfalse; | |
| } | |
| // 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 | |
| conststsdHeader=this.readBytes(4); | |
| constnumEntries=newDataView(stsdHeader.buffer).getUint32(0); | |
| if(numEntries===0){ | |
| returnfalse; | |
| } | |
| consthvc1=this.seekAtomWithinAtom(stsd,'hvc1'); | |
| return!!hvc1; | |
| } | |
| } |
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment