mp4-inspector.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828
  1. /**
  2. * mux.js
  3. *
  4. * Copyright (c) 2015 Brightcove
  5. * All rights reserved.
  6. *
  7. * Parse the internal MP4 structure into an equivalent javascript
  8. * object.
  9. */
  10. 'use strict';
  11. var
  12. inspectMp4,
  13. textifyMp4,
  14. parseType = require('../mp4/probe').parseType,
  15. parseMp4Date = function(seconds) {
  16. return new Date(seconds * 1000 - 2082844800000);
  17. },
  18. parseSampleFlags = function(flags) {
  19. return {
  20. isLeading: (flags[0] & 0x0c) >>> 2,
  21. dependsOn: flags[0] & 0x03,
  22. isDependedOn: (flags[1] & 0xc0) >>> 6,
  23. hasRedundancy: (flags[1] & 0x30) >>> 4,
  24. paddingValue: (flags[1] & 0x0e) >>> 1,
  25. isNonSyncSample: flags[1] & 0x01,
  26. degradationPriority: (flags[2] << 8) | flags[3]
  27. };
  28. },
  29. nalParse = function(avcStream) {
  30. var
  31. avcView = new DataView(avcStream.buffer, avcStream.byteOffset, avcStream.byteLength),
  32. result = [],
  33. i,
  34. length;
  35. for (i = 0; i + 4 < avcStream.length; i += length) {
  36. length = avcView.getUint32(i);
  37. i += 4;
  38. // bail if this doesn't appear to be an H264 stream
  39. if (length <= 0) {
  40. result.push('<span style=\'color:red;\'>MALFORMED DATA</span>');
  41. continue;
  42. }
  43. switch (avcStream[i] & 0x1F) {
  44. case 0x01:
  45. result.push('slice_layer_without_partitioning_rbsp');
  46. break;
  47. case 0x05:
  48. result.push('slice_layer_without_partitioning_rbsp_idr');
  49. break;
  50. case 0x06:
  51. result.push('sei_rbsp');
  52. break;
  53. case 0x07:
  54. result.push('seq_parameter_set_rbsp');
  55. break;
  56. case 0x08:
  57. result.push('pic_parameter_set_rbsp');
  58. break;
  59. case 0x09:
  60. result.push('access_unit_delimiter_rbsp');
  61. break;
  62. default:
  63. result.push('UNKNOWN NAL - ' + avcStream[i] & 0x1F);
  64. break;
  65. }
  66. }
  67. return result;
  68. },
  69. // registry of handlers for individual mp4 box types
  70. parse = {
  71. // codingname, not a first-class box type. stsd entries share the
  72. // same format as real boxes so the parsing infrastructure can be
  73. // shared
  74. avc1: function(data) {
  75. var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
  76. return {
  77. dataReferenceIndex: view.getUint16(6),
  78. width: view.getUint16(24),
  79. height: view.getUint16(26),
  80. horizresolution: view.getUint16(28) + (view.getUint16(30) / 16),
  81. vertresolution: view.getUint16(32) + (view.getUint16(34) / 16),
  82. frameCount: view.getUint16(40),
  83. depth: view.getUint16(74),
  84. config: inspectMp4(data.subarray(78, data.byteLength))
  85. };
  86. },
  87. avcC: function(data) {
  88. var
  89. view = new DataView(data.buffer, data.byteOffset, data.byteLength),
  90. result = {
  91. configurationVersion: data[0],
  92. avcProfileIndication: data[1],
  93. profileCompatibility: data[2],
  94. avcLevelIndication: data[3],
  95. lengthSizeMinusOne: data[4] & 0x03,
  96. sps: [],
  97. pps: []
  98. },
  99. numOfSequenceParameterSets = data[5] & 0x1f,
  100. numOfPictureParameterSets,
  101. nalSize,
  102. offset,
  103. i;
  104. // iterate past any SPSs
  105. offset = 6;
  106. for (i = 0; i < numOfSequenceParameterSets; i++) {
  107. nalSize = view.getUint16(offset);
  108. offset += 2;
  109. result.sps.push(new Uint8Array(data.subarray(offset, offset + nalSize)));
  110. offset += nalSize;
  111. }
  112. // iterate past any PPSs
  113. numOfPictureParameterSets = data[offset];
  114. offset++;
  115. for (i = 0; i < numOfPictureParameterSets; i++) {
  116. nalSize = view.getUint16(offset);
  117. offset += 2;
  118. result.pps.push(new Uint8Array(data.subarray(offset, offset + nalSize)));
  119. offset += nalSize;
  120. }
  121. return result;
  122. },
  123. btrt: function(data) {
  124. var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
  125. return {
  126. bufferSizeDB: view.getUint32(0),
  127. maxBitrate: view.getUint32(4),
  128. avgBitrate: view.getUint32(8)
  129. };
  130. },
  131. esds: function(data) {
  132. return {
  133. version: data[0],
  134. flags: new Uint8Array(data.subarray(1, 4)),
  135. esId: (data[6] << 8) | data[7],
  136. streamPriority: data[8] & 0x1f,
  137. decoderConfig: {
  138. objectProfileIndication: data[11],
  139. streamType: (data[12] >>> 2) & 0x3f,
  140. bufferSize: (data[13] << 16) | (data[14] << 8) | data[15],
  141. maxBitrate: (data[16] << 24) |
  142. (data[17] << 16) |
  143. (data[18] << 8) |
  144. data[19],
  145. avgBitrate: (data[20] << 24) |
  146. (data[21] << 16) |
  147. (data[22] << 8) |
  148. data[23],
  149. decoderConfigDescriptor: {
  150. tag: data[24],
  151. length: data[25],
  152. audioObjectType: (data[26] >>> 3) & 0x1f,
  153. samplingFrequencyIndex: ((data[26] & 0x07) << 1) |
  154. ((data[27] >>> 7) & 0x01),
  155. channelConfiguration: (data[27] >>> 3) & 0x0f
  156. }
  157. }
  158. };
  159. },
  160. ftyp: function(data) {
  161. var
  162. view = new DataView(data.buffer, data.byteOffset, data.byteLength),
  163. result = {
  164. majorBrand: parseType(data.subarray(0, 4)),
  165. minorVersion: view.getUint32(4),
  166. compatibleBrands: []
  167. },
  168. i = 8;
  169. while (i < data.byteLength) {
  170. result.compatibleBrands.push(parseType(data.subarray(i, i + 4)));
  171. i += 4;
  172. }
  173. return result;
  174. },
  175. dinf: function(data) {
  176. return {
  177. boxes: inspectMp4(data)
  178. };
  179. },
  180. dref: function(data) {
  181. return {
  182. version: data[0],
  183. flags: new Uint8Array(data.subarray(1, 4)),
  184. dataReferences: inspectMp4(data.subarray(8))
  185. };
  186. },
  187. hdlr: function(data) {
  188. var
  189. view = new DataView(data.buffer, data.byteOffset, data.byteLength),
  190. result = {
  191. version: view.getUint8(0),
  192. flags: new Uint8Array(data.subarray(1, 4)),
  193. handlerType: parseType(data.subarray(8, 12)),
  194. name: ''
  195. },
  196. i = 8;
  197. // parse out the name field
  198. for (i = 24; i < data.byteLength; i++) {
  199. if (data[i] === 0x00) {
  200. // the name field is null-terminated
  201. i++;
  202. break;
  203. }
  204. result.name += String.fromCharCode(data[i]);
  205. }
  206. // decode UTF-8 to javascript's internal representation
  207. // see http://ecmanaut.blogspot.com/2006/07/encoding-decoding-utf8-in-javascript.html
  208. result.name = decodeURIComponent(global.escape(result.name));
  209. return result;
  210. },
  211. mdat: function(data) {
  212. return {
  213. byteLength: data.byteLength,
  214. nals: nalParse(data)
  215. };
  216. },
  217. mdhd: function(data) {
  218. var
  219. view = new DataView(data.buffer, data.byteOffset, data.byteLength),
  220. i = 4,
  221. language,
  222. result = {
  223. version: view.getUint8(0),
  224. flags: new Uint8Array(data.subarray(1, 4)),
  225. language: ''
  226. };
  227. if (result.version === 1) {
  228. i += 4;
  229. result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
  230. i += 8;
  231. result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
  232. i += 4;
  233. result.timescale = view.getUint32(i);
  234. i += 8;
  235. result.duration = view.getUint32(i); // truncating top 4 bytes
  236. } else {
  237. result.creationTime = parseMp4Date(view.getUint32(i));
  238. i += 4;
  239. result.modificationTime = parseMp4Date(view.getUint32(i));
  240. i += 4;
  241. result.timescale = view.getUint32(i);
  242. i += 4;
  243. result.duration = view.getUint32(i);
  244. }
  245. i += 4;
  246. // language is stored as an ISO-639-2/T code in an array of three 5-bit fields
  247. // each field is the packed difference between its ASCII value and 0x60
  248. language = view.getUint16(i);
  249. result.language += String.fromCharCode((language >> 10) + 0x60);
  250. result.language += String.fromCharCode(((language & 0x03c0) >> 5) + 0x60);
  251. result.language += String.fromCharCode((language & 0x1f) + 0x60);
  252. return result;
  253. },
  254. mdia: function(data) {
  255. return {
  256. boxes: inspectMp4(data)
  257. };
  258. },
  259. mfhd: function(data) {
  260. return {
  261. version: data[0],
  262. flags: new Uint8Array(data.subarray(1, 4)),
  263. sequenceNumber: (data[4] << 24) |
  264. (data[5] << 16) |
  265. (data[6] << 8) |
  266. (data[7])
  267. };
  268. },
  269. minf: function(data) {
  270. return {
  271. boxes: inspectMp4(data)
  272. };
  273. },
  274. // codingname, not a first-class box type. stsd entries share the
  275. // same format as real boxes so the parsing infrastructure can be
  276. // shared
  277. mp4a: function(data) {
  278. var
  279. view = new DataView(data.buffer, data.byteOffset, data.byteLength),
  280. result = {
  281. // 6 bytes reserved
  282. dataReferenceIndex: view.getUint16(6),
  283. // 4 + 4 bytes reserved
  284. channelcount: view.getUint16(16),
  285. samplesize: view.getUint16(18),
  286. // 2 bytes pre_defined
  287. // 2 bytes reserved
  288. samplerate: view.getUint16(24) + (view.getUint16(26) / 65536)
  289. };
  290. // if there are more bytes to process, assume this is an ISO/IEC
  291. // 14496-14 MP4AudioSampleEntry and parse the ESDBox
  292. if (data.byteLength > 28) {
  293. result.streamDescriptor = inspectMp4(data.subarray(28))[0];
  294. }
  295. return result;
  296. },
  297. moof: function(data) {
  298. return {
  299. boxes: inspectMp4(data)
  300. };
  301. },
  302. moov: function(data) {
  303. return {
  304. boxes: inspectMp4(data)
  305. };
  306. },
  307. mvex: function(data) {
  308. return {
  309. boxes: inspectMp4(data)
  310. };
  311. },
  312. mvhd: function(data) {
  313. var
  314. view = new DataView(data.buffer, data.byteOffset, data.byteLength),
  315. i = 4,
  316. result = {
  317. version: view.getUint8(0),
  318. flags: new Uint8Array(data.subarray(1, 4))
  319. };
  320. if (result.version === 1) {
  321. i += 4;
  322. result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
  323. i += 8;
  324. result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
  325. i += 4;
  326. result.timescale = view.getUint32(i);
  327. i += 8;
  328. result.duration = view.getUint32(i); // truncating top 4 bytes
  329. } else {
  330. result.creationTime = parseMp4Date(view.getUint32(i));
  331. i += 4;
  332. result.modificationTime = parseMp4Date(view.getUint32(i));
  333. i += 4;
  334. result.timescale = view.getUint32(i);
  335. i += 4;
  336. result.duration = view.getUint32(i);
  337. }
  338. i += 4;
  339. // convert fixed-point, base 16 back to a number
  340. result.rate = view.getUint16(i) + (view.getUint16(i + 2) / 16);
  341. i += 4;
  342. result.volume = view.getUint8(i) + (view.getUint8(i + 1) / 8);
  343. i += 2;
  344. i += 2;
  345. i += 2 * 4;
  346. result.matrix = new Uint32Array(data.subarray(i, i + (9 * 4)));
  347. i += 9 * 4;
  348. i += 6 * 4;
  349. result.nextTrackId = view.getUint32(i);
  350. return result;
  351. },
  352. pdin: function(data) {
  353. var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
  354. return {
  355. version: view.getUint8(0),
  356. flags: new Uint8Array(data.subarray(1, 4)),
  357. rate: view.getUint32(4),
  358. initialDelay: view.getUint32(8)
  359. };
  360. },
  361. sdtp: function(data) {
  362. var
  363. result = {
  364. version: data[0],
  365. flags: new Uint8Array(data.subarray(1, 4)),
  366. samples: []
  367. }, i;
  368. for (i = 4; i < data.byteLength; i++) {
  369. result.samples.push({
  370. dependsOn: (data[i] & 0x30) >> 4,
  371. isDependedOn: (data[i] & 0x0c) >> 2,
  372. hasRedundancy: data[i] & 0x03
  373. });
  374. }
  375. return result;
  376. },
  377. sidx: function(data) {
  378. var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
  379. result = {
  380. version: data[0],
  381. flags: new Uint8Array(data.subarray(1, 4)),
  382. references: [],
  383. referenceId: view.getUint32(4),
  384. timescale: view.getUint32(8),
  385. earliestPresentationTime: view.getUint32(12),
  386. firstOffset: view.getUint32(16)
  387. },
  388. referenceCount = view.getUint16(22),
  389. i;
  390. for (i = 24; referenceCount; i += 12, referenceCount--) {
  391. result.references.push({
  392. referenceType: (data[i] & 0x80) >>> 7,
  393. referencedSize: view.getUint32(i) & 0x7FFFFFFF,
  394. subsegmentDuration: view.getUint32(i + 4),
  395. startsWithSap: !!(data[i + 8] & 0x80),
  396. sapType: (data[i + 8] & 0x70) >>> 4,
  397. sapDeltaTime: view.getUint32(i + 8) & 0x0FFFFFFF
  398. });
  399. }
  400. return result;
  401. },
  402. smhd: function(data) {
  403. return {
  404. version: data[0],
  405. flags: new Uint8Array(data.subarray(1, 4)),
  406. balance: data[4] + (data[5] / 256)
  407. };
  408. },
  409. stbl: function(data) {
  410. return {
  411. boxes: inspectMp4(data)
  412. };
  413. },
  414. stco: function(data) {
  415. var
  416. view = new DataView(data.buffer, data.byteOffset, data.byteLength),
  417. result = {
  418. version: data[0],
  419. flags: new Uint8Array(data.subarray(1, 4)),
  420. chunkOffsets: []
  421. },
  422. entryCount = view.getUint32(4),
  423. i;
  424. for (i = 8; entryCount; i += 4, entryCount--) {
  425. result.chunkOffsets.push(view.getUint32(i));
  426. }
  427. return result;
  428. },
  429. stsc: function(data) {
  430. var
  431. view = new DataView(data.buffer, data.byteOffset, data.byteLength),
  432. entryCount = view.getUint32(4),
  433. result = {
  434. version: data[0],
  435. flags: new Uint8Array(data.subarray(1, 4)),
  436. sampleToChunks: []
  437. },
  438. i;
  439. for (i = 8; entryCount; i += 12, entryCount--) {
  440. result.sampleToChunks.push({
  441. firstChunk: view.getUint32(i),
  442. samplesPerChunk: view.getUint32(i + 4),
  443. sampleDescriptionIndex: view.getUint32(i + 8)
  444. });
  445. }
  446. return result;
  447. },
  448. stsd: function(data) {
  449. return {
  450. version: data[0],
  451. flags: new Uint8Array(data.subarray(1, 4)),
  452. sampleDescriptions: inspectMp4(data.subarray(8))
  453. };
  454. },
  455. stsz: function(data) {
  456. var
  457. view = new DataView(data.buffer, data.byteOffset, data.byteLength),
  458. result = {
  459. version: data[0],
  460. flags: new Uint8Array(data.subarray(1, 4)),
  461. sampleSize: view.getUint32(4),
  462. entries: []
  463. },
  464. i;
  465. for (i = 12; i < data.byteLength; i += 4) {
  466. result.entries.push(view.getUint32(i));
  467. }
  468. return result;
  469. },
  470. stts: function(data) {
  471. var
  472. view = new DataView(data.buffer, data.byteOffset, data.byteLength),
  473. result = {
  474. version: data[0],
  475. flags: new Uint8Array(data.subarray(1, 4)),
  476. timeToSamples: []
  477. },
  478. entryCount = view.getUint32(4),
  479. i;
  480. for (i = 8; entryCount; i += 8, entryCount--) {
  481. result.timeToSamples.push({
  482. sampleCount: view.getUint32(i),
  483. sampleDelta: view.getUint32(i + 4)
  484. });
  485. }
  486. return result;
  487. },
  488. styp: function(data) {
  489. return parse.ftyp(data);
  490. },
  491. tfdt: function(data) {
  492. var result = {
  493. version: data[0],
  494. flags: new Uint8Array(data.subarray(1, 4)),
  495. baseMediaDecodeTime: data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]
  496. };
  497. if (result.version === 1) {
  498. result.baseMediaDecodeTime *= Math.pow(2, 32);
  499. result.baseMediaDecodeTime += data[8] << 24 | data[9] << 16 | data[10] << 8 | data[11];
  500. }
  501. return result;
  502. },
  503. tfhd: function(data) {
  504. var
  505. view = new DataView(data.buffer, data.byteOffset, data.byteLength),
  506. result = {
  507. version: data[0],
  508. flags: new Uint8Array(data.subarray(1, 4)),
  509. trackId: view.getUint32(4)
  510. },
  511. baseDataOffsetPresent = result.flags[2] & 0x01,
  512. sampleDescriptionIndexPresent = result.flags[2] & 0x02,
  513. defaultSampleDurationPresent = result.flags[2] & 0x08,
  514. defaultSampleSizePresent = result.flags[2] & 0x10,
  515. defaultSampleFlagsPresent = result.flags[2] & 0x20,
  516. i;
  517. i = 8;
  518. if (baseDataOffsetPresent) {
  519. i += 4; // truncate top 4 bytes
  520. result.baseDataOffset = view.getUint32(12);
  521. i += 4;
  522. }
  523. if (sampleDescriptionIndexPresent) {
  524. result.sampleDescriptionIndex = view.getUint32(i);
  525. i += 4;
  526. }
  527. if (defaultSampleDurationPresent) {
  528. result.defaultSampleDuration = view.getUint32(i);
  529. i += 4;
  530. }
  531. if (defaultSampleSizePresent) {
  532. result.defaultSampleSize = view.getUint32(i);
  533. i += 4;
  534. }
  535. if (defaultSampleFlagsPresent) {
  536. result.defaultSampleFlags = view.getUint32(i);
  537. }
  538. return result;
  539. },
  540. tkhd: function(data) {
  541. var
  542. view = new DataView(data.buffer, data.byteOffset, data.byteLength),
  543. i = 4,
  544. result = {
  545. version: view.getUint8(0),
  546. flags: new Uint8Array(data.subarray(1, 4))
  547. };
  548. if (result.version === 1) {
  549. i += 4;
  550. result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
  551. i += 8;
  552. result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
  553. i += 4;
  554. result.trackId = view.getUint32(i);
  555. i += 4;
  556. i += 8;
  557. result.duration = view.getUint32(i); // truncating top 4 bytes
  558. } else {
  559. result.creationTime = parseMp4Date(view.getUint32(i));
  560. i += 4;
  561. result.modificationTime = parseMp4Date(view.getUint32(i));
  562. i += 4;
  563. result.trackId = view.getUint32(i);
  564. i += 4;
  565. i += 4;
  566. result.duration = view.getUint32(i);
  567. }
  568. i += 4;
  569. i += 2 * 4;
  570. result.layer = view.getUint16(i);
  571. i += 2;
  572. result.alternateGroup = view.getUint16(i);
  573. i += 2;
  574. // convert fixed-point, base 16 back to a number
  575. result.volume = view.getUint8(i) + (view.getUint8(i + 1) / 8);
  576. i += 2;
  577. i += 2;
  578. result.matrix = new Uint32Array(data.subarray(i, i + (9 * 4)));
  579. i += 9 * 4;
  580. result.width = view.getUint16(i) + (view.getUint16(i + 2) / 16);
  581. i += 4;
  582. result.height = view.getUint16(i) + (view.getUint16(i + 2) / 16);
  583. return result;
  584. },
  585. traf: function(data) {
  586. return {
  587. boxes: inspectMp4(data)
  588. };
  589. },
  590. trak: function(data) {
  591. return {
  592. boxes: inspectMp4(data)
  593. };
  594. },
  595. trex: function(data) {
  596. var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
  597. return {
  598. version: data[0],
  599. flags: new Uint8Array(data.subarray(1, 4)),
  600. trackId: view.getUint32(4),
  601. defaultSampleDescriptionIndex: view.getUint32(8),
  602. defaultSampleDuration: view.getUint32(12),
  603. defaultSampleSize: view.getUint32(16),
  604. sampleDependsOn: data[20] & 0x03,
  605. sampleIsDependedOn: (data[21] & 0xc0) >> 6,
  606. sampleHasRedundancy: (data[21] & 0x30) >> 4,
  607. samplePaddingValue: (data[21] & 0x0e) >> 1,
  608. sampleIsDifferenceSample: !!(data[21] & 0x01),
  609. sampleDegradationPriority: view.getUint16(22)
  610. };
  611. },
  612. trun: function(data) {
  613. var
  614. result = {
  615. version: data[0],
  616. flags: new Uint8Array(data.subarray(1, 4)),
  617. samples: []
  618. },
  619. view = new DataView(data.buffer, data.byteOffset, data.byteLength),
  620. dataOffsetPresent = result.flags[2] & 0x01,
  621. firstSampleFlagsPresent = result.flags[2] & 0x04,
  622. sampleDurationPresent = result.flags[1] & 0x01,
  623. sampleSizePresent = result.flags[1] & 0x02,
  624. sampleFlagsPresent = result.flags[1] & 0x04,
  625. sampleCompositionTimeOffsetPresent = result.flags[1] & 0x08,
  626. sampleCount = view.getUint32(4),
  627. offset = 8,
  628. sample;
  629. if (dataOffsetPresent) {
  630. result.dataOffset = view.getUint32(offset);
  631. offset += 4;
  632. }
  633. if (firstSampleFlagsPresent && sampleCount) {
  634. sample = {
  635. flags: parseSampleFlags(data.subarray(offset, offset + 4))
  636. };
  637. offset += 4;
  638. if (sampleDurationPresent) {
  639. sample.duration = view.getUint32(offset);
  640. offset += 4;
  641. }
  642. if (sampleSizePresent) {
  643. sample.size = view.getUint32(offset);
  644. offset += 4;
  645. }
  646. if (sampleCompositionTimeOffsetPresent) {
  647. sample.compositionTimeOffset = view.getUint32(offset);
  648. offset += 4;
  649. }
  650. result.samples.push(sample);
  651. sampleCount--;
  652. }
  653. while (sampleCount--) {
  654. sample = {};
  655. if (sampleDurationPresent) {
  656. sample.duration = view.getUint32(offset);
  657. offset += 4;
  658. }
  659. if (sampleSizePresent) {
  660. sample.size = view.getUint32(offset);
  661. offset += 4;
  662. }
  663. if (sampleFlagsPresent) {
  664. sample.flags = parseSampleFlags(data.subarray(offset, offset + 4));
  665. offset += 4;
  666. }
  667. if (sampleCompositionTimeOffsetPresent) {
  668. sample.compositionTimeOffset = view.getUint32(offset);
  669. offset += 4;
  670. }
  671. result.samples.push(sample);
  672. }
  673. return result;
  674. },
  675. 'url ': function(data) {
  676. return {
  677. version: data[0],
  678. flags: new Uint8Array(data.subarray(1, 4))
  679. };
  680. },
  681. vmhd: function(data) {
  682. var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
  683. return {
  684. version: data[0],
  685. flags: new Uint8Array(data.subarray(1, 4)),
  686. graphicsmode: view.getUint16(4),
  687. opcolor: new Uint16Array([view.getUint16(6),
  688. view.getUint16(8),
  689. view.getUint16(10)])
  690. };
  691. }
  692. };
  693. /**
  694. * Return a javascript array of box objects parsed from an ISO base
  695. * media file.
  696. * @param data {Uint8Array} the binary data of the media to be inspected
  697. * @return {array} a javascript array of potentially nested box objects
  698. */
  699. inspectMp4 = function(data) {
  700. var
  701. i = 0,
  702. result = [],
  703. view,
  704. size,
  705. type,
  706. end,
  707. box;
  708. // Convert data from Uint8Array to ArrayBuffer, to follow Dataview API
  709. var ab = new ArrayBuffer(data.length);
  710. var v = new Uint8Array(ab);
  711. for (var z = 0; z < data.length; ++z) {
  712. v[z] = data[z];
  713. }
  714. view = new DataView(ab);
  715. while (i < data.byteLength) {
  716. // parse box data
  717. size = view.getUint32(i);
  718. type = parseType(data.subarray(i + 4, i + 8));
  719. end = size > 1 ? i + size : data.byteLength;
  720. // parse type-specific data
  721. box = (parse[type] || function(data) {
  722. return {
  723. data: data
  724. };
  725. })(data.subarray(i + 8, end));
  726. box.size = size;
  727. box.type = type;
  728. // store this box and move to the next
  729. result.push(box);
  730. i = end;
  731. }
  732. return result;
  733. };
  734. /**
  735. * Returns a textual representation of the javascript represtentation
  736. * of an MP4 file. You can use it as an alternative to
  737. * JSON.stringify() to compare inspected MP4s.
  738. * @param inspectedMp4 {array} the parsed array of boxes in an MP4
  739. * file
  740. * @param depth {number} (optional) the number of ancestor boxes of
  741. * the elements of inspectedMp4. Assumed to be zero if unspecified.
  742. * @return {string} a text representation of the parsed MP4
  743. */
  744. textifyMp4 = function(inspectedMp4, depth) {
  745. var indent;
  746. depth = depth || 0;
  747. indent = new Array(depth * 2 + 1).join(' ');
  748. // iterate over all the boxes
  749. return inspectedMp4.map(function(box, index) {
  750. // list the box type first at the current indentation level
  751. return indent + box.type + '\n' +
  752. // the type is already included and handle child boxes separately
  753. Object.keys(box).filter(function(key) {
  754. return key !== 'type' && key !== 'boxes';
  755. // output all the box properties
  756. }).map(function(key) {
  757. var prefix = indent + ' ' + key + ': ',
  758. value = box[key];
  759. // print out raw bytes as hexademical
  760. if (value instanceof Uint8Array || value instanceof Uint32Array) {
  761. var bytes = Array.prototype.slice.call(new Uint8Array(value.buffer, value.byteOffset, value.byteLength))
  762. .map(function(byte) {
  763. return ' ' + ('00' + byte.toString(16)).slice(-2);
  764. }).join('').match(/.{1,24}/g);
  765. if (!bytes) {
  766. return prefix + '<>';
  767. }
  768. if (bytes.length === 1) {
  769. return prefix + '<' + bytes.join('').slice(1) + '>';
  770. }
  771. return prefix + '<\n' + bytes.map(function(line) {
  772. return indent + ' ' + line;
  773. }).join('\n') + '\n' + indent + ' >';
  774. }
  775. // stringify generic objects
  776. return prefix +
  777. JSON.stringify(value, null, 2)
  778. .split('\n').map(function(line, index) {
  779. if (index === 0) {
  780. return line;
  781. }
  782. return indent + ' ' + line;
  783. }).join('\n');
  784. }).join('\n') +
  785. // recursively textify the child boxes
  786. (box.boxes ? '\n' + textifyMp4(box.boxes, depth + 1) : '');
  787. }).join('\n');
  788. };
  789. module.exports = {
  790. inspect: inspectMp4,
  791. textify: textifyMp4
  792. };