// add a getString to make reading box headers easier DataView.prototype.getString = function(offset, length) { text = ''; let end = offset + length; for (let i = offset; i < end; i++) { text += String.fromCharCode(this.getUint8(i)); } return text; } // mp4 tree traverser, as is only good for getting avc1 + mp4a codec info. // relevant tree parts: // root // |-moov // |-trak // |-mdia // |-minf // |-vmhd // |-stbl // |-stsd // |-avc1 // |-avcC // has video codec profile // |-trak // |-mdia // |-minf // |-smhd // |-stbl // |-stsd // |-mp4a // |-esds // has audio codec profile // // headers are 4byte len + 4byte name string // most details taken from https://web.archive.org/web/20180219054429/http://l.web.umkc.edu/lizhu/teaching/2016sp.video-communication/ref/mp4.pdf // avc1 and mp4a specific header offsets from https://xhelmboyx.tripod.com/formats/mp4-layout.txt class MP4Tree { constructor(data) { this.data = new DataView(data); this.codecs = []; this.idx = 0; // current byte index in data this.parse_codecs(); } parse_codecs() { this.parse_into('moov', 8); // loop over all traks (can handle pure audio, pure video, both, multiple) // only doing h264 video since thats all I can check currently doing // transcoding and running obs together with cpu usage while (this.idx < this.data.byteLength) { if (!this.parse_until('trak')) { // if no more tracks in file break; } const curr_track_end = this.idx + this.read_next_head().len; this.idx += 8 this.parse_into('mdia', 8); this.parse_into('minf', 8); const track_type_head = this.read_next_head().name; if (track_type_head == 'smhd' || track_type_head == 'vmhd') { this.parse_into('stbl', 8); this.parse_into('stsd', 16); const media_sample = this.read_next_head().name; if (media_sample == 'avc1') { this.parse_avc1(); } else if (media_sample == 'vp09') { this.parse_vp09(); } else if (media_sample == 'mp4a') { this.parse_mp4a(); } else if (media_sample == 'Opus') { this.codecs.push('opus'); } else { this.codecs.push('unsup codec'); } } this.idx = curr_track_end; // always skip to end of current processed track } } parse_avc1() { this.parse_into('avc1', 86); // avc1 has 78 bytes of frame metadata to skip this.parse_into('avcC', 8); // ex, 01 64 00 1f // 01 is avc version (always 1, bit-and it off) // rest is video codec profile, rep as hex const avc_profile = (this.data.getUint32(this.idx) & 0xffffff).toString(16); this.codecs.push('avc1.' + avc_profile); } parse_vp09() { this.parse_into('vp09', 86); this.parse_into('vpcC', 12); const profile = this.data.getUint8(this.idx).toString().padStart(2, "0"); const level = this.data.getUint8(this.idx + 1).toString(); const depth = (this.data.getUint8(this.idx + 2) & 0xf0).toString().padStart(2, "0"); this.codecs.push(["vp09", profile, level, depth].join('.')); } parse_mp4a() { this.parse_into('mp4a', 36); // 28 bytes of subboxes with unclear offsets this.parse_into('esds', 12); // mp4a is as mp4a.40.2 // oti has defined position, rep as hex // aot as a subtype is defined as 5 bits of the fetched byte, rep as dec const oti = this.data.getUint8(this.idx + 13).toString(16); const aot = (this.data.getUint8(this.idx + 31) >> 3).toString(10); this.codecs.push(["mp4a", oti, aot].join('.')); } get_mime() { return 'video/mp4; codecs="' + this.get_codec_string() + '"'; } get_codec_string() { return this.codecs.join(', '); } // find the given box, also descend into its child box depth // offset is almost always 8, sometimes 12 or 16, defined in spec not file parse_into(name, header_offset) { this.parse_until(name); this.idx += header_offset; } // find the box with the given name at the current box depth // acts as though the current box extends to EOF. parse_until(name) { let curr_head = null; while (this.idx + 8 < this.data.byteLength) { curr_head = this.read_next_head(); if (curr_head.name == name) { return true; } else { this.idx += curr_head.len; } } return false; } // read the mp4 box header as if the idx is positioned there read_next_head() { return {"len": this.data.getUint32(this.idx), "name": this.data.getString(this.idx + 4, 4)}; } }