Source: lib/media/transmuxer.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.media.Transmuxer');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.util.BufferUtils');
  9. goog.require('shaka.util.Error');
  10. goog.require('shaka.util.IDestroyable');
  11. goog.require('shaka.util.ManifestParserUtils');
  12. goog.require('shaka.util.PublicPromise');
  13. goog.require('shaka.util.Uint8ArrayUtils');
  14. goog.require('shaka.dependencies');
  15. /**
  16. * Transmuxer provides all operations for transmuxing from Transport
  17. * Stream to MP4.
  18. *
  19. * @implements {shaka.util.IDestroyable}
  20. */
  21. shaka.media.Transmuxer = class {
  22. /** */
  23. constructor() {
  24. /** @private {?muxjs} */
  25. this.muxjs_ = shaka.dependencies.muxjs();
  26. /** @private {muxjs.mp4.Transmuxer} */
  27. this.muxTransmuxer_ = null;
  28. /** @private {shaka.util.PublicPromise} */
  29. this.transmuxPromise_ = null;
  30. /** @private {!Array.<!Uint8Array>} */
  31. this.transmuxedData_ = [];
  32. /** @private {!Array.<muxjs.mp4.ClosedCaption>} */
  33. this.captions_ = [];
  34. /** @private {!Array.<muxjs.mp4.Metadata>} */
  35. this.metadata_ = [];
  36. /** @private {boolean} */
  37. this.isTransmuxing_ = false;
  38. if (this.muxjs_) {
  39. this.muxTransmuxer_ = new this.muxjs_.mp4.Transmuxer({
  40. 'keepOriginalTimestamps': true,
  41. });
  42. this.muxTransmuxer_.on('data', (segment) => this.onTransmuxed_(segment));
  43. this.muxTransmuxer_.on('done', () => this.onTransmuxDone_());
  44. }
  45. }
  46. /**
  47. * @override
  48. */
  49. destroy() {
  50. if (this.muxTransmuxer_) {
  51. this.muxTransmuxer_.dispose();
  52. }
  53. this.muxTransmuxer_ = null;
  54. return Promise.resolve();
  55. }
  56. /**
  57. * Check if the content type is Transport Stream, and if muxjs is loaded.
  58. * @param {string} mimeType
  59. * @param {string=} contentType
  60. * @return {boolean}
  61. */
  62. static isSupported(mimeType, contentType) {
  63. const Transmuxer = shaka.media.Transmuxer;
  64. if (!shaka.dependencies.muxjs() || !Transmuxer.isTsContainer(mimeType)) {
  65. return false;
  66. }
  67. if (contentType) {
  68. return MediaSource.isTypeSupported(
  69. Transmuxer.convertTsCodecs(contentType, mimeType));
  70. }
  71. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  72. const audioMime = Transmuxer.convertTsCodecs(ContentType.AUDIO, mimeType);
  73. const videoMime = Transmuxer.convertTsCodecs(ContentType.VIDEO, mimeType);
  74. return MediaSource.isTypeSupported(audioMime) ||
  75. MediaSource.isTypeSupported(videoMime);
  76. }
  77. /**
  78. * Check if the mimetype contains 'mp2t'.
  79. * @param {string} mimeType
  80. * @return {boolean}
  81. */
  82. static isTsContainer(mimeType) {
  83. return mimeType.toLowerCase().split(';')[0].split('/')[1] == 'mp2t';
  84. }
  85. /**
  86. * For transport stream, convert its codecs to MP4 codecs.
  87. * @param {string} contentType
  88. * @param {string} tsMimeType
  89. * @return {string}
  90. */
  91. static convertTsCodecs(contentType, tsMimeType) {
  92. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  93. let mp4MimeType = tsMimeType.replace(/mp2t/i, 'mp4');
  94. if (contentType == ContentType.AUDIO) {
  95. mp4MimeType = mp4MimeType.replace('video', 'audio');
  96. }
  97. // Handle legacy AVC1 codec strings (pre-RFC 6381).
  98. // Look for "avc1.<profile>.<level>", where profile is:
  99. // 66 (baseline => 0x42)
  100. // 77 (main => 0x4d)
  101. // 100 (high => 0x64)
  102. // Reference: https://bit.ly/2K9JI3x
  103. const match = /avc1\.(66|77|100)\.(\d+)/.exec(mp4MimeType);
  104. if (match) {
  105. let newCodecString = 'avc1.';
  106. const profile = match[1];
  107. if (profile == '66') {
  108. newCodecString += '4200';
  109. } else if (profile == '77') {
  110. newCodecString += '4d00';
  111. } else {
  112. goog.asserts.assert(profile == '100',
  113. 'Legacy avc1 parsing code out of sync with regex!');
  114. newCodecString += '6400';
  115. }
  116. // Convert the level to hex and append to the codec string.
  117. const level = Number(match[2]);
  118. goog.asserts.assert(level < 256,
  119. 'Invalid legacy avc1 level number!');
  120. newCodecString += (level >> 4).toString(16);
  121. newCodecString += (level & 0xf).toString(16);
  122. mp4MimeType = mp4MimeType.replace(match[0], newCodecString);
  123. }
  124. return mp4MimeType;
  125. }
  126. /**
  127. * Transmux from Transport stream to MP4, using the mux.js library.
  128. * @param {BufferSource} data
  129. * @return {!Promise.<{data: !Uint8Array,
  130. * captions: !Array.<!muxjs.mp4.ClosedCaption>,
  131. * metadata: !Array.<!Object>}>}
  132. */
  133. transmux(data) {
  134. goog.asserts.assert(this.muxTransmuxer_,
  135. 'mux.js should be available.');
  136. goog.asserts.assert(!this.isTransmuxing_,
  137. 'No transmuxing should be in progress.');
  138. this.isTransmuxing_ = true;
  139. this.transmuxPromise_ = new shaka.util.PublicPromise();
  140. this.transmuxedData_ = [];
  141. this.captions_ = [];
  142. this.metadata_ = [];
  143. const dataArray = shaka.util.BufferUtils.toUint8(data);
  144. this.muxTransmuxer_.push(dataArray);
  145. this.muxTransmuxer_.flush();
  146. // Workaround for https://bit.ly/Shaka1449 mux.js not
  147. // emitting 'data' and 'done' events.
  148. // mux.js code is synchronous, so if onTransmuxDone_ has
  149. // not been called by now, it's not going to be.
  150. // Treat it as a transmuxing failure and reject the promise.
  151. if (this.isTransmuxing_) {
  152. this.transmuxPromise_.reject(new shaka.util.Error(
  153. shaka.util.Error.Severity.CRITICAL,
  154. shaka.util.Error.Category.MEDIA,
  155. shaka.util.Error.Code.TRANSMUXING_FAILED));
  156. }
  157. return this.transmuxPromise_;
  158. }
  159. /**
  160. * Handles the 'data' event of the transmuxer.
  161. * Extracts the cues from the transmuxed segment, and adds them to an array.
  162. * Stores the transmuxed data in another array, to pass it back to
  163. * MediaSourceEngine, and append to the source buffer.
  164. *
  165. * @param {muxjs.mp4.Transmuxer.Segment} segment
  166. * @private
  167. */
  168. onTransmuxed_(segment) {
  169. this.captions_ = segment.captions;
  170. this.metadata_ = segment.metadata;
  171. this.transmuxedData_.push(
  172. shaka.util.Uint8ArrayUtils.concat(segment.initSegment, segment.data));
  173. }
  174. /**
  175. * Handles the 'done' event of the transmuxer.
  176. * Resolves the transmux Promise, and returns the transmuxed data.
  177. * @private
  178. */
  179. onTransmuxDone_() {
  180. const output = {
  181. data: shaka.util.Uint8ArrayUtils.concat(...this.transmuxedData_),
  182. captions: this.captions_,
  183. metadata: this.metadata_,
  184. };
  185. this.transmuxPromise_.resolve(output);
  186. this.isTransmuxing_ = false;
  187. }
  188. };