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 {!Object.<number, shaka.util.Mp4Parser.BoxType_>} */
  17. this.headers_ = [];
  18. /** @private {!Object.<number, !shaka.util.Mp4Parser.CallbackType>} */
  19. this.boxDefinitions_ = [];
  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_[typeCode] = shaka.util.Mp4Parser.BoxType_.BASIC_BOX;
  34. this.boxDefinitions_[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_[typeCode] = shaka.util.Mp4Parser.BoxType_.FULL_BOX;
  48. this.boxDefinitions_[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_[type];
  118. if (boxDefinition) {
  119. let version = null;
  120. let flags = null;
  121. if (this.headers_[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. * Create a callback that tells the Mp4 parser to treat the body of a box as a
  238. * binary blob and to parse the body's contents using the provided callback.
  239. *
  240. * @param {function(!Uint8Array)} callback
  241. * @return {!shaka.util.Mp4Parser.CallbackType}
  242. * @export
  243. */
  244. static allData(callback) {
  245. return (box) => {
  246. const all = box.reader.getLength() - box.reader.getPosition();
  247. callback(box.reader.readBytes(all));
  248. };
  249. }
  250. /**
  251. * Convert an ascii string name to the integer type for a box.
  252. *
  253. * @param {string} name The name of the box. The name must be four
  254. * characters long.
  255. * @return {number}
  256. * @private
  257. */
  258. static typeFromString_(name) {
  259. goog.asserts.assert(
  260. name.length == 4,
  261. 'Mp4 box names must be 4 characters long');
  262. let code = 0;
  263. for (const chr of name) {
  264. code = (code << 8) | chr.charCodeAt(0);
  265. }
  266. return code;
  267. }
  268. /**
  269. * Convert an integer type from a box into an ascii string name.
  270. * Useful for debugging.
  271. *
  272. * @param {number} type The type of the box, a uint32.
  273. * @return {string}
  274. * @export
  275. */
  276. static typeToString(type) {
  277. const name = String.fromCharCode(
  278. (type >> 24) & 0xff,
  279. (type >> 16) & 0xff,
  280. (type >> 8) & 0xff,
  281. type & 0xff);
  282. return name;
  283. }
  284. /**
  285. * Find the header size of the box.
  286. * Useful for modifying boxes in place or finding the exact offset of a field.
  287. *
  288. * @param {shaka.extern.ParsedBox} box
  289. * @return {number}
  290. * @export
  291. */
  292. static headerSize(box) {
  293. const basicHeaderSize = 8;
  294. const _64BitFieldSize = box.has64BitSize ? 8 : 0;
  295. const versionAndFlagsSize = box.flags != null ? 4 : 0;
  296. return basicHeaderSize + _64BitFieldSize + versionAndFlagsSize;
  297. }
  298. };
  299. /**
  300. * @typedef {function(!shaka.extern.ParsedBox)}
  301. * @exportInterface
  302. */
  303. shaka.util.Mp4Parser.CallbackType;
  304. /**
  305. * An enum used to track the type of box so that the correct values can be
  306. * read from the header.
  307. *
  308. * @enum {number}
  309. * @private
  310. */
  311. shaka.util.Mp4Parser.BoxType_ = {
  312. BASIC_BOX: 0,
  313. FULL_BOX: 1,
  314. };