Source: lib/util/mp4_parser.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.util.Mp4Parser');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.log');
  9. goog.require('shaka.util.DataViewReader');
  10. /**
  11. * @export
  12. */
  13. shaka.util.Mp4Parser = class {
  14. /** */
  15. constructor() {
  16. /** @private {!Map<number, shaka.util.Mp4Parser.BoxType_>} */
  17. this.headers_ = new Map();
  18. /** @private {!Map<number, !shaka.util.Mp4Parser.CallbackType>} */
  19. this.boxDefinitions_ = new Map();
  20. /** @private {boolean} */
  21. this.done_ = false;
  22. }
  23. /**
  24. * Declare a box type as a Box.
  25. *
  26. * @param {string} type
  27. * @param {!shaka.util.Mp4Parser.CallbackType} definition
  28. * @return {!shaka.util.Mp4Parser}
  29. * @export
  30. */
  31. box(type, definition) {
  32. const typeCode = shaka.util.Mp4Parser.typeFromString_(type);
  33. this.headers_.set(typeCode, shaka.util.Mp4Parser.BoxType_.BASIC_BOX);
  34. this.boxDefinitions_.set(typeCode, definition);
  35. return this;
  36. }
  37. /**
  38. * Declare a box type as a Full Box.
  39. *
  40. * @param {string} type
  41. * @param {!shaka.util.Mp4Parser.CallbackType} definition
  42. * @return {!shaka.util.Mp4Parser}
  43. * @export
  44. */
  45. fullBox(type, definition) {
  46. const typeCode = shaka.util.Mp4Parser.typeFromString_(type);
  47. this.headers_.set(typeCode, shaka.util.Mp4Parser.BoxType_.FULL_BOX);
  48. this.boxDefinitions_.set(typeCode, definition);
  49. return this;
  50. }
  51. /**
  52. * Stop parsing. Useful for extracting information from partial segments and
  53. * avoiding an out-of-bounds error once you find what you are looking for.
  54. *
  55. * @export
  56. */
  57. stop() {
  58. this.done_ = true;
  59. }
  60. /**
  61. * Parse the given data using the added callbacks.
  62. *
  63. * @param {!BufferSource} data
  64. * @param {boolean=} partialOkay If true, allow reading partial payloads
  65. * from some boxes. If the goal is a child box, we can sometimes find it
  66. * without enough data to find all child boxes.
  67. * @param {boolean=} stopOnPartial If true, stop reading if an incomplete
  68. * box is detected.
  69. * @export
  70. */
  71. parse(data, partialOkay, stopOnPartial) {
  72. const reader = new shaka.util.DataViewReader(
  73. data, shaka.util.DataViewReader.Endianness.BIG_ENDIAN);
  74. this.done_ = false;
  75. while (reader.hasMoreData() && !this.done_) {
  76. this.parseNext(0, reader, partialOkay, stopOnPartial);
  77. }
  78. }
  79. /**
  80. * Parse the next box on the current level.
  81. *
  82. * @param {number} absStart The absolute start position in the original
  83. * byte array.
  84. * @param {!shaka.util.DataViewReader} reader
  85. * @param {boolean=} partialOkay If true, allow reading partial payloads
  86. * from some boxes. If the goal is a child box, we can sometimes find it
  87. * without enough data to find all child boxes.
  88. * @param {boolean=} stopOnPartial If true, stop reading if an incomplete
  89. * box is detected.
  90. * @export
  91. */
  92. parseNext(absStart, reader, partialOkay, stopOnPartial) {
  93. const start = reader.getPosition();
  94. // size(4 bytes) + type(4 bytes) = 8 bytes
  95. if (stopOnPartial && start + 8 > reader.getLength()) {
  96. this.done_ = true;
  97. return;
  98. }
  99. let size = reader.readUint32();
  100. const type = reader.readUint32();
  101. const name = shaka.util.Mp4Parser.typeToString(type);
  102. let has64BitSize = false;
  103. shaka.log.v2('Parsing MP4 box', name);
  104. switch (size) {
  105. case 0:
  106. size = reader.getLength() - start;
  107. break;
  108. case 1:
  109. if (stopOnPartial && reader.getPosition() + 8 > reader.getLength()) {
  110. this.done_ = true;
  111. return;
  112. }
  113. size = reader.readUint64();
  114. has64BitSize = true;
  115. break;
  116. }
  117. const boxDefinition = this.boxDefinitions_.get(type);
  118. if (boxDefinition) {
  119. let version = null;
  120. let flags = null;
  121. if (this.headers_.get(type) == shaka.util.Mp4Parser.BoxType_.FULL_BOX) {
  122. if (stopOnPartial && reader.getPosition() + 4 > reader.getLength()) {
  123. this.done_ = true;
  124. return;
  125. }
  126. const versionAndFlags = reader.readUint32();
  127. version = versionAndFlags >>> 24;
  128. flags = versionAndFlags & 0xFFFFFF;
  129. }
  130. // Read the whole payload so that the current level can be safely read
  131. // regardless of how the payload is parsed.
  132. let end = start + size;
  133. if (partialOkay && end > reader.getLength()) {
  134. // For partial reads, truncate the payload if we must.
  135. end = reader.getLength();
  136. }
  137. if (stopOnPartial && end > reader.getLength()) {
  138. this.done_ = true;
  139. return;
  140. }
  141. const payloadSize = end - reader.getPosition();
  142. const payload =
  143. (payloadSize > 0) ? reader.readBytes(payloadSize) : new Uint8Array(0);
  144. const payloadReader = new shaka.util.DataViewReader(
  145. payload, shaka.util.DataViewReader.Endianness.BIG_ENDIAN);
  146. /** @type {shaka.extern.ParsedBox} */
  147. const box = {
  148. name,
  149. parser: this,
  150. partialOkay: partialOkay || false,
  151. stopOnPartial: stopOnPartial || false,
  152. version,
  153. flags,
  154. reader: payloadReader,
  155. size,
  156. start: start + absStart,
  157. has64BitSize,
  158. };
  159. boxDefinition(box);
  160. } else {
  161. // Move the read head to be at the end of the box.
  162. // If the box is longer than the remaining parts of the file, e.g. the
  163. // mp4 is improperly formatted, or this was a partial range request that
  164. // ended in the middle of a box, just skip to the end.
  165. const skipLength = Math.min(
  166. start + size - reader.getPosition(),
  167. reader.getLength() - reader.getPosition());
  168. reader.skip(skipLength);
  169. }
  170. }
  171. /**
  172. * A callback that tells the Mp4 parser to treat the body of a box as a series
  173. * of boxes. The number of boxes is limited by the size of the parent box.
  174. *
  175. * @param {!shaka.extern.ParsedBox} box
  176. * @export
  177. */
  178. static children(box) {
  179. // The "reader" starts at the payload, so we need to add the header to the
  180. // start position. The header size varies.
  181. const headerSize = shaka.util.Mp4Parser.headerSize(box);
  182. while (box.reader.hasMoreData() && !box.parser.done_) {
  183. box.parser.parseNext(box.start + headerSize, box.reader, box.partialOkay,
  184. box.stopOnPartial);
  185. }
  186. }
  187. /**
  188. * A callback that tells the Mp4 parser to treat the body of a box as a sample
  189. * description. A sample description box has a fixed number of children. The
  190. * number of children is represented by a 4 byte unsigned integer. Each child
  191. * is a box.
  192. *
  193. * @param {!shaka.extern.ParsedBox} box
  194. * @export
  195. */
  196. static sampleDescription(box) {
  197. // The "reader" starts at the payload, so we need to add the header to the
  198. // start position. The header size varies.
  199. const headerSize = shaka.util.Mp4Parser.headerSize(box);
  200. const count = box.reader.readUint32();
  201. for (let i = 0; i < count; i++) {
  202. box.parser.parseNext(box.start + headerSize, box.reader, box.partialOkay,
  203. box.stopOnPartial);
  204. if (box.parser.done_) {
  205. break;
  206. }
  207. }
  208. }
  209. /**
  210. * A callback that tells the Mp4 parser to treat the body of a box as a visual
  211. * sample entry. A visual sample entry has some fixed-sized fields
  212. * describing the video codec parameters, followed by an arbitrary number of
  213. * appended children. Each child is a box.
  214. *
  215. * @param {!shaka.extern.ParsedBox} box
  216. * @export
  217. */
  218. static visualSampleEntry(box) {
  219. // The "reader" starts at the payload, so we need to add the header to the
  220. // start position. The header size varies.
  221. const headerSize = shaka.util.Mp4Parser.headerSize(box);
  222. // Skip 6 reserved bytes.
  223. // Skip 2-byte data reference index.
  224. // Skip 16 more reserved bytes.
  225. // Skip 4 bytes for width/height.
  226. // Skip 8 bytes for horizontal/vertical resolution.
  227. // Skip 4 more reserved bytes (0)
  228. // Skip 2-byte frame count.
  229. // Skip 32-byte compressor name (length byte, then name, then 0-padding).
  230. // Skip 2-byte depth.
  231. // Skip 2 more reserved bytes (0xff)
  232. // 78 bytes total.
  233. // See also https://github.com/shaka-project/shaka-packager/blob/d5ca6e84/packager/media/formats/mp4/box_definitions.cc#L1544
  234. box.reader.skip(78);
  235. while (box.reader.hasMoreData() && !box.parser.done_) {
  236. box.parser.parseNext(box.start + headerSize, box.reader, box.partialOkay,
  237. box.stopOnPartial);
  238. }
  239. }
  240. /**
  241. * A callback that tells the Mp4 parser to treat the body of a box as a audio
  242. * sample entry. A audio sample entry has some fixed-sized fields
  243. * describing the audio codec parameters, followed by an arbitrary number of
  244. * appended children. Each child is a box.
  245. *
  246. * @param {!shaka.extern.ParsedBox} box
  247. * @export
  248. */
  249. static audioSampleEntry(box) {
  250. // The "reader" starts at the payload, so we need to add the header to the
  251. // start position. The header size varies.
  252. const headerSize = shaka.util.Mp4Parser.headerSize(box);
  253. // 6 bytes reserved
  254. // 2 bytes data reference index
  255. box.reader.skip(8);
  256. // 2 bytes version
  257. const version = box.reader.readUint16();
  258. // 2 bytes revision (0, could be ignored)
  259. // 4 bytes reserved
  260. box.reader.skip(6);
  261. if (version == 2) {
  262. // 16 bytes hard-coded values with no comments
  263. // 8 bytes sample rate
  264. // 4 bytes channel count
  265. // 4 bytes hard-coded values with no comments
  266. // 4 bytes bits per sample
  267. // 4 bytes lpcm flags
  268. // 4 bytes sample size
  269. // 4 bytes samples per packet
  270. box.reader.skip(48);
  271. } else {
  272. // 2 bytes channel count
  273. // 2 bytes bits per sample
  274. // 2 bytes compression ID
  275. // 2 bytes packet size
  276. // 2 bytes sample rate
  277. // 2 byte reserved
  278. box.reader.skip(12);
  279. }
  280. if (version == 1) {
  281. // 4 bytes samples per packet
  282. // 4 bytes bytes per packet
  283. // 4 bytes bytes per frame
  284. // 4 bytes bytes per sample
  285. box.reader.skip(16);
  286. }
  287. while (box.reader.hasMoreData() && !box.parser.done_) {
  288. box.parser.parseNext(box.start + headerSize, box.reader, box.partialOkay,
  289. box.stopOnPartial);
  290. }
  291. }
  292. /**
  293. * Create a callback that tells the Mp4 parser to treat the body of a box as a
  294. * binary blob and to parse the body's contents using the provided callback.
  295. *
  296. * @param {function(!Uint8Array)} callback
  297. * @return {!shaka.util.Mp4Parser.CallbackType}
  298. * @export
  299. */
  300. static allData(callback) {
  301. return (box) => {
  302. const all = box.reader.getLength() - box.reader.getPosition();
  303. callback(box.reader.readBytes(all));
  304. };
  305. }
  306. /**
  307. * Convert an ascii string name to the integer type for a box.
  308. *
  309. * @param {string} name The name of the box. The name must be four
  310. * characters long.
  311. * @return {number}
  312. * @private
  313. */
  314. static typeFromString_(name) {
  315. goog.asserts.assert(
  316. name.length == 4,
  317. 'Mp4 box names must be 4 characters long');
  318. let code = 0;
  319. for (const chr of name) {
  320. code = (code << 8) | chr.charCodeAt(0);
  321. }
  322. return code;
  323. }
  324. /**
  325. * Convert an integer type from a box into an ascii string name.
  326. * Useful for debugging.
  327. *
  328. * @param {number} type The type of the box, a uint32.
  329. * @return {string}
  330. * @export
  331. */
  332. static typeToString(type) {
  333. const name = String.fromCharCode(
  334. (type >> 24) & 0xff,
  335. (type >> 16) & 0xff,
  336. (type >> 8) & 0xff,
  337. type & 0xff);
  338. return name;
  339. }
  340. /**
  341. * Find the header size of the box.
  342. * Useful for modifying boxes in place or finding the exact offset of a field.
  343. *
  344. * @param {shaka.extern.ParsedBox} box
  345. * @return {number}
  346. * @export
  347. */
  348. static headerSize(box) {
  349. const basicHeaderSize = 8;
  350. const _64BitFieldSize = box.has64BitSize ? 8 : 0;
  351. const versionAndFlagsSize = box.flags != null ? 4 : 0;
  352. return basicHeaderSize + _64BitFieldSize + versionAndFlagsSize;
  353. }
  354. };
  355. /**
  356. * @typedef {function(!shaka.extern.ParsedBox)}
  357. * @exportInterface
  358. */
  359. shaka.util.Mp4Parser.CallbackType;
  360. /**
  361. * An enum used to track the type of box so that the correct values can be
  362. * read from the header.
  363. *
  364. * @enum {number}
  365. * @private
  366. */
  367. shaka.util.Mp4Parser.BoxType_ = {
  368. BASIC_BOX: 0,
  369. FULL_BOX: 1,
  370. };