Source: lib/media/segment_utils.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.media.SegmentUtils');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.log');
  9. goog.require('shaka.media.Capabilities');
  10. goog.require('shaka.media.ClosedCaptionParser');
  11. goog.require('shaka.util.BufferUtils');
  12. goog.require('shaka.util.ManifestParserUtils');
  13. goog.require('shaka.util.MimeUtils');
  14. goog.require('shaka.util.Mp4BoxParsers');
  15. goog.require('shaka.util.Mp4Parser');
  16. goog.require('shaka.util.TsParser');
  17. /**
  18. * @summary Utility functions for segment parsing.
  19. */
  20. shaka.media.SegmentUtils = class {
  21. /**
  22. * @param {string} mimeType
  23. * @return {shaka.media.SegmentUtils.BasicInfo}
  24. */
  25. static getBasicInfoFromMimeType(mimeType) {
  26. const baseMimeType = shaka.util.MimeUtils.getBasicType(mimeType);
  27. const type = baseMimeType.split('/')[0];
  28. const codecs = shaka.util.MimeUtils.getCodecs(mimeType);
  29. return {
  30. type: type,
  31. mimeType: baseMimeType,
  32. codecs: codecs,
  33. language: null,
  34. height: null,
  35. width: null,
  36. channelCount: null,
  37. sampleRate: null,
  38. closedCaptions: new Map(),
  39. videoRange: null,
  40. colorGamut: null,
  41. };
  42. }
  43. /**
  44. * @param {!BufferSource} data
  45. * @return {?shaka.media.SegmentUtils.BasicInfo}
  46. */
  47. static getBasicInfoFromTs(data) {
  48. const uint8ArrayData = shaka.util.BufferUtils.toUint8(data);
  49. const tsParser = new shaka.util.TsParser().parse(uint8ArrayData);
  50. const tsCodecs = tsParser.getCodecs();
  51. const videoInfo = tsParser.getVideoInfo();
  52. const codecs = [];
  53. let hasAudio = false;
  54. let hasVideo = false;
  55. switch (tsCodecs.audio) {
  56. case 'aac':
  57. case 'aac-loas':
  58. codecs.push('mp4a.40.2');
  59. hasAudio = true;
  60. break;
  61. case 'mp3':
  62. codecs.push('mp4a.40.34');
  63. hasAudio = true;
  64. break;
  65. case 'ac3':
  66. codecs.push('ac-3');
  67. hasAudio = true;
  68. break;
  69. case 'ec3':
  70. codecs.push('ec-3');
  71. hasAudio = true;
  72. break;
  73. case 'opus':
  74. codecs.push('opus');
  75. hasAudio = true;
  76. break;
  77. }
  78. switch (tsCodecs.video) {
  79. case 'avc':
  80. if (videoInfo.codec) {
  81. codecs.push(videoInfo.codec);
  82. } else {
  83. codecs.push('avc1.42E01E');
  84. }
  85. hasVideo = true;
  86. break;
  87. case 'hvc':
  88. if (videoInfo.codec) {
  89. codecs.push(videoInfo.codec);
  90. } else {
  91. codecs.push('hvc1.1.6.L93.90');
  92. }
  93. hasVideo = true;
  94. break;
  95. case 'av1':
  96. codecs.push('av01.0.01M.08');
  97. hasVideo = true;
  98. break;
  99. }
  100. if (!codecs.length) {
  101. return null;
  102. }
  103. const onlyAudio = hasAudio && !hasVideo;
  104. const closedCaptions = new Map();
  105. if (hasVideo) {
  106. const captionParser = new shaka.media.ClosedCaptionParser('video/mp2t');
  107. captionParser.parseFrom(data);
  108. for (const stream of captionParser.getStreams()) {
  109. closedCaptions.set(stream, stream);
  110. }
  111. captionParser.reset();
  112. }
  113. return {
  114. type: onlyAudio ? 'audio' : 'video',
  115. mimeType: 'video/mp2t',
  116. codecs: codecs.join(', '),
  117. language: null,
  118. height: videoInfo.height,
  119. width: videoInfo.width,
  120. channelCount: null,
  121. sampleRate: null,
  122. closedCaptions: closedCaptions,
  123. videoRange: null,
  124. colorGamut: null,
  125. };
  126. }
  127. /**
  128. * @param {?BufferSource} initData
  129. * @param {!BufferSource} data
  130. * @return {?shaka.media.SegmentUtils.BasicInfo}
  131. */
  132. static getBasicInfoFromMp4(initData, data) {
  133. const Mp4Parser = shaka.util.Mp4Parser;
  134. const SegmentUtils = shaka.media.SegmentUtils;
  135. const audioCodecs = [];
  136. const videoCodecs = [];
  137. let hasAudio = false;
  138. let hasVideo = false;
  139. const addCodec = (codec) => {
  140. const codecLC = codec.toLowerCase();
  141. switch (codecLC) {
  142. case 'avc1':
  143. case 'avc3':
  144. videoCodecs.push(codecLC + '.42E01E');
  145. hasVideo = true;
  146. break;
  147. case 'hev1':
  148. case 'hvc1':
  149. videoCodecs.push(codecLC + '.1.6.L93.90');
  150. hasVideo = true;
  151. break;
  152. case 'dvh1':
  153. case 'dvhe':
  154. videoCodecs.push(codecLC + '.05.04');
  155. hasVideo = true;
  156. break;
  157. case 'vp09':
  158. videoCodecs.push(codecLC + '.00.10.08');
  159. hasVideo = true;
  160. break;
  161. case 'av01':
  162. videoCodecs.push(codecLC + '.0.01M.08');
  163. hasVideo = true;
  164. break;
  165. case 'mp4a':
  166. // We assume AAC, but this can be wrong since mp4a supports
  167. // others codecs
  168. audioCodecs.push('mp4a.40.2');
  169. hasAudio = true;
  170. break;
  171. case 'ac-3':
  172. case 'ec-3':
  173. case 'opus':
  174. case 'flac':
  175. audioCodecs.push(codecLC);
  176. hasAudio = true;
  177. break;
  178. }
  179. };
  180. const codecBoxParser = (box) => addCodec(box.name);
  181. /** @type {?string} */
  182. let language = null;
  183. /** @type {?string} */
  184. let height = null;
  185. /** @type {?string} */
  186. let width = null;
  187. /** @type {?number} */
  188. let channelCount = null;
  189. /** @type {?number} */
  190. let sampleRate = null;
  191. /** @type {?string} */
  192. let realVideoRange = null;
  193. /** @type {?string} */
  194. let realColorGamut = null;
  195. /** @type {?string} */
  196. let baseBox;
  197. new Mp4Parser()
  198. .box('moov', Mp4Parser.children)
  199. .box('trak', Mp4Parser.children)
  200. .fullBox('tkhd', (box) => {
  201. goog.asserts.assert(
  202. box.version != null,
  203. 'TKHD is a full box and should have a valid version.');
  204. const parsedTKHDBox = shaka.util.Mp4BoxParsers.parseTKHD(
  205. box.reader, box.version);
  206. height = String(parsedTKHDBox.height);
  207. width = String(parsedTKHDBox.width);
  208. })
  209. .box('mdia', Mp4Parser.children)
  210. .fullBox('mdhd', (box) => {
  211. goog.asserts.assert(
  212. box.version != null,
  213. 'MDHD is a full box and should have a valid version.');
  214. const parsedMDHDBox = shaka.util.Mp4BoxParsers.parseMDHD(
  215. box.reader, box.version);
  216. language = parsedMDHDBox.language;
  217. })
  218. .box('minf', Mp4Parser.children)
  219. .box('stbl', Mp4Parser.children)
  220. .fullBox('stsd', Mp4Parser.sampleDescription)
  221. // AUDIO
  222. // These are the various boxes that signal a codec.
  223. .box('mp4a', (box) => {
  224. const parsedMP4ABox = shaka.util.Mp4BoxParsers.parseMP4A(box.reader);
  225. channelCount = parsedMP4ABox.channelCount;
  226. sampleRate = parsedMP4ABox.sampleRate;
  227. if (box.reader.hasMoreData()) {
  228. Mp4Parser.children(box);
  229. } else {
  230. codecBoxParser(box);
  231. }
  232. })
  233. .box('esds', (box) => {
  234. const parsedESDSBox = shaka.util.Mp4BoxParsers.parseESDS(box.reader);
  235. audioCodecs.push(parsedESDSBox.codec);
  236. hasAudio = true;
  237. })
  238. .box('ac-3', codecBoxParser)
  239. .box('ec-3', codecBoxParser)
  240. .box('opus', codecBoxParser)
  241. .box('Opus', codecBoxParser)
  242. .box('fLaC', codecBoxParser)
  243. // VIDEO
  244. // These are the various boxes that signal a codec.
  245. .box('avc1', (box) => {
  246. baseBox = box.name;
  247. Mp4Parser.visualSampleEntry(box);
  248. })
  249. .box('avc3', (box) => {
  250. baseBox = box.name;
  251. Mp4Parser.visualSampleEntry(box);
  252. })
  253. .box('hev1', (box) => {
  254. baseBox = box.name;
  255. Mp4Parser.visualSampleEntry(box);
  256. })
  257. .box('hvc1', (box) => {
  258. baseBox = box.name;
  259. Mp4Parser.visualSampleEntry(box);
  260. })
  261. .box('dva1', (box) => {
  262. baseBox = box.name;
  263. Mp4Parser.visualSampleEntry(box);
  264. })
  265. .box('dvav', (box) => {
  266. baseBox = box.name;
  267. Mp4Parser.visualSampleEntry(box);
  268. })
  269. .box('dvh1', (box) => {
  270. baseBox = box.name;
  271. Mp4Parser.visualSampleEntry(box);
  272. })
  273. .box('dvhe', (box) => {
  274. baseBox = box.name;
  275. Mp4Parser.visualSampleEntry(box);
  276. })
  277. .box('vp09', (box) => {
  278. baseBox = box.name;
  279. Mp4Parser.visualSampleEntry(box);
  280. })
  281. .box('av01', (box) => {
  282. baseBox = box.name;
  283. Mp4Parser.visualSampleEntry(box);
  284. })
  285. .box('avcC', (box) => {
  286. let codecBase = baseBox || '';
  287. switch (baseBox) {
  288. case 'dvav':
  289. codecBase = 'avc3';
  290. break;
  291. case 'dva1':
  292. codecBase = 'avc1';
  293. break;
  294. }
  295. const parsedAVCCBox = shaka.util.Mp4BoxParsers.parseAVCC(
  296. codecBase, box.reader, box.name);
  297. videoCodecs.push(parsedAVCCBox.codec);
  298. hasVideo = true;
  299. })
  300. .box('hvcC', (box) => {
  301. let codecBase = baseBox || '';
  302. switch (baseBox) {
  303. case 'dvh1':
  304. codecBase = 'hvc1';
  305. break;
  306. case 'dvhe':
  307. codecBase = 'hev1';
  308. break;
  309. }
  310. const parsedHVCCBox = shaka.util.Mp4BoxParsers.parseHVCC(
  311. codecBase, box.reader, box.name);
  312. videoCodecs.push(parsedHVCCBox.codec);
  313. hasVideo = true;
  314. })
  315. .box('dvcC', (box) => {
  316. let codecBase = baseBox || '';
  317. switch (baseBox) {
  318. case 'hvc1':
  319. codecBase = 'dvh1';
  320. break;
  321. case 'hev1':
  322. codecBase = 'dvhe';
  323. break;
  324. case 'avc1':
  325. codecBase = 'dva1';
  326. break;
  327. case 'avc3':
  328. codecBase = 'dvav';
  329. break;
  330. }
  331. const parsedDVCCBox = shaka.util.Mp4BoxParsers.parseDVCC(
  332. codecBase, box.reader, box.name);
  333. videoCodecs.push(parsedDVCCBox.codec);
  334. hasVideo = true;
  335. })
  336. .fullBox('vpcC', (box) => {
  337. const codecBase = baseBox || '';
  338. const parsedVPCCBox = shaka.util.Mp4BoxParsers.parseVPCC(
  339. codecBase, box.reader, box.name);
  340. videoCodecs.push(parsedVPCCBox.codec);
  341. hasVideo = true;
  342. })
  343. .box('av1C', (box) => {
  344. const codecBase = baseBox || '';
  345. const parsedAV1CBox = shaka.util.Mp4BoxParsers.parseAV1C(
  346. codecBase, box.reader, box.name);
  347. videoCodecs.push(parsedAV1CBox.codec);
  348. hasVideo = true;
  349. })
  350. // This signals an encrypted sample, which we can go inside of to
  351. // find the codec used.
  352. // Note: If encrypted, you can only have audio or video, not both.
  353. .box('enca', Mp4Parser.audioSampleEntry)
  354. .box('encv', Mp4Parser.visualSampleEntry)
  355. .box('sinf', Mp4Parser.children)
  356. .box('frma', (box) => {
  357. const {codec} = shaka.util.Mp4BoxParsers.parseFRMA(box.reader);
  358. addCodec(codec);
  359. })
  360. .box('colr', (box) => {
  361. const {videoRange, colorGamut} =
  362. shaka.util.Mp4BoxParsers.parseCOLR(box.reader);
  363. realVideoRange = videoRange;
  364. realColorGamut = colorGamut;
  365. })
  366. .parse(initData || data, /* partialOkay= */ true);
  367. if (!audioCodecs.length && !videoCodecs.length) {
  368. return null;
  369. }
  370. const onlyAudio = hasAudio && !hasVideo;
  371. const closedCaptions = new Map();
  372. if (hasVideo) {
  373. const captionParser = new shaka.media.ClosedCaptionParser('video/mp4');
  374. if (initData) {
  375. captionParser.init(initData);
  376. }
  377. captionParser.parseFrom(data);
  378. for (const stream of captionParser.getStreams()) {
  379. closedCaptions.set(stream, stream);
  380. }
  381. captionParser.reset();
  382. }
  383. const codecs = audioCodecs.concat(videoCodecs);
  384. return {
  385. type: onlyAudio ? 'audio' : 'video',
  386. mimeType: onlyAudio ? 'audio/mp4' : 'video/mp4',
  387. codecs: SegmentUtils.codecsFiltering(codecs).join(', '),
  388. language: language,
  389. height: height,
  390. width: width,
  391. channelCount: channelCount,
  392. sampleRate: sampleRate,
  393. closedCaptions: closedCaptions,
  394. videoRange: realVideoRange,
  395. colorGamut: realColorGamut,
  396. };
  397. }
  398. /**
  399. * @param {!Array.<string>} codecs
  400. * @return {!Array.<string>} codecs
  401. */
  402. static codecsFiltering(codecs) {
  403. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  404. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  405. const SegmentUtils = shaka.media.SegmentUtils;
  406. const allCodecs = SegmentUtils.filterDuplicateCodecs_(codecs);
  407. const audioCodecs =
  408. ManifestParserUtils.guessAllCodecsSafe(ContentType.AUDIO, allCodecs);
  409. const videoCodecs =
  410. ManifestParserUtils.guessAllCodecsSafe(ContentType.VIDEO, allCodecs);
  411. const textCodecs =
  412. ManifestParserUtils.guessAllCodecsSafe(ContentType.TEXT, allCodecs);
  413. const validVideoCodecs = SegmentUtils.chooseBetterCodecs_(videoCodecs);
  414. const finalCodecs =
  415. audioCodecs.concat(validVideoCodecs).concat(textCodecs);
  416. if (allCodecs.length && !finalCodecs.length) {
  417. return allCodecs;
  418. }
  419. return finalCodecs;
  420. }
  421. /**
  422. * @param {!Array.<string>} codecs
  423. * @return {!Array.<string>} codecs
  424. * @private
  425. */
  426. static filterDuplicateCodecs_(codecs) {
  427. // Filter out duplicate codecs.
  428. const seen = new Set();
  429. const ret = [];
  430. for (const codec of codecs) {
  431. const shortCodec = shaka.util.MimeUtils.getCodecBase(codec);
  432. if (!seen.has(shortCodec)) {
  433. ret.push(codec);
  434. seen.add(shortCodec);
  435. } else {
  436. shaka.log.debug('Ignoring duplicate codec');
  437. }
  438. }
  439. return ret;
  440. }
  441. /**
  442. * Prioritizes Dolby Vision if supported. This is necessary because with
  443. * Dolby Vision we could have hvcC and dvcC boxes at the same time.
  444. *
  445. * @param {!Array.<string>} codecs
  446. * @return {!Array.<string>} codecs
  447. * @private
  448. */
  449. static chooseBetterCodecs_(codecs) {
  450. if (codecs.length <= 1) {
  451. return codecs;
  452. }
  453. const dolbyVision = codecs.find((codec) => {
  454. return codec.startsWith('dvh1.') ||
  455. codec.startsWith('dvhe.') ||
  456. codec.startsWith('dav1.');
  457. });
  458. if (!dolbyVision) {
  459. return codecs;
  460. }
  461. const type = `video/mp4; codecs="${dolbyVision}"`;
  462. if (shaka.media.Capabilities.isTypeSupported(type)) {
  463. return [dolbyVision];
  464. }
  465. return codecs.filter((codec) => codec != dolbyVision);
  466. }
  467. /**
  468. * @param {!BufferSource} data
  469. * @return {?string}
  470. */
  471. static getDefaultKID(data) {
  472. const Mp4Parser = shaka.util.Mp4Parser;
  473. let defaultKID = null;
  474. new Mp4Parser()
  475. .box('moov', Mp4Parser.children)
  476. .box('trak', Mp4Parser.children)
  477. .box('mdia', Mp4Parser.children)
  478. .box('minf', Mp4Parser.children)
  479. .box('stbl', Mp4Parser.children)
  480. .fullBox('stsd', Mp4Parser.sampleDescription)
  481. .box('encv', Mp4Parser.visualSampleEntry)
  482. .box('enca', Mp4Parser.audioSampleEntry)
  483. .box('sinf', Mp4Parser.children)
  484. .box('schi', Mp4Parser.children)
  485. .fullBox('tenc', (box) => {
  486. const parsedTENCBox = shaka.util.Mp4BoxParsers.parseTENC(box.reader);
  487. defaultKID = parsedTENCBox.defaultKID;
  488. })
  489. .parse(data, /* partialOkay= */ true);
  490. return defaultKID;
  491. }
  492. };
  493. /**
  494. * @typedef {{
  495. * type: string,
  496. * mimeType: string,
  497. * codecs: string,
  498. * language: ?string,
  499. * height: ?string,
  500. * width: ?string,
  501. * channelCount: ?number,
  502. * sampleRate: ?number,
  503. * closedCaptions: Map.<string, string>,
  504. * videoRange: ?string,
  505. * colorGamut: ?string
  506. * }}
  507. *
  508. * @property {string} type
  509. * @property {string} mimeType
  510. * @property {string} codecs
  511. * @property {?string} language
  512. * @property {?string} height
  513. * @property {?string} width
  514. * @property {?number} channelCount
  515. * @property {?number} sampleRate
  516. * @property {Map.<string, string>} closedCaptions
  517. * @property {?string} videoRange
  518. * @property {?string} colorGamut
  519. */
  520. shaka.media.SegmentUtils.BasicInfo;