mp4-helpers.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.parseMediaInfo = exports.parseTracks = exports.addSampleDescription = exports.buildFrameTable = exports.findNamedBox = exports.findBox = exports.parseDescriptors = void 0;
  6. var _byteHelpers = require("./byte-helpers.js");
  7. var _codecHelpers = require("./codec-helpers.js");
  8. var _opusHelpers = require("./opus-helpers.js");
  9. var normalizePath = function normalizePath(path) {
  10. if (typeof path === 'string') {
  11. return (0, _byteHelpers.stringToBytes)(path);
  12. }
  13. if (typeof path === 'number') {
  14. return path;
  15. }
  16. return path;
  17. };
  18. var normalizePaths = function normalizePaths(paths) {
  19. if (!Array.isArray(paths)) {
  20. return [normalizePath(paths)];
  21. }
  22. return paths.map(function (p) {
  23. return normalizePath(p);
  24. });
  25. };
  26. var DESCRIPTORS;
  27. var parseDescriptors = function parseDescriptors(bytes) {
  28. bytes = (0, _byteHelpers.toUint8)(bytes);
  29. var results = [];
  30. var i = 0;
  31. while (bytes.length > i) {
  32. var tag = bytes[i];
  33. var size = 0;
  34. var headerSize = 0; // tag
  35. headerSize++;
  36. var byte = bytes[headerSize]; // first byte
  37. headerSize++;
  38. while (byte & 0x80) {
  39. size = (byte & 0x7F) << 7;
  40. byte = bytes[headerSize];
  41. headerSize++;
  42. }
  43. size += byte & 0x7F;
  44. for (var z = 0; z < DESCRIPTORS.length; z++) {
  45. var _DESCRIPTORS$z = DESCRIPTORS[z],
  46. id = _DESCRIPTORS$z.id,
  47. parser = _DESCRIPTORS$z.parser;
  48. if (tag === id) {
  49. results.push(parser(bytes.subarray(headerSize, headerSize + size)));
  50. break;
  51. }
  52. }
  53. i += size + headerSize;
  54. }
  55. return results;
  56. };
  57. exports.parseDescriptors = parseDescriptors;
  58. DESCRIPTORS = [{
  59. id: 0x03,
  60. parser: function parser(bytes) {
  61. var desc = {
  62. tag: 0x03,
  63. id: bytes[0] << 8 | bytes[1],
  64. flags: bytes[2],
  65. size: 3,
  66. dependsOnEsId: 0,
  67. ocrEsId: 0,
  68. descriptors: [],
  69. url: ''
  70. }; // depends on es id
  71. if (desc.flags & 0x80) {
  72. desc.dependsOnEsId = bytes[desc.size] << 8 | bytes[desc.size + 1];
  73. desc.size += 2;
  74. } // url
  75. if (desc.flags & 0x40) {
  76. var len = bytes[desc.size];
  77. desc.url = (0, _byteHelpers.bytesToString)(bytes.subarray(desc.size + 1, desc.size + 1 + len));
  78. desc.size += len;
  79. } // ocr es id
  80. if (desc.flags & 0x20) {
  81. desc.ocrEsId = bytes[desc.size] << 8 | bytes[desc.size + 1];
  82. desc.size += 2;
  83. }
  84. desc.descriptors = parseDescriptors(bytes.subarray(desc.size)) || [];
  85. return desc;
  86. }
  87. }, {
  88. id: 0x04,
  89. parser: function parser(bytes) {
  90. // DecoderConfigDescriptor
  91. var desc = {
  92. tag: 0x04,
  93. oti: bytes[0],
  94. streamType: bytes[1],
  95. bufferSize: bytes[2] << 16 | bytes[3] << 8 | bytes[4],
  96. maxBitrate: bytes[5] << 24 | bytes[6] << 16 | bytes[7] << 8 | bytes[8],
  97. avgBitrate: bytes[9] << 24 | bytes[10] << 16 | bytes[11] << 8 | bytes[12],
  98. descriptors: parseDescriptors(bytes.subarray(13))
  99. };
  100. return desc;
  101. }
  102. }, {
  103. id: 0x05,
  104. parser: function parser(bytes) {
  105. // DecoderSpecificInfo
  106. return {
  107. tag: 0x05,
  108. bytes: bytes
  109. };
  110. }
  111. }, {
  112. id: 0x06,
  113. parser: function parser(bytes) {
  114. // SLConfigDescriptor
  115. return {
  116. tag: 0x06,
  117. bytes: bytes
  118. };
  119. }
  120. }];
  121. /**
  122. * find any number of boxes by name given a path to it in an iso bmff
  123. * such as mp4.
  124. *
  125. * @param {TypedArray} bytes
  126. * bytes for the iso bmff to search for boxes in
  127. *
  128. * @param {Uint8Array[]|string[]|string|Uint8Array} name
  129. * An array of paths or a single path representing the name
  130. * of boxes to search through in bytes. Paths may be
  131. * uint8 (character codes) or strings.
  132. *
  133. * @param {boolean} [complete=false]
  134. * Should we search only for complete boxes on the final path.
  135. * This is very useful when you do not want to get back partial boxes
  136. * in the case of streaming files.
  137. *
  138. * @return {Uint8Array[]}
  139. * An array of the end paths that we found.
  140. */
  141. var findBox = function findBox(bytes, paths, complete) {
  142. if (complete === void 0) {
  143. complete = false;
  144. }
  145. paths = normalizePaths(paths);
  146. bytes = (0, _byteHelpers.toUint8)(bytes);
  147. var results = [];
  148. if (!paths.length) {
  149. // short-circuit the search for empty paths
  150. return results;
  151. }
  152. var i = 0;
  153. while (i < bytes.length) {
  154. var size = (bytes[i] << 24 | bytes[i + 1] << 16 | bytes[i + 2] << 8 | bytes[i + 3]) >>> 0;
  155. var type = bytes.subarray(i + 4, i + 8); // invalid box format.
  156. if (size === 0) {
  157. break;
  158. }
  159. var end = i + size;
  160. if (end > bytes.length) {
  161. // this box is bigger than the number of bytes we have
  162. // and complete is set, we cannot find any more boxes.
  163. if (complete) {
  164. break;
  165. }
  166. end = bytes.length;
  167. }
  168. var data = bytes.subarray(i + 8, end);
  169. if ((0, _byteHelpers.bytesMatch)(type, paths[0])) {
  170. if (paths.length === 1) {
  171. // this is the end of the path and we've found the box we were
  172. // looking for
  173. results.push(data);
  174. } else {
  175. // recursively search for the next box along the path
  176. results.push.apply(results, findBox(data, paths.slice(1), complete));
  177. }
  178. }
  179. i = end;
  180. } // we've finished searching all of bytes
  181. return results;
  182. };
  183. /**
  184. * Search for a single matching box by name in an iso bmff format like
  185. * mp4. This function is useful for finding codec boxes which
  186. * can be placed arbitrarily in sample descriptions depending
  187. * on the version of the file or file type.
  188. *
  189. * @param {TypedArray} bytes
  190. * bytes for the iso bmff to search for boxes in
  191. *
  192. * @param {string|Uint8Array} name
  193. * The name of the box to find.
  194. *
  195. * @return {Uint8Array[]}
  196. * a subarray of bytes representing the name boxed we found.
  197. */
  198. exports.findBox = findBox;
  199. var findNamedBox = function findNamedBox(bytes, name) {
  200. name = normalizePath(name);
  201. if (!name.length) {
  202. // short-circuit the search for empty paths
  203. return bytes.subarray(bytes.length);
  204. }
  205. var i = 0;
  206. while (i < bytes.length) {
  207. if ((0, _byteHelpers.bytesMatch)(bytes.subarray(i, i + name.length), name)) {
  208. var size = (bytes[i - 4] << 24 | bytes[i - 3] << 16 | bytes[i - 2] << 8 | bytes[i - 1]) >>> 0;
  209. var end = size > 1 ? i + size : bytes.byteLength;
  210. return bytes.subarray(i + 4, end);
  211. }
  212. i++;
  213. } // we've finished searching all of bytes
  214. return bytes.subarray(bytes.length);
  215. };
  216. exports.findNamedBox = findNamedBox;
  217. var parseSamples = function parseSamples(data, entrySize, parseEntry) {
  218. if (entrySize === void 0) {
  219. entrySize = 4;
  220. }
  221. if (parseEntry === void 0) {
  222. parseEntry = function parseEntry(d) {
  223. return (0, _byteHelpers.bytesToNumber)(d);
  224. };
  225. }
  226. var entries = [];
  227. if (!data || !data.length) {
  228. return entries;
  229. }
  230. var entryCount = (0, _byteHelpers.bytesToNumber)(data.subarray(4, 8));
  231. for (var i = 8; entryCount; i += entrySize, entryCount--) {
  232. entries.push(parseEntry(data.subarray(i, i + entrySize)));
  233. }
  234. return entries;
  235. };
  236. var buildFrameTable = function buildFrameTable(stbl, timescale) {
  237. var keySamples = parseSamples(findBox(stbl, ['stss'])[0]);
  238. var chunkOffsets = parseSamples(findBox(stbl, ['stco'])[0]);
  239. var timeToSamples = parseSamples(findBox(stbl, ['stts'])[0], 8, function (entry) {
  240. return {
  241. sampleCount: (0, _byteHelpers.bytesToNumber)(entry.subarray(0, 4)),
  242. sampleDelta: (0, _byteHelpers.bytesToNumber)(entry.subarray(4, 8))
  243. };
  244. });
  245. var samplesToChunks = parseSamples(findBox(stbl, ['stsc'])[0], 12, function (entry) {
  246. return {
  247. firstChunk: (0, _byteHelpers.bytesToNumber)(entry.subarray(0, 4)),
  248. samplesPerChunk: (0, _byteHelpers.bytesToNumber)(entry.subarray(4, 8)),
  249. sampleDescriptionIndex: (0, _byteHelpers.bytesToNumber)(entry.subarray(8, 12))
  250. };
  251. });
  252. var stsz = findBox(stbl, ['stsz'])[0]; // stsz starts with a 4 byte sampleSize which we don't need
  253. var sampleSizes = parseSamples(stsz && stsz.length && stsz.subarray(4) || null);
  254. var frames = [];
  255. for (var chunkIndex = 0; chunkIndex < chunkOffsets.length; chunkIndex++) {
  256. var samplesInChunk = void 0;
  257. for (var i = 0; i < samplesToChunks.length; i++) {
  258. var sampleToChunk = samplesToChunks[i];
  259. var isThisOne = chunkIndex + 1 >= sampleToChunk.firstChunk && (i + 1 >= samplesToChunks.length || chunkIndex + 1 < samplesToChunks[i + 1].firstChunk);
  260. if (isThisOne) {
  261. samplesInChunk = sampleToChunk.samplesPerChunk;
  262. break;
  263. }
  264. }
  265. var chunkOffset = chunkOffsets[chunkIndex];
  266. for (var _i = 0; _i < samplesInChunk; _i++) {
  267. var frameEnd = sampleSizes[frames.length]; // if we don't have key samples every frame is a keyframe
  268. var keyframe = !keySamples.length;
  269. if (keySamples.length && keySamples.indexOf(frames.length + 1) !== -1) {
  270. keyframe = true;
  271. }
  272. var frame = {
  273. keyframe: keyframe,
  274. start: chunkOffset,
  275. end: chunkOffset + frameEnd
  276. };
  277. for (var k = 0; k < timeToSamples.length; k++) {
  278. var _timeToSamples$k = timeToSamples[k],
  279. sampleCount = _timeToSamples$k.sampleCount,
  280. sampleDelta = _timeToSamples$k.sampleDelta;
  281. if (frames.length <= sampleCount) {
  282. // ms to ns
  283. var lastTimestamp = frames.length ? frames[frames.length - 1].timestamp : 0;
  284. frame.timestamp = lastTimestamp + sampleDelta / timescale * 1000;
  285. frame.duration = sampleDelta;
  286. break;
  287. }
  288. }
  289. frames.push(frame);
  290. chunkOffset += frameEnd;
  291. }
  292. }
  293. return frames;
  294. };
  295. exports.buildFrameTable = buildFrameTable;
  296. var addSampleDescription = function addSampleDescription(track, bytes) {
  297. var codec = (0, _byteHelpers.bytesToString)(bytes.subarray(0, 4));
  298. if (track.type === 'video') {
  299. track.info = track.info || {};
  300. track.info.width = bytes[28] << 8 | bytes[29];
  301. track.info.height = bytes[30] << 8 | bytes[31];
  302. } else if (track.type === 'audio') {
  303. track.info = track.info || {};
  304. track.info.channels = bytes[20] << 8 | bytes[21];
  305. track.info.bitDepth = bytes[22] << 8 | bytes[23];
  306. track.info.sampleRate = bytes[28] << 8 | bytes[29];
  307. }
  308. if (codec === 'avc1') {
  309. var avcC = findNamedBox(bytes, 'avcC'); // AVCDecoderConfigurationRecord
  310. codec += "." + (0, _codecHelpers.getAvcCodec)(avcC);
  311. track.info.avcC = avcC; // TODO: do we need to parse all this?
  312. /* {
  313. configurationVersion: avcC[0],
  314. profile: avcC[1],
  315. profileCompatibility: avcC[2],
  316. level: avcC[3],
  317. lengthSizeMinusOne: avcC[4] & 0x3
  318. };
  319. let spsNalUnitCount = avcC[5] & 0x1F;
  320. const spsNalUnits = track.info.avc.spsNalUnits = [];
  321. // past spsNalUnitCount
  322. let offset = 6;
  323. while (spsNalUnitCount--) {
  324. const nalLen = avcC[offset] << 8 | avcC[offset + 1];
  325. spsNalUnits.push(avcC.subarray(offset + 2, offset + 2 + nalLen));
  326. offset += nalLen + 2;
  327. }
  328. let ppsNalUnitCount = avcC[offset];
  329. const ppsNalUnits = track.info.avc.ppsNalUnits = [];
  330. // past ppsNalUnitCount
  331. offset += 1;
  332. while (ppsNalUnitCount--) {
  333. const nalLen = avcC[offset] << 8 | avcC[offset + 1];
  334. ppsNalUnits.push(avcC.subarray(offset + 2, offset + 2 + nalLen));
  335. offset += nalLen + 2;
  336. }*/
  337. // HEVCDecoderConfigurationRecord
  338. } else if (codec === 'hvc1' || codec === 'hev1') {
  339. codec += "." + (0, _codecHelpers.getHvcCodec)(findNamedBox(bytes, 'hvcC'));
  340. } else if (codec === 'mp4a' || codec === 'mp4v') {
  341. var esds = findNamedBox(bytes, 'esds');
  342. var esDescriptor = parseDescriptors(esds.subarray(4))[0];
  343. var decoderConfig = esDescriptor && esDescriptor.descriptors.filter(function (_ref) {
  344. var tag = _ref.tag;
  345. return tag === 0x04;
  346. })[0];
  347. if (decoderConfig) {
  348. // most codecs do not have a further '.'
  349. // such as 0xa5 for ac-3 and 0xa6 for e-ac-3
  350. codec += '.' + (0, _byteHelpers.toHexString)(decoderConfig.oti);
  351. if (decoderConfig.oti === 0x40) {
  352. codec += '.' + (decoderConfig.descriptors[0].bytes[0] >> 3).toString();
  353. } else if (decoderConfig.oti === 0x20) {
  354. codec += '.' + decoderConfig.descriptors[0].bytes[4].toString();
  355. } else if (decoderConfig.oti === 0xdd) {
  356. codec = 'vorbis';
  357. }
  358. } else if (track.type === 'audio') {
  359. codec += '.40.2';
  360. } else {
  361. codec += '.20.9';
  362. }
  363. } else if (codec === 'av01') {
  364. // AV1DecoderConfigurationRecord
  365. codec += "." + (0, _codecHelpers.getAv1Codec)(findNamedBox(bytes, 'av1C'));
  366. } else if (codec === 'vp09') {
  367. // VPCodecConfigurationRecord
  368. var vpcC = findNamedBox(bytes, 'vpcC'); // https://www.webmproject.org/vp9/mp4/
  369. var profile = vpcC[0];
  370. var level = vpcC[1];
  371. var bitDepth = vpcC[2] >> 4;
  372. var chromaSubsampling = (vpcC[2] & 0x0F) >> 1;
  373. var videoFullRangeFlag = (vpcC[2] & 0x0F) >> 3;
  374. var colourPrimaries = vpcC[3];
  375. var transferCharacteristics = vpcC[4];
  376. var matrixCoefficients = vpcC[5];
  377. codec += "." + (0, _byteHelpers.padStart)(profile, 2, '0');
  378. codec += "." + (0, _byteHelpers.padStart)(level, 2, '0');
  379. codec += "." + (0, _byteHelpers.padStart)(bitDepth, 2, '0');
  380. codec += "." + (0, _byteHelpers.padStart)(chromaSubsampling, 2, '0');
  381. codec += "." + (0, _byteHelpers.padStart)(colourPrimaries, 2, '0');
  382. codec += "." + (0, _byteHelpers.padStart)(transferCharacteristics, 2, '0');
  383. codec += "." + (0, _byteHelpers.padStart)(matrixCoefficients, 2, '0');
  384. codec += "." + (0, _byteHelpers.padStart)(videoFullRangeFlag, 2, '0');
  385. } else if (codec === 'theo') {
  386. codec = 'theora';
  387. } else if (codec === 'spex') {
  388. codec = 'speex';
  389. } else if (codec === '.mp3') {
  390. codec = 'mp4a.40.34';
  391. } else if (codec === 'msVo') {
  392. codec = 'vorbis';
  393. } else if (codec === 'Opus') {
  394. codec = 'opus';
  395. var dOps = findNamedBox(bytes, 'dOps');
  396. track.info.opus = (0, _opusHelpers.parseOpusHead)(dOps); // TODO: should this go into the webm code??
  397. // Firefox requires a codecDelay for opus playback
  398. // see https://bugzilla.mozilla.org/show_bug.cgi?id=1276238
  399. track.info.codecDelay = 6500000;
  400. } else {
  401. codec = codec.toLowerCase();
  402. }
  403. /* eslint-enable */
  404. // flac, ac-3, ec-3, opus
  405. track.codec = codec;
  406. };
  407. exports.addSampleDescription = addSampleDescription;
  408. var parseTracks = function parseTracks(bytes, frameTable) {
  409. if (frameTable === void 0) {
  410. frameTable = true;
  411. }
  412. bytes = (0, _byteHelpers.toUint8)(bytes);
  413. var traks = findBox(bytes, ['moov', 'trak'], true);
  414. var tracks = [];
  415. traks.forEach(function (trak) {
  416. var track = {
  417. bytes: trak
  418. };
  419. var mdia = findBox(trak, ['mdia'])[0];
  420. var hdlr = findBox(mdia, ['hdlr'])[0];
  421. var trakType = (0, _byteHelpers.bytesToString)(hdlr.subarray(8, 12));
  422. if (trakType === 'soun') {
  423. track.type = 'audio';
  424. } else if (trakType === 'vide') {
  425. track.type = 'video';
  426. } else {
  427. track.type = trakType;
  428. }
  429. var tkhd = findBox(trak, ['tkhd'])[0];
  430. if (tkhd) {
  431. var view = new DataView(tkhd.buffer, tkhd.byteOffset, tkhd.byteLength);
  432. var tkhdVersion = view.getUint8(0);
  433. track.number = tkhdVersion === 0 ? view.getUint32(12) : view.getUint32(20);
  434. }
  435. var mdhd = findBox(mdia, ['mdhd'])[0];
  436. if (mdhd) {
  437. // mdhd is a FullBox, meaning it will have its own version as the first byte
  438. var version = mdhd[0];
  439. var index = version === 0 ? 12 : 20;
  440. track.timescale = (mdhd[index] << 24 | mdhd[index + 1] << 16 | mdhd[index + 2] << 8 | mdhd[index + 3]) >>> 0;
  441. }
  442. var stbl = findBox(mdia, ['minf', 'stbl'])[0];
  443. var stsd = findBox(stbl, ['stsd'])[0];
  444. var descriptionCount = (0, _byteHelpers.bytesToNumber)(stsd.subarray(4, 8));
  445. var offset = 8; // add codec and codec info
  446. while (descriptionCount--) {
  447. var len = (0, _byteHelpers.bytesToNumber)(stsd.subarray(offset, offset + 4));
  448. var sampleDescriptor = stsd.subarray(offset + 4, offset + 4 + len);
  449. addSampleDescription(track, sampleDescriptor);
  450. offset += 4 + len;
  451. }
  452. if (frameTable) {
  453. track.frameTable = buildFrameTable(stbl, track.timescale);
  454. } // codec has no sub parameters
  455. tracks.push(track);
  456. });
  457. return tracks;
  458. };
  459. exports.parseTracks = parseTracks;
  460. var parseMediaInfo = function parseMediaInfo(bytes) {
  461. var mvhd = findBox(bytes, ['moov', 'mvhd'], true)[0];
  462. if (!mvhd || !mvhd.length) {
  463. return;
  464. }
  465. var info = {}; // ms to ns
  466. // mvhd v1 has 8 byte duration and other fields too
  467. if (mvhd[0] === 1) {
  468. info.timestampScale = (0, _byteHelpers.bytesToNumber)(mvhd.subarray(20, 24));
  469. info.duration = (0, _byteHelpers.bytesToNumber)(mvhd.subarray(24, 32));
  470. } else {
  471. info.timestampScale = (0, _byteHelpers.bytesToNumber)(mvhd.subarray(12, 16));
  472. info.duration = (0, _byteHelpers.bytesToNumber)(mvhd.subarray(16, 20));
  473. }
  474. info.bytes = mvhd;
  475. return info;
  476. };
  477. exports.parseMediaInfo = parseMediaInfo;