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. version,
  152. flags,
  153. reader: payloadReader,
  154. size,
  155. start: start + absStart,
  156. has64BitSize,
  157. };
  158. boxDefinition(box);
  159. } else {
  160. // Move the read head to be at the end of the box.
  161. // If the box is longer than the remaining parts of the file, e.g. the
  162. // mp4 is improperly formatted, or this was a partial range request that
  163. // ended in the middle of a box, just skip to the end.
  164. const skipLength = Math.min(
  165. start + size - reader.getPosition(),
  166. reader.getLength() - reader.getPosition());
  167. reader.skip(skipLength);
  168. }
  169. }
  170. /**
  171. * A callback that tells the Mp4 parser to treat the body of a box as a series
  172. * of boxes. The number of boxes is limited by the size of the parent box.
  173. *
  174. * @param {!shaka.extern.ParsedBox} box
  175. * @export
  176. */
  177. static children(box) {
  178. // The "reader" starts at the payload, so we need to add the header to the
  179. // start position. The header size varies.
  180. const headerSize = shaka.util.Mp4Parser.headerSize(box);
  181. while (box.reader.hasMoreData() && !box.parser.done_) {
  182. box.parser.parseNext(box.start + headerSize, box.reader, box.partialOkay);
  183. }
  184. }
  185. /**
  186. * A callback that tells the Mp4 parser to treat the body of a box as a sample
  187. * description. A sample description box has a fixed number of children. The
  188. * number of children is represented by a 4 byte unsigned integer. Each child
  189. * is a box.
  190. *
  191. * @param {!shaka.extern.ParsedBox} box
  192. * @export
  193. */
  194. static sampleDescription(box) {
  195. // The "reader" starts at the payload, so we need to add the header to the
  196. // start position. The header size varies.
  197. const headerSize = shaka.util.Mp4Parser.headerSize(box);
  198. const count = box.reader.readUint32();
  199. for (let i = 0; i < count; i++) {
  200. box.parser.parseNext(box.start + headerSize, box.reader, box.partialOkay);
  201. if (box.parser.done_) {
  202. break;
  203. }
  204. }
  205. }
  206. /**
  207. * A callback that tells the Mp4 parser to treat the body of a box as a visual
  208. * sample entry. A visual sample entry has some fixed-sized fields
  209. * describing the video codec parameters, followed by an arbitrary number of
  210. * appended children. Each child is a box.
  211. *
  212. * @param {!shaka.extern.ParsedBox} box
  213. * @export
  214. */
  215. static visualSampleEntry(box) {
  216. // The "reader" starts at the payload, so we need to add the header to the
  217. // start position. The header size varies.
  218. const headerSize = shaka.util.Mp4Parser.headerSize(box);
  219. // Skip 6 reserved bytes.
  220. // Skip 2-byte data reference index.
  221. // Skip 16 more reserved bytes.
  222. // Skip 4 bytes for width/height.
  223. // Skip 8 bytes for horizontal/vertical resolution.
  224. // Skip 4 more reserved bytes (0)
  225. // Skip 2-byte frame count.
  226. // Skip 32-byte compressor name (length byte, then name, then 0-padding).
  227. // Skip 2-byte depth.
  228. // Skip 2 more reserved bytes (0xff)
  229. // 78 bytes total.
  230. // See also https://github.com/shaka-project/shaka-packager/blob/d5ca6e84/packager/media/formats/mp4/box_definitions.cc#L1544
  231. box.reader.skip(78);
  232. while (box.reader.hasMoreData() && !box.parser.done_) {
  233. box.parser.parseNext(box.start + headerSize, box.reader, box.partialOkay);
  234. }
  235. }
  236. /**
  237. * A callback that tells the Mp4 parser to treat the body of a box as a audio
  238. * sample entry. A audio sample entry has some fixed-sized fields
  239. * describing the audio codec parameters, followed by an arbitrary number of
  240. * appended children. Each child is a box.
  241. *
  242. * @param {!shaka.extern.ParsedBox} box
  243. * @export
  244. */
  245. static audioSampleEntry(box) {
  246. // The "reader" starts at the payload, so we need to add the header to the
  247. // start position. The header size varies.
  248. const headerSize = shaka.util.Mp4Parser.headerSize(box);
  249. // 6 bytes reserved
  250. // 2 bytes data reference index
  251. box.reader.skip(8);
  252. // 2 bytes version
  253. const version = box.reader.readUint16();
  254. // 2 bytes revision (0, could be ignored)
  255. // 4 bytes reserved
  256. box.reader.skip(6);
  257. if (version == 2) {
  258. // 16 bytes hard-coded values with no comments
  259. // 8 bytes sample rate
  260. // 4 bytes channel count
  261. // 4 bytes hard-coded values with no comments
  262. // 4 bytes bits per sample
  263. // 4 bytes lpcm flags
  264. // 4 bytes sample size
  265. // 4 bytes samples per packet
  266. box.reader.skip(48);
  267. } else {
  268. // 2 bytes channel count
  269. // 2 bytes bits per sample
  270. // 2 bytes compression ID
  271. // 2 bytes packet size
  272. // 2 bytes sample rate
  273. // 2 byte reserved
  274. box.reader.skip(12);
  275. }
  276. if (version == 1) {
  277. // 4 bytes samples per packet
  278. // 4 bytes bytes per packet
  279. // 4 bytes bytes per frame
  280. // 4 bytes bytes per sample
  281. box.reader.skip(16);
  282. }
  283. while (box.reader.hasMoreData() && !box.parser.done_) {
  284. box.parser.parseNext(box.start + headerSize, box.reader, box.partialOkay);
  285. }
  286. }
  287. /**
  288. * Create a callback that tells the Mp4 parser to treat the body of a box as a
  289. * binary blob and to parse the body's contents using the provided callback.
  290. *
  291. * @param {function(!Uint8Array)} callback
  292. * @return {!shaka.util.Mp4Parser.CallbackType}
  293. * @export
  294. */
  295. static allData(callback) {
  296. return (box) => {
  297. const all = box.reader.getLength() - box.reader.getPosition();
  298. callback(box.reader.readBytes(all));
  299. };
  300. }
  301. /**
  302. * Convert an ascii string name to the integer type for a box.
  303. *
  304. * @param {string} name The name of the box. The name must be four
  305. * characters long.
  306. * @return {number}
  307. * @private
  308. */
  309. static typeFromString_(name) {
  310. goog.asserts.assert(
  311. name.length == 4,
  312. 'Mp4 box names must be 4 characters long');
  313. let code = 0;
  314. for (const chr of name) {
  315. code = (code << 8) | chr.charCodeAt(0);
  316. }
  317. return code;
  318. }
  319. /**
  320. * Convert an integer type from a box into an ascii string name.
  321. * Useful for debugging.
  322. *
  323. * @param {number} type The type of the box, a uint32.
  324. * @return {string}
  325. * @export
  326. */
  327. static typeToString(type) {
  328. const name = String.fromCharCode(
  329. (type >> 24) & 0xff,
  330. (type >> 16) & 0xff,
  331. (type >> 8) & 0xff,
  332. type & 0xff);
  333. return name;
  334. }
  335. /**
  336. * Find the header size of the box.
  337. * Useful for modifying boxes in place or finding the exact offset of a field.
  338. *
  339. * @param {shaka.extern.ParsedBox} box
  340. * @return {number}
  341. * @export
  342. */
  343. static headerSize(box) {
  344. const basicHeaderSize = 8;
  345. const _64BitFieldSize = box.has64BitSize ? 8 : 0;
  346. const versionAndFlagsSize = box.flags != null ? 4 : 0;
  347. return basicHeaderSize + _64BitFieldSize + versionAndFlagsSize;
  348. }
  349. };
  350. /**
  351. * @typedef {function(!shaka.extern.ParsedBox)}
  352. * @exportInterface
  353. */
  354. shaka.util.Mp4Parser.CallbackType;
  355. /**
  356. * An enum used to track the type of box so that the correct values can be
  357. * read from the header.
  358. *
  359. * @enum {number}
  360. * @private
  361. */
  362. shaka.util.Mp4Parser.BoxType_ = {
  363. BASIC_BOX: 0,
  364. FULL_BOX: 1,
  365. };