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. // cspell:words crypted usertype
  32. goog.provide('shaka.transmuxer.MssTransmuxer');
  33. goog.require('shaka.media.Capabilities');
  34. goog.require('shaka.transmuxer.TransmuxerEngine');
  35. goog.require('shaka.util.BufferUtils');
  36. goog.require('shaka.util.Error');
  37. goog.require('shaka.util.ManifestParserUtils');
  38. goog.require('shaka.dependencies');
  39. goog.requireType('shaka.media.SegmentReference');
  40. /**
  41. * @implements {shaka.extern.Transmuxer}
  42. * @export
  43. */
  44. shaka.transmuxer.MssTransmuxer = class {
  45. /**
  46. * @param {string} mimeType
  47. */
  48. constructor(mimeType) {
  49. /** @private {string} */
  50. this.originalMimeType_ = mimeType;
  51. /** @private {?ISOBoxer} */
  52. this.isoBoxer_ = shaka.dependencies.isoBoxer();
  53. if (this.isoBoxer_) {
  54. this.addSpecificBoxProcessor_();
  55. }
  56. }
  57. /**
  58. * Add specific box processor for codem-isoboxer
  59. *
  60. * @private
  61. */
  62. addSpecificBoxProcessor_() {
  63. // eslint-disable-next-line no-restricted-syntax
  64. this.isoBoxer_.addBoxProcessor('saio', function() {
  65. // eslint-disable-next-line no-invalid-this
  66. const box = /** @type {!ISOBox} */(this);
  67. box._procFullBox();
  68. if (box.flags & 1) {
  69. box._procField('aux_info_type', 'uint', 32);
  70. box._procField('aux_info_type_parameter', 'uint', 32);
  71. }
  72. box._procField('entry_count', 'uint', 32);
  73. box._procFieldArray('offset', box.entry_count, 'uint',
  74. (box.version === 1) ? 64 : 32);
  75. });
  76. // eslint-disable-next-line no-restricted-syntax
  77. this.isoBoxer_.addBoxProcessor('saiz', function() {
  78. // eslint-disable-next-line no-invalid-this
  79. const box = /** @type {!ISOBox} */(this);
  80. box._procFullBox();
  81. if (box.flags & 1) {
  82. box._procField('aux_info_type', 'uint', 32);
  83. box._procField('aux_info_type_parameter', 'uint', 32);
  84. }
  85. box._procField('default_sample_info_size', 'uint', 8);
  86. box._procField('sample_count', 'uint', 32);
  87. if (box.default_sample_info_size === 0) {
  88. box._procFieldArray('sample_info_size',
  89. box.sample_count, 'uint', 8);
  90. }
  91. });
  92. // eslint-disable-next-line no-restricted-syntax
  93. const sencProcessor = function() {
  94. // eslint-disable-next-line no-invalid-this
  95. const box = /** @type {!ISOBox} */(this);
  96. box._procFullBox();
  97. if (box.flags & 1) {
  98. box._procField('AlgorithmID', 'uint', 24);
  99. box._procField('IV_size', 'uint', 8);
  100. box._procFieldArray('KID', 16, 'uint', 8);
  101. }
  102. box._procField('sample_count', 'uint', 32);
  103. // eslint-disable-next-line no-restricted-syntax
  104. box._procEntries('entry', box.sample_count, function(entry) {
  105. // eslint-disable-next-line no-invalid-this
  106. const boxEntry = /** @type {!ISOBox} */(this);
  107. boxEntry._procEntryField(entry, 'InitializationVector', 'data', 8);
  108. if (boxEntry.flags & 2) {
  109. boxEntry._procEntryField(entry, 'NumberOfEntries', 'uint', 16);
  110. boxEntry._procSubEntries(entry, 'clearAndCryptedData',
  111. // eslint-disable-next-line no-restricted-syntax
  112. entry.NumberOfEntries, function(clearAndCryptedData) {
  113. // eslint-disable-next-line no-invalid-this
  114. const subBoxEntry = /** @type {!ISOBox} */(this);
  115. subBoxEntry._procEntryField(clearAndCryptedData,
  116. 'BytesOfClearData', 'uint', 16);
  117. subBoxEntry._procEntryField(clearAndCryptedData,
  118. 'BytesOfEncryptedData', 'uint', 32);
  119. });
  120. }
  121. });
  122. };
  123. this.isoBoxer_.addBoxProcessor('senc', sencProcessor);
  124. // eslint-disable-next-line no-restricted-syntax
  125. this.isoBoxer_.addBoxProcessor('uuid', function() {
  126. const MssTransmuxer = shaka.transmuxer.MssTransmuxer;
  127. // eslint-disable-next-line no-invalid-this
  128. const box = /** @type {!ISOBox} */(this);
  129. let isSENC = true;
  130. for (let i = 0; i < 16; i++) {
  131. if (box.usertype[i] !== MssTransmuxer.UUID_SENC_[i]) {
  132. isSENC = false;
  133. }
  134. // Add support for other user types here
  135. }
  136. if (isSENC) {
  137. if (box._parsing) {
  138. // Convert this box to sepiff for later processing.
  139. // See processMediaSegment_ function.
  140. box.type = 'sepiff';
  141. }
  142. // eslint-disable-next-line no-restricted-syntax, no-invalid-this
  143. sencProcessor.call(/** @type {!ISOBox} */(this));
  144. }
  145. });
  146. }
  147. /**
  148. * @override
  149. * @export
  150. */
  151. destroy() {
  152. // Nothing
  153. }
  154. /**
  155. * Check if the mime type and the content type is supported.
  156. * @param {string} mimeType
  157. * @param {string=} contentType
  158. * @return {boolean}
  159. * @override
  160. * @export
  161. */
  162. isSupported(mimeType, contentType) {
  163. const Capabilities = shaka.media.Capabilities;
  164. const isMss = mimeType.startsWith('mss/');
  165. if (!this.isoBoxer_ || !isMss) {
  166. return false;
  167. }
  168. if (contentType) {
  169. return Capabilities.isTypeSupported(
  170. this.convertCodecs(contentType, mimeType));
  171. }
  172. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  173. const audioMime = this.convertCodecs(ContentType.AUDIO, mimeType);
  174. const videoMime = this.convertCodecs(ContentType.VIDEO, mimeType);
  175. return Capabilities.isTypeSupported(audioMime) ||
  176. Capabilities.isTypeSupported(videoMime);
  177. }
  178. /**
  179. * @override
  180. * @export
  181. */
  182. convertCodecs(contentType, mimeType) {
  183. return mimeType.replace('mss/', '');
  184. }
  185. /**
  186. * @override
  187. * @export
  188. */
  189. getOriginalMimeType() {
  190. return this.originalMimeType_;
  191. }
  192. /**
  193. * @override
  194. * @export
  195. */
  196. transmux(data, stream, reference) {
  197. if (!reference) {
  198. // Init segment doesn't need transmux
  199. return Promise.resolve(shaka.util.BufferUtils.toUint8(data));
  200. }
  201. if (!stream.mssPrivateData) {
  202. return Promise.reject(new shaka.util.Error(
  203. shaka.util.Error.Severity.CRITICAL,
  204. shaka.util.Error.Category.MEDIA,
  205. shaka.util.Error.Code.MSS_MISSING_DATA_FOR_TRANSMUXING,
  206. reference ? reference.getUris()[0] : null));
  207. }
  208. try {
  209. const transmuxedData = this.processMediaSegment_(
  210. data, stream, reference);
  211. return Promise.resolve(transmuxedData);
  212. } catch (exception) {
  213. if (exception instanceof shaka.util.Error) {
  214. return Promise.reject(exception);
  215. }
  216. return Promise.reject(new shaka.util.Error(
  217. shaka.util.Error.Severity.CRITICAL,
  218. shaka.util.Error.Category.MEDIA,
  219. shaka.util.Error.Code.MSS_TRANSMUXING_FAILED,
  220. reference ? reference.getUris()[0] : null));
  221. }
  222. }
  223. /**
  224. * Process a media segment from a data and stream.
  225. * @param {BufferSource} data
  226. * @param {shaka.extern.Stream} stream
  227. * @param {shaka.media.SegmentReference} reference
  228. * @return {!Uint8Array}
  229. * @private
  230. */
  231. processMediaSegment_(data, stream, reference) {
  232. let i;
  233. /** @type {!ISOFile} */
  234. const isoFile = this.isoBoxer_.parseBuffer(data);
  235. // Update track_Id in tfhd box
  236. const tfhd = isoFile.fetch('tfhd');
  237. tfhd.track_ID = stream.id + 1;
  238. // Add tfdt box
  239. let tfdt = isoFile.fetch('tfdt');
  240. const traf = isoFile.fetch('traf');
  241. if (tfdt === null) {
  242. tfdt = this.isoBoxer_.createFullBox('tfdt', traf, tfhd);
  243. tfdt.version = 1;
  244. tfdt.flags = 0;
  245. const timescale = stream.mssPrivateData.timescale;
  246. const startTime = reference.startTime;
  247. tfdt.baseMediaDecodeTime = Math.floor(startTime * timescale);
  248. }
  249. const trun = isoFile.fetch('trun');
  250. // Process tfxd boxes
  251. // This box provide absolute timestamp but we take the segment start
  252. // time for tfdt
  253. let tfxd = isoFile.fetch('tfxd');
  254. if (tfxd) {
  255. tfxd._parent.boxes.splice(tfxd._parent.boxes.indexOf(tfxd), 1);
  256. tfxd = null;
  257. }
  258. let tfrf = isoFile.fetch('tfrf');
  259. if (tfrf) {
  260. tfrf._parent.boxes.splice(tfrf._parent.boxes.indexOf(tfrf), 1);
  261. tfrf = null;
  262. }
  263. // If protected content in PIFF1.1 format
  264. // (sepiff box = Sample Encryption PIFF)
  265. // => convert sepiff box it into a senc box
  266. // => create saio and saiz boxes (if not already present)
  267. const sepiff = isoFile.fetch('sepiff');
  268. if (sepiff !== null) {
  269. sepiff.type = 'senc';
  270. sepiff.usertype = undefined;
  271. let saio = isoFile.fetch('saio');
  272. if (saio === null) {
  273. // Create Sample Auxiliary Information Offsets Box box (saio)
  274. saio = this.isoBoxer_.createFullBox('saio', traf);
  275. saio.version = 0;
  276. saio.flags = 0;
  277. saio.entry_count = 1;
  278. saio.offset = [0];
  279. const saiz = this.isoBoxer_.createFullBox('saiz', traf);
  280. saiz.version = 0;
  281. saiz.flags = 0;
  282. saiz.sample_count = sepiff.sample_count;
  283. saiz.default_sample_info_size = 0;
  284. saiz.sample_info_size = [];
  285. if (sepiff.flags & 0x02) {
  286. // Sub-sample encryption => set sample_info_size for each sample
  287. for (i = 0; i < sepiff.sample_count; i += 1) {
  288. // 10 = 8 (InitializationVector field size) + 2
  289. // (subsample_count field size)
  290. // 6 = 2 (BytesOfClearData field size) + 4
  291. // (BytesOfEncryptedData field size)
  292. const entry = /** @type {!ISOEntry} */ (sepiff.entry[i]);
  293. saiz.sample_info_size[i] = 10 + (6 * entry.NumberOfEntries);
  294. }
  295. } else {
  296. // No sub-sample encryption => set default
  297. // sample_info_size = InitializationVector field size (8)
  298. saiz.default_sample_info_size = 8;
  299. }
  300. }
  301. }
  302. // set tfhd.base-data-offset-present to false
  303. tfhd.flags &= 0xFFFFFE;
  304. // set tfhd.default-base-is-moof to true
  305. tfhd.flags |= 0x020000;
  306. // set trun.data-offset-present to true
  307. trun.flags |= 0x000001;
  308. // Update trun.data_offset field that corresponds to first data byte
  309. // (inside mdat box)
  310. const moof = isoFile.fetch('moof');
  311. const length = moof.getLength();
  312. trun.data_offset = length + 8;
  313. // Update saio box offset field according to new senc box offset
  314. const saio = isoFile.fetch('saio');
  315. if (saio !== null) {
  316. const trafPosInMoof = this.getBoxOffset_(moof, 'traf');
  317. const sencPosInTraf = this.getBoxOffset_(traf, 'senc');
  318. // Set offset from begin fragment to the first IV field in senc box
  319. // 16 = box header (12) + sample_count field size (4)
  320. saio.offset[0] = trafPosInMoof + sencPosInTraf + 16;
  321. }
  322. return shaka.util.BufferUtils.toUint8(isoFile.write());
  323. }
  324. /**
  325. * This function returns the offset of the 1st byte of a child box within
  326. * a container box.
  327. *
  328. * @param {ISOBox} parent
  329. * @param {string} type
  330. * @return {number}
  331. * @private
  332. */
  333. getBoxOffset_(parent, type) {
  334. let offset = 8;
  335. for (let i = 0; i < parent.boxes.length; i++) {
  336. if (parent.boxes[i].type === type) {
  337. return offset;
  338. }
  339. offset += parent.boxes[i].size;
  340. }
  341. return offset;
  342. }
  343. };
  344. /**
  345. * @private {!Uint8Array}
  346. */
  347. shaka.transmuxer.MssTransmuxer.UUID_SENC_ = new Uint8Array([
  348. 0xA2, 0x39, 0x4F, 0x52, 0x5A, 0x9B, 0x4F, 0x14,
  349. 0xA2, 0x44, 0x6C, 0x42, 0x7C, 0x64, 0x8D, 0xF4,
  350. ]);
  351. shaka.transmuxer.TransmuxerEngine.registerTransmuxer(
  352. 'mss/audio/mp4',
  353. () => new shaka.transmuxer.MssTransmuxer('mss/audio/mp4'),
  354. shaka.transmuxer.TransmuxerEngine.PluginPriority.FALLBACK);
  355. shaka.transmuxer.TransmuxerEngine.registerTransmuxer(
  356. 'mss/video/mp4',
  357. () => new shaka.transmuxer.MssTransmuxer('mss/video/mp4'),
  358. shaka.transmuxer.TransmuxerEngine.PluginPriority.FALLBACK);