Source: lib/util/mp4_parser.js

  1. /**
  2. * @license
  3. * Copyright 2016 Google Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. goog.provide('shaka.util.Mp4Parser');
  18. goog.require('goog.asserts');
  19. goog.require('shaka.log');
  20. goog.require('shaka.util.DataViewReader');
  21. /**
  22. * Create a new MP4 Parser
  23. * @struct
  24. * @constructor
  25. * @export
  26. */
  27. shaka.util.Mp4Parser = function() {
  28. /** @private {!Object.<number, shaka.util.Mp4Parser.BoxType_>} */
  29. this.headers_ = [];
  30. /** @private {!Object.<number, !shaka.util.Mp4Parser.CallbackType>} */
  31. this.boxDefinitions_ = [];
  32. /** @private {boolean} */
  33. this.done_ = false;
  34. };
  35. /**
  36. * @typedef {function(!shakaExtern.ParsedBox)}
  37. * @exportInterface
  38. */
  39. shaka.util.Mp4Parser.CallbackType;
  40. /**
  41. * An enum used to track the type of box so that the correct values can be
  42. * read from the header.
  43. *
  44. * @enum {number}
  45. * @private
  46. */
  47. shaka.util.Mp4Parser.BoxType_ = {
  48. BASIC_BOX: 0,
  49. FULL_BOX: 1
  50. };
  51. /**
  52. * Declare a box type as a Box.
  53. *
  54. * @param {string} type
  55. * @param {!shaka.util.Mp4Parser.CallbackType} definition
  56. * @return {!shaka.util.Mp4Parser}
  57. * @export
  58. */
  59. shaka.util.Mp4Parser.prototype.box = function(type, definition) {
  60. let typeCode = shaka.util.Mp4Parser.typeFromString_(type);
  61. this.headers_[typeCode] = shaka.util.Mp4Parser.BoxType_.BASIC_BOX;
  62. this.boxDefinitions_[typeCode] = definition;
  63. return this;
  64. };
  65. /**
  66. * Declare a box type as a Full Box.
  67. *
  68. * @param {string} type
  69. * @param {!shaka.util.Mp4Parser.CallbackType} definition
  70. * @return {!shaka.util.Mp4Parser}
  71. * @export
  72. */
  73. shaka.util.Mp4Parser.prototype.fullBox = function(type, definition) {
  74. let typeCode = shaka.util.Mp4Parser.typeFromString_(type);
  75. this.headers_[typeCode] = shaka.util.Mp4Parser.BoxType_.FULL_BOX;
  76. this.boxDefinitions_[typeCode] = definition;
  77. return this;
  78. };
  79. /**
  80. * Stop parsing. Useful for extracting information from partial segments and
  81. * avoiding an out-of-bounds error once you find what you are looking for.
  82. *
  83. * @export
  84. */
  85. shaka.util.Mp4Parser.prototype.stop = function() {
  86. this.done_ = true;
  87. };
  88. /**
  89. * Parse the given data using the added callbacks.
  90. *
  91. * @param {!BufferSource} data
  92. * @param {boolean=} opt_partialOkay If true, allow reading partial payloads
  93. * from some boxes. If the goal is a child box, we can sometimes find it
  94. * without enough data to find all child boxes.
  95. * @export
  96. */
  97. shaka.util.Mp4Parser.prototype.parse = function(data, opt_partialOkay) {
  98. let wrapped = new Uint8Array(data);
  99. let reader = new shaka.util.DataViewReader(
  100. new DataView(wrapped.buffer, wrapped.byteOffset, wrapped.byteLength),
  101. shaka.util.DataViewReader.Endianness.BIG_ENDIAN);
  102. this.done_ = false;
  103. while (reader.hasMoreData() && !this.done_) {
  104. this.parseNext(0, reader, opt_partialOkay);
  105. }
  106. };
  107. /**
  108. * Parse the next box on the current level.
  109. *
  110. * @param {number} absStart The absolute start position in the original
  111. * byte array.
  112. * @param {!shaka.util.DataViewReader} reader
  113. * @param {boolean=} opt_partialOkay If true, allow reading partial payloads
  114. * from some boxes. If the goal is a child box, we can sometimes find it
  115. * without enough data to find all child boxes.
  116. * @export
  117. */
  118. shaka.util.Mp4Parser.prototype.parseNext =
  119. function(absStart, reader, opt_partialOkay) {
  120. let start = reader.getPosition();
  121. let size = reader.readUint32();
  122. let type = reader.readUint32();
  123. let name = shaka.util.Mp4Parser.typeToString(type);
  124. shaka.log.v2('Parsing MP4 box', name);
  125. switch (size) {
  126. case 0:
  127. size = reader.getLength() - start;
  128. break;
  129. case 1:
  130. size = reader.readUint64();
  131. break;
  132. }
  133. let boxDefinition = this.boxDefinitions_[type];
  134. if (boxDefinition) {
  135. let version = null;
  136. let flags = null;
  137. if (this.headers_[type] == shaka.util.Mp4Parser.BoxType_.FULL_BOX) {
  138. let versionAndFlags = reader.readUint32();
  139. version = versionAndFlags >>> 24;
  140. flags = versionAndFlags & 0xFFFFFF;
  141. }
  142. // Read the whole payload so that the current level can be safely read
  143. // regardless of how the payload is parsed.
  144. let end = start + size;
  145. if (opt_partialOkay && end > reader.getLength()) {
  146. // For partial reads, truncate the payload if we must.
  147. end = reader.getLength();
  148. }
  149. let payloadSize = end - reader.getPosition();
  150. let payload =
  151. (payloadSize > 0) ? reader.readBytes(payloadSize) : new Uint8Array(0);
  152. let payloadReader = new shaka.util.DataViewReader(
  153. new DataView(payload.buffer, payload.byteOffset, payload.byteLength),
  154. shaka.util.DataViewReader.Endianness.BIG_ENDIAN);
  155. /** @type {shakaExtern.ParsedBox} */
  156. let box = {
  157. parser: this,
  158. partialOkay: opt_partialOkay || false,
  159. version: version,
  160. flags: flags,
  161. reader: payloadReader,
  162. size: size,
  163. start: start + absStart
  164. };
  165. boxDefinition(box);
  166. } else {
  167. // Move the read head to be at the end of the box.
  168. // If the box is longer than the remaining parts of the file, e.g. the
  169. // mp4 is improperly formatted, or this was a partial range request that
  170. // ended in the middle of a box, just skip to the end.
  171. const skipLength = Math.min(
  172. start + size - reader.getPosition(),
  173. reader.getLength() - reader.getPosition());
  174. reader.skip(skipLength);
  175. }
  176. };
  177. /**
  178. * A callback that tells the Mp4 parser to treat the body of a box as a series
  179. * of boxes. The number of boxes is limited by the size of the parent box.
  180. *
  181. * @param {!shakaExtern.ParsedBox} box
  182. * @export
  183. */
  184. shaka.util.Mp4Parser.children = function(box) {
  185. while (box.reader.hasMoreData() && !box.parser.done_) {
  186. box.parser.parseNext(box.start, box.reader, box.partialOkay);
  187. }
  188. };
  189. /**
  190. * A callback that tells the Mp4 parser to treat the body of a box as a sample
  191. * description. A sample description box has a fixed number of children. The
  192. * number of children is represented by a 4 byte unsigned integer. Each child
  193. * is a box.
  194. *
  195. * @param {!shakaExtern.ParsedBox} box
  196. * @export
  197. */
  198. shaka.util.Mp4Parser.sampleDescription = function(box) {
  199. for (let count = box.reader.readUint32();
  200. count > 0 && !box.parser.done_;
  201. count -= 1) {
  202. box.parser.parseNext(box.start, box.reader, box.partialOkay);
  203. }
  204. };
  205. /**
  206. * Create a callback that tells the Mp4 parser to treat the body of a box as a
  207. * binary blob and to parse the body's contents using the provided callback.
  208. *
  209. * @param {function(!Uint8Array)} callback
  210. * @return {!shaka.util.Mp4Parser.CallbackType}
  211. * @export
  212. */
  213. shaka.util.Mp4Parser.allData = function(callback) {
  214. return function(box) {
  215. let all = box.reader.getLength() - box.reader.getPosition();
  216. callback(box.reader.readBytes(all));
  217. };
  218. };
  219. /**
  220. * Convert an ascii string name to the integer type for a box.
  221. *
  222. * @param {string} name The name of the box. The name must be four
  223. * characters long.
  224. * @return {number}
  225. * @private
  226. */
  227. shaka.util.Mp4Parser.typeFromString_ = function(name) {
  228. goog.asserts.assert(
  229. name.length == 4,
  230. 'Mp4 box names must be 4 characters long');
  231. let code = 0;
  232. for (let i = 0; i < name.length; i++) {
  233. code = (code << 8) | name.charCodeAt(i);
  234. }
  235. return code;
  236. };
  237. /**
  238. * Convert an integer type from a box into an ascii string name.
  239. * Useful for debugging.
  240. *
  241. * @param {number} type The type of the box, a uint32.
  242. * @return {string}
  243. * @export
  244. */
  245. shaka.util.Mp4Parser.typeToString = function(type) {
  246. let name = String.fromCharCode(
  247. (type >> 24) & 0xff,
  248. (type >> 16) & 0xff,
  249. (type >> 8) & 0xff,
  250. type & 0xff);
  251. return name;
  252. };