Source: lib/transmuxer/mss_transmuxer.js

  1. /*! @license
  2. * MSS Transmuxer
  3. * Copyright 2015 Dash Industry Forum
  4. * SPDX-License-Identifier: BSD-3-Clause
  5. */
  6. /*
  7. * Redistribution and use in source and binary forms, with or without
  8. * modification, are permitted provided that the following conditions are met:
  9. *
  10. * - Redistributions of source code must retain the above copyright notice,
  11. * this list of conditions and the following disclaimer.
  12. * - Redistributions in binary form must reproduce the above copyright notice,
  13. * this list of conditions and the following disclaimer in the documentation
  14. * and/or other materials provided with the distribution.
  15. * - Neither the name of the Dash Industry Forum nor the names of its
  16. * contributors may be used to endorse or promote products derived from this
  17. * software without specific prior written permission.
  18. *
  19. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS”
  20. * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  21. * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  22. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
  23. * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  24. * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  25. * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  26. * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  27. * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  28. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  29. * POSSIBILITY OF SUCH DAMAGE.
  30. */
  31. goog.provide('shaka.transmuxer.MssTransmuxer');
  32. goog.require('shaka.media.Capabilities');
  33. goog.require('shaka.transmuxer.TransmuxerEngine');
  34. goog.require('shaka.util.BufferUtils');
  35. goog.require('shaka.util.Error');
  36. goog.require('shaka.util.ManifestParserUtils');
  37. goog.require('shaka.dependencies');
  38. goog.requireType('shaka.media.SegmentReference');
  39. /**
  40. * @implements {shaka.extern.Transmuxer}
  41. * @export
  42. */
  43. shaka.transmuxer.MssTransmuxer = class {
  44. /**
  45. * @param {string} mimeType
  46. */
  47. constructor(mimeType) {
  48. /** @private {string} */
  49. this.originalMimeType_ = mimeType;
  50. /** @private {?ISOBoxer} */
  51. this.isoBoxer_ = shaka.dependencies.isoBoxer();
  52. if (this.isoBoxer_) {
  53. this.addSpecificBoxProcessor_();
  54. }
  55. }
  56. /**
  57. * Add specific box processor for codem-isoboxer
  58. *
  59. * @private
  60. */
  61. addSpecificBoxProcessor_() {
  62. // eslint-disable-next-line no-restricted-syntax
  63. this.isoBoxer_.addBoxProcessor('saio', function() {
  64. // eslint-disable-next-line no-invalid-this
  65. const box = /** @type {!ISOBox} */(this);
  66. box._procFullBox();
  67. if (box.flags & 1) {
  68. box._procField('aux_info_type', 'uint', 32);
  69. box._procField('aux_info_type_parameter', 'uint', 32);
  70. }
  71. box._procField('entry_count', 'uint', 32);
  72. box._procFieldArray('offset', box.entry_count, 'uint',
  73. (box.version === 1) ? 64 : 32);
  74. });
  75. // eslint-disable-next-line no-restricted-syntax
  76. this.isoBoxer_.addBoxProcessor('saiz', function() {
  77. // eslint-disable-next-line no-invalid-this
  78. const box = /** @type {!ISOBox} */(this);
  79. box._procFullBox();
  80. if (box.flags & 1) {
  81. box._procField('aux_info_type', 'uint', 32);
  82. box._procField('aux_info_type_parameter', 'uint', 32);
  83. }
  84. box._procField('default_sample_info_size', 'uint', 8);
  85. box._procField('sample_count', 'uint', 32);
  86. if (box.default_sample_info_size === 0) {
  87. box._procFieldArray('sample_info_size',
  88. box.sample_count, 'uint', 8);
  89. }
  90. });
  91. // eslint-disable-next-line no-restricted-syntax
  92. const sencProcessor = function() {
  93. // eslint-disable-next-line no-invalid-this
  94. const box = /** @type {!ISOBox} */(this);
  95. box._procFullBox();
  96. if (box.flags & 1) {
  97. box._procField('AlgorithmID', 'uint', 24);
  98. box._procField('IV_size', 'uint', 8);
  99. box._procFieldArray('KID', 16, 'uint', 8);
  100. }
  101. box._procField('sample_count', 'uint', 32);
  102. // eslint-disable-next-line no-restricted-syntax
  103. box._procEntries('entry', box.sample_count, function(entry) {
  104. // eslint-disable-next-line no-invalid-this
  105. const boxEntry = /** @type {!ISOBox} */(this);
  106. boxEntry._procEntryField(entry, 'InitializationVector', 'data', 8);
  107. if (boxEntry.flags & 2) {
  108. boxEntry._procEntryField(entry, 'NumberOfEntries', 'uint', 16);
  109. boxEntry._procSubEntries(entry, 'clearAndCryptedData',
  110. // eslint-disable-next-line no-restricted-syntax
  111. entry.NumberOfEntries, function(clearAndCryptedData) {
  112. // eslint-disable-next-line no-invalid-this
  113. const subBoxEntry = /** @type {!ISOBox} */(this);
  114. subBoxEntry._procEntryField(clearAndCryptedData,
  115. 'BytesOfClearData', 'uint', 16);
  116. subBoxEntry._procEntryField(clearAndCryptedData,
  117. 'BytesOfEncryptedData', 'uint', 32);
  118. });
  119. }
  120. });
  121. };
  122. this.isoBoxer_.addBoxProcessor('senc', sencProcessor);
  123. // eslint-disable-next-line no-restricted-syntax
  124. this.isoBoxer_.addBoxProcessor('uuid', function() {
  125. const MssTransmuxer = shaka.transmuxer.MssTransmuxer;
  126. // eslint-disable-next-line no-invalid-this
  127. const box = /** @type {!ISOBox} */(this);
  128. let isSENC = true;
  129. for (let i = 0; i < 16; i++) {
  130. if (box.usertype[i] !== MssTransmuxer.UUID_SENC_[i]) {
  131. isSENC = false;
  132. }
  133. // Add support for other user types here
  134. }
  135. if (isSENC) {
  136. if (box._parsing) {
  137. // Convert this box to sepiff for later processing.
  138. // See processMediaSegment_ function.
  139. box.type = 'sepiff';
  140. }
  141. // eslint-disable-next-line no-restricted-syntax, no-invalid-this
  142. sencProcessor.call(/** @type {!ISOBox} */(this));
  143. }
  144. });
  145. }
  146. /**
  147. * @override
  148. * @export
  149. */
  150. destroy() {
  151. // Nothing
  152. }
  153. /**
  154. * Check if the mime type and the content type is supported.
  155. * @param {string} mimeType
  156. * @param {string=} contentType
  157. * @return {boolean}
  158. * @override
  159. * @export
  160. */
  161. isSupported(mimeType, contentType) {
  162. const Capabilities = shaka.media.Capabilities;
  163. const isMss = mimeType.startsWith('mss/');
  164. if (!this.isoBoxer_ || !isMss) {
  165. return false;
  166. }
  167. if (contentType) {
  168. return Capabilities.isTypeSupported(
  169. this.convertCodecs(contentType, mimeType));
  170. }
  171. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  172. const audioMime = this.convertCodecs(ContentType.AUDIO, mimeType);
  173. const videoMime = this.convertCodecs(ContentType.VIDEO, mimeType);
  174. return Capabilities.isTypeSupported(audioMime) ||
  175. Capabilities.isTypeSupported(videoMime);
  176. }
  177. /**
  178. * @override
  179. * @export
  180. */
  181. convertCodecs(contentType, mimeType) {
  182. return mimeType.replace('mss/', '');
  183. }
  184. /**
  185. * @override
  186. * @export
  187. */
  188. getOriginalMimeType() {
  189. return this.originalMimeType_;
  190. }
  191. /**
  192. * @override
  193. * @export
  194. */
  195. transmux(data, stream, reference) {
  196. if (!reference) {
  197. // Init segment doesn't need transmux
  198. return Promise.resolve(shaka.util.BufferUtils.toUint8(data));
  199. }
  200. if (!stream.mssPrivateData) {
  201. return Promise.reject(new shaka.util.Error(
  202. shaka.util.Error.Severity.CRITICAL,
  203. shaka.util.Error.Category.MEDIA,
  204. shaka.util.Error.Code.MSS_MISSING_DATA_FOR_TRANSMUXING));
  205. }
  206. try {
  207. const transmuxedData = this.processMediaSegment_(
  208. data, stream, reference);
  209. return Promise.resolve(transmuxedData);
  210. } catch (exception) {
  211. if (exception instanceof shaka.util.Error) {
  212. return Promise.reject(exception);
  213. }
  214. return Promise.reject(new shaka.util.Error(
  215. shaka.util.Error.Severity.CRITICAL,
  216. shaka.util.Error.Category.MEDIA,
  217. shaka.util.Error.Code.MSS_TRANSMUXING_FAILED));
  218. }
  219. }
  220. /**
  221. * Process a media segment from a data and stream.
  222. * @param {BufferSource} data
  223. * @param {shaka.extern.Stream} stream
  224. * @param {shaka.media.SegmentReference} reference
  225. * @return {!Uint8Array}
  226. * @private
  227. */
  228. processMediaSegment_(data, stream, reference) {
  229. let i;
  230. /** @type {!ISOFile} */
  231. const isoFile = this.isoBoxer_.parseBuffer(data);
  232. // Update track_Id in tfhd box
  233. const tfhd = isoFile.fetch('tfhd');
  234. tfhd.track_ID = stream.id + 1;
  235. // Add tfdt box
  236. let tfdt = isoFile.fetch('tfdt');
  237. const traf = isoFile.fetch('traf');
  238. if (tfdt === null) {
  239. tfdt = this.isoBoxer_.createFullBox('tfdt', traf, tfhd);
  240. tfdt.version = 1;
  241. tfdt.flags = 0;
  242. const timescale = stream.mssPrivateData.timescale;
  243. const startTime = reference.startTime;
  244. tfdt.baseMediaDecodeTime = Math.floor(startTime * timescale);
  245. }
  246. const trun = isoFile.fetch('trun');
  247. // Process tfxd boxes
  248. // This box provide absolute timestamp but we take the segment start
  249. // time for tfdt
  250. let tfxd = isoFile.fetch('tfxd');
  251. if (tfxd) {
  252. tfxd._parent.boxes.splice(tfxd._parent.boxes.indexOf(tfxd), 1);
  253. tfxd = null;
  254. }
  255. let tfrf = isoFile.fetch('tfrf');
  256. if (tfrf) {
  257. tfrf._parent.boxes.splice(tfrf._parent.boxes.indexOf(tfrf), 1);
  258. tfrf = null;
  259. }
  260. // If protected content in PIFF1.1 format
  261. // (sepiff box = Sample Encryption PIFF)
  262. // => convert sepiff box it into a senc box
  263. // => create saio and saiz boxes (if not already present)
  264. const sepiff = isoFile.fetch('sepiff');
  265. if (sepiff !== null) {
  266. sepiff.type = 'senc';
  267. sepiff.usertype = undefined;
  268. let saio = isoFile.fetch('saio');
  269. if (saio === null) {
  270. // Create Sample Auxiliary Information Offsets Box box (saio)
  271. saio = this.isoBoxer_.createFullBox('saio', traf);
  272. saio.version = 0;
  273. saio.flags = 0;
  274. saio.entry_count = 1;
  275. saio.offset = [0];
  276. const saiz = this.isoBoxer_.createFullBox('saiz', traf);
  277. saiz.version = 0;
  278. saiz.flags = 0;
  279. saiz.sample_count = sepiff.sample_count;
  280. saiz.default_sample_info_size = 0;
  281. saiz.sample_info_size = [];
  282. if (sepiff.flags & 0x02) {
  283. // Sub-sample encryption => set sample_info_size for each sample
  284. for (i = 0; i < sepiff.sample_count; i += 1) {
  285. // 10 = 8 (InitializationVector field size) + 2
  286. // (subsample_count field size)
  287. // 6 = 2 (BytesOfClearData field size) + 4
  288. // (BytesOfEncryptedData field size)
  289. const entry = /** @type {!ISOEntry} */ (sepiff.entry[i]);
  290. saiz.sample_info_size[i] = 10 + (6 * entry.NumberOfEntries);
  291. }
  292. } else {
  293. // No sub-sample encryption => set default
  294. // sample_info_size = InitializationVector field size (8)
  295. saiz.default_sample_info_size = 8;
  296. }
  297. }
  298. }
  299. // set tfhd.base-data-offset-present to false
  300. tfhd.flags &= 0xFFFFFE;
  301. // set tfhd.default-base-is-moof to true
  302. tfhd.flags |= 0x020000;
  303. // set trun.data-offset-present to true
  304. trun.flags |= 0x000001;
  305. // Update trun.data_offset field that corresponds to first data byte
  306. // (inside mdat box)
  307. const moof = isoFile.fetch('moof');
  308. const length = moof.getLength();
  309. trun.data_offset = length + 8;
  310. // Update saio box offset field according to new senc box offset
  311. const saio = isoFile.fetch('saio');
  312. if (saio !== null) {
  313. const trafPosInMoof = this.getBoxOffset_(moof, 'traf');
  314. const sencPosInTraf = this.getBoxOffset_(traf, 'senc');
  315. // Set offset from begin fragment to the first IV field in senc box
  316. // 16 = box header (12) + sample_count field size (4)
  317. saio.offset[0] = trafPosInMoof + sencPosInTraf + 16;
  318. }
  319. return shaka.util.BufferUtils.toUint8(isoFile.write());
  320. }
  321. /**
  322. * This function returns the offset of the 1st byte of a child box within
  323. * a container box.
  324. *
  325. * @param {ISOBox} parent
  326. * @param {string} type
  327. * @return {number}
  328. * @private
  329. */
  330. getBoxOffset_(parent, type) {
  331. let offset = 8;
  332. for (let i = 0; i < parent.boxes.length; i++) {
  333. if (parent.boxes[i].type === type) {
  334. return offset;
  335. }
  336. offset += parent.boxes[i].size;
  337. }
  338. return offset;
  339. }
  340. };
  341. /**
  342. * @private {!Uint8Array}
  343. */
  344. shaka.transmuxer.MssTransmuxer.UUID_SENC_ = new Uint8Array([
  345. 0xA2, 0x39, 0x4F, 0x52, 0x5A, 0x9B, 0x4F, 0x14,
  346. 0xA2, 0x44, 0x6C, 0x42, 0x7C, 0x64, 0x8D, 0xF4,
  347. ]);
  348. shaka.transmuxer.TransmuxerEngine.registerTransmuxer(
  349. 'mss/audio/mp4',
  350. () => new shaka.transmuxer.MssTransmuxer('mss/audio/mp4'),
  351. shaka.transmuxer.TransmuxerEngine.PluginPriority.FALLBACK);
  352. shaka.transmuxer.TransmuxerEngine.registerTransmuxer(
  353. 'mss/video/mp4',
  354. () => new shaka.transmuxer.MssTransmuxer('mss/video/mp4'),
  355. shaka.transmuxer.TransmuxerEngine.PluginPriority.FALLBACK);