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