Source: lib/util/manifest_parser_utils.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.util.ManifestParserUtils');
  7. goog.require('goog.Uri');
  8. goog.require('shaka.util.BufferUtils');
  9. goog.require('shaka.util.Error');
  10. goog.require('shaka.util.StringUtils');
  11. goog.require('shaka.util.Uint8ArrayUtils');
  12. /**
  13. * @summary Utility functions for manifest parsing.
  14. */
  15. shaka.util.ManifestParserUtils = class {
  16. /**
  17. * Resolves an array of relative URIs to the given base URIs. This will result
  18. * in M*N number of URIs.
  19. *
  20. * Note: This method is slow in SmartTVs and Consoles. It should only be
  21. * called when necessary.
  22. *
  23. * @param {!Array<string>} baseUris
  24. * @param {!Array<string>} relativeUris
  25. * @param {string=} extraQueryParams
  26. * @return {!Array<string>}
  27. */
  28. static resolveUris(baseUris, relativeUris, extraQueryParams = '') {
  29. if (relativeUris.length == 0) {
  30. return baseUris;
  31. }
  32. if (baseUris.length == 1 && relativeUris.length == 1) {
  33. const baseUri = new goog.Uri(baseUris[0]);
  34. const relativeUri = new goog.Uri(relativeUris[0]);
  35. const resolvedUri = baseUri.resolve(relativeUri);
  36. if (extraQueryParams) {
  37. resolvedUri.setQueryData(extraQueryParams);
  38. }
  39. return [resolvedUri.toString()];
  40. }
  41. const relativeAsGoog = relativeUris.map((uri) => new goog.Uri(uri));
  42. // For each base URI, this code resolves it with every relative URI.
  43. // The result is a single array containing all the resolved URIs.
  44. const resolvedUris = [];
  45. for (const baseStr of baseUris) {
  46. const base = new goog.Uri(baseStr);
  47. for (const relative of relativeAsGoog) {
  48. const resolvedUri = base.resolve(relative);
  49. if (extraQueryParams) {
  50. resolvedUri.setQueryData(extraQueryParams);
  51. }
  52. resolvedUris.push(resolvedUri.toString());
  53. }
  54. }
  55. return resolvedUris;
  56. }
  57. /**
  58. * Creates a DrmInfo object from the given info.
  59. *
  60. * @param {string} keySystem
  61. * @param {string} encryptionScheme
  62. * @param {Array<shaka.extern.InitDataOverride>} initData
  63. * @param {string} [keySystemUri]
  64. * @return {shaka.extern.DrmInfo}
  65. */
  66. static createDrmInfo(keySystem, encryptionScheme, initData, keySystemUri) {
  67. const drmInfo = {
  68. keySystem,
  69. encryptionScheme,
  70. licenseServerUri: '',
  71. distinctiveIdentifierRequired: false,
  72. persistentStateRequired: false,
  73. audioRobustness: '',
  74. videoRobustness: '',
  75. serverCertificate: null,
  76. serverCertificateUri: '',
  77. sessionType: '',
  78. initData: initData || [],
  79. keyIds: new Set(),
  80. };
  81. if (keySystemUri) {
  82. drmInfo.keySystemUris = new Set([keySystemUri]);
  83. }
  84. return drmInfo;
  85. }
  86. /**
  87. * Creates a DrmInfo object from ClearKeys.
  88. *
  89. * @param {!Map<string, string>} clearKeys
  90. * @param {string=} encryptionScheme
  91. * @return {shaka.extern.DrmInfo}
  92. */
  93. static createDrmInfoFromClearKeys(clearKeys, encryptionScheme = 'cenc') {
  94. const StringUtils = shaka.util.StringUtils;
  95. const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils;
  96. const keys = [];
  97. const keyIds = [];
  98. const originalKeyIds = [];
  99. clearKeys.forEach((key, keyId) => {
  100. let kid = keyId;
  101. if (kid.length != 22) {
  102. kid = Uint8ArrayUtils.toBase64(
  103. Uint8ArrayUtils.fromHex(keyId), false);
  104. }
  105. let k = key;
  106. if (k.length != 22) {
  107. k = Uint8ArrayUtils.toBase64(
  108. Uint8ArrayUtils.fromHex(key), false);
  109. }
  110. const keyObj = {
  111. kty: 'oct',
  112. kid: kid,
  113. k: k,
  114. };
  115. keys.push(keyObj);
  116. keyIds.push(keyObj.kid);
  117. originalKeyIds.push(keyId);
  118. });
  119. const jwkSet = {keys: keys};
  120. const license = JSON.stringify(jwkSet);
  121. // Use the keyids init data since is suggested by EME.
  122. // Suggestion: https://bit.ly/2JYcNTu
  123. // Format: https://www.w3.org/TR/eme-initdata-keyids/
  124. const initDataStr = JSON.stringify({'kids': keyIds});
  125. const initData =
  126. shaka.util.BufferUtils.toUint8(StringUtils.toUTF8(initDataStr));
  127. const initDatas = [{initData: initData, initDataType: 'keyids'}];
  128. return {
  129. keySystem: 'org.w3.clearkey',
  130. encryptionScheme,
  131. licenseServerUri: 'data:application/json;base64,' + window.btoa(license),
  132. distinctiveIdentifierRequired: false,
  133. persistentStateRequired: false,
  134. audioRobustness: '',
  135. videoRobustness: '',
  136. serverCertificate: null,
  137. serverCertificateUri: '',
  138. sessionType: '',
  139. initData: initDatas,
  140. keyIds: new Set(originalKeyIds),
  141. };
  142. }
  143. /**
  144. * Attempts to guess which codecs from the codecs list belong to a given
  145. * content type.
  146. * Assumes that at least one codec is correct, and throws if none are.
  147. *
  148. * @param {string} contentType
  149. * @param {!Array<string>} codecs
  150. * @return {string}
  151. */
  152. static guessCodecs(contentType, codecs) {
  153. if (codecs.length == 1) {
  154. return codecs[0];
  155. }
  156. const match = shaka.util.ManifestParserUtils.guessCodecsSafe(
  157. contentType, codecs);
  158. // A failure is specifically denoted by null; an empty string represents a
  159. // valid match of no codec.
  160. if (match != null) {
  161. return match;
  162. }
  163. // Unable to guess codecs.
  164. throw new shaka.util.Error(
  165. shaka.util.Error.Severity.CRITICAL,
  166. shaka.util.Error.Category.MANIFEST,
  167. shaka.util.Error.Code.HLS_COULD_NOT_GUESS_CODECS,
  168. codecs);
  169. }
  170. /**
  171. * Attempts to guess which codecs from the codecs list belong to a given
  172. * content type. Does not assume a single codec is anything special, and does
  173. * not throw if it fails to match.
  174. *
  175. * @param {string} contentType
  176. * @param {!Array<string>} codecs
  177. * @return {?string} or null if no match is found
  178. */
  179. static guessCodecsSafe(contentType, codecs) {
  180. const formats = shaka.util.ManifestParserUtils
  181. .CODEC_REGEXPS_BY_CONTENT_TYPE_[contentType];
  182. for (const format of formats) {
  183. for (const codec of codecs) {
  184. if (format.test(codec.trim())) {
  185. return codec.trim();
  186. }
  187. }
  188. }
  189. // Text does not require a codec string.
  190. if (contentType == shaka.util.ManifestParserUtils.ContentType.TEXT) {
  191. return '';
  192. }
  193. return null;
  194. }
  195. /**
  196. * Attempts to guess which codecs from the codecs list belong to a given
  197. * content.
  198. *
  199. * @param {string} contentType
  200. * @param {!Array<string>} codecs
  201. * @return {!Array<string>}
  202. */
  203. static guessAllCodecsSafe(contentType, codecs) {
  204. const allCodecs = [];
  205. const formats = shaka.util.ManifestParserUtils
  206. .CODEC_REGEXPS_BY_CONTENT_TYPE_[contentType];
  207. for (const format of formats) {
  208. for (const codec of codecs) {
  209. if (format.test(codec.trim())) {
  210. allCodecs.push(codec.trim());
  211. }
  212. }
  213. }
  214. return allCodecs;
  215. }
  216. };
  217. /**
  218. * @enum {string}
  219. */
  220. shaka.util.ManifestParserUtils.ContentType = {
  221. VIDEO: 'video',
  222. AUDIO: 'audio',
  223. TEXT: 'text',
  224. IMAGE: 'image',
  225. APPLICATION: 'application',
  226. };
  227. /**
  228. * @enum {string}
  229. */
  230. shaka.util.ManifestParserUtils.TextStreamKind = {
  231. SUBTITLE: 'subtitle',
  232. CLOSED_CAPTION: 'caption',
  233. };
  234. /**
  235. * Specifies how tolerant the player is of inaccurate segment start times and
  236. * end times within a manifest. For example, gaps or overlaps between segments
  237. * in a SegmentTimeline which are greater than or equal to this value will
  238. * result in a warning message.
  239. *
  240. * @const {number}
  241. */
  242. shaka.util.ManifestParserUtils.GAP_OVERLAP_TOLERANCE_SECONDS = 1 / 15;
  243. /**
  244. * A list of regexps to detect well-known video codecs.
  245. *
  246. * @const {!Array<!RegExp>}
  247. * @private
  248. */
  249. shaka.util.ManifestParserUtils.VIDEO_CODEC_REGEXPS_ = [
  250. /^avc/,
  251. /^hev/,
  252. /^hvc/,
  253. /^vvc/,
  254. /^vvi/,
  255. /^vp0?[89]/,
  256. /^av01/,
  257. /^dvh/, // Dolby Vision based in HEVC
  258. /^dva/, // Dolby Vision based in AVC
  259. /^dav/, // Dolby Vision based in AV1
  260. ];
  261. /**
  262. * A list of regexps to detect well-known audio codecs.
  263. *
  264. * @const {!Array<!RegExp>}
  265. * @private
  266. */
  267. shaka.util.ManifestParserUtils.AUDIO_CODEC_REGEXPS_ = [
  268. /^vorbis$/,
  269. /^Opus$/, // correct codec string according to RFC 6381 section 3.3
  270. /^opus$/, // some manifests wrongfully use this
  271. /^fLaC$/, // correct codec string according to RFC 6381 section 3.3
  272. /^flac$/, // some manifests wrongfully use this
  273. /^mp4a/,
  274. /^[ae]c-3$/,
  275. /^ac-4/,
  276. /^dts[cex]$/, // DTS Digital Surround (dtsc), DTS Express (dtse), DTS:X (dtsx)
  277. /^iamf/,
  278. /^mhm[12]/, // MPEG-H Audio LC
  279. ];
  280. /**
  281. * A list of regexps to detect well-known text codecs.
  282. *
  283. * @const {!Array<!RegExp>}
  284. * @private
  285. */
  286. shaka.util.ManifestParserUtils.TEXT_CODEC_REGEXPS_ = [
  287. /^vtt$/,
  288. /^wvtt/,
  289. /^stpp/,
  290. ];
  291. /**
  292. * @const {!Object<string, !Array<!RegExp>>}
  293. */
  294. shaka.util.ManifestParserUtils.CODEC_REGEXPS_BY_CONTENT_TYPE_ = {
  295. 'audio': shaka.util.ManifestParserUtils.AUDIO_CODEC_REGEXPS_,
  296. 'video': shaka.util.ManifestParserUtils.VIDEO_CODEC_REGEXPS_,
  297. 'text': shaka.util.ManifestParserUtils.TEXT_CODEC_REGEXPS_,
  298. };