Source: lib/offline/indexeddb/v1_storage_cell.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.offline.indexeddb.V1StorageCell');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.log');
  9. goog.require('shaka.offline.indexeddb.BaseStorageCell');
  10. goog.require('shaka.util.Error');
  11. goog.require('shaka.util.ManifestParserUtils');
  12. goog.require('shaka.util.PeriodCombiner');
  13. goog.require('shaka.util.PublicPromise');
  14. /**
  15. * The V1StorageCell is for all stores that follow the shaka.externs V1 offline
  16. * types, introduced in Shaka Player v2.0 and deprecated in v2.3.
  17. *
  18. * @implements {shaka.extern.StorageCell}
  19. */
  20. shaka.offline.indexeddb.V1StorageCell = class
  21. extends shaka.offline.indexeddb.BaseStorageCell {
  22. /** @override */
  23. async updateManifestExpiration(key, newExpiration) {
  24. const op = this.connection_.startReadWriteOperation(this.manifestStore_);
  25. /** @type {IDBObjectStore} */
  26. const store = op.store();
  27. /** @type {!shaka.util.PublicPromise} */
  28. const p = new shaka.util.PublicPromise();
  29. store.get(key).onsuccess = (event) => {
  30. // Make sure a defined value was found. Indexeddb treats "no value found"
  31. // as a success with an undefined result.
  32. const manifest = /** @type {shaka.extern.ManifestDBV1} */(
  33. event.target.result);
  34. // Indexeddb does not fail when you get a value that is not in the
  35. // database. It will return an undefined value. However, we expect
  36. // the value to never be null, so something is wrong if we get a
  37. // falsy value.
  38. if (manifest) {
  39. // Since this store's scheme uses in-line keys, we don't specify the key
  40. // with |put|. This difference is why we must override the base class.
  41. goog.asserts.assert(
  42. manifest.key == key,
  43. 'With in-line keys, the keys should match');
  44. manifest.expiration = newExpiration;
  45. store.put(manifest);
  46. p.resolve();
  47. } else {
  48. p.reject(new shaka.util.Error(
  49. shaka.util.Error.Severity.CRITICAL,
  50. shaka.util.Error.Category.STORAGE,
  51. shaka.util.Error.Code.KEY_NOT_FOUND,
  52. 'Could not find values for ' + key));
  53. }
  54. };
  55. await Promise.all([op.promise(), p]);
  56. }
  57. /**
  58. * @override
  59. * @param {shaka.extern.ManifestDBV1} old
  60. * @return {!Promise<shaka.extern.ManifestDB>}
  61. */
  62. async convertManifest(old) {
  63. const V1StorageCell = shaka.offline.indexeddb.V1StorageCell;
  64. const streamsPerPeriod = [];
  65. for (let i = 0; i < old.periods.length; ++i) {
  66. // The last period ends at the end of the presentation.
  67. const periodEnd = i == old.periods.length - 1 ?
  68. old.duration : old.periods[i + 1].startTime;
  69. const duration = periodEnd - old.periods[i].startTime;
  70. const streams = V1StorageCell.convertPeriod_(old.periods[i], duration);
  71. streamsPerPeriod.push(streams);
  72. }
  73. const streams = await shaka.util.PeriodCombiner.combineDbStreams(
  74. streamsPerPeriod);
  75. return {
  76. creationTime: 0,
  77. originalManifestUri: old.originalManifestUri,
  78. duration: old.duration,
  79. size: old.size,
  80. expiration: old.expiration == null ? Infinity : old.expiration,
  81. streams,
  82. sessionIds: old.sessionIds,
  83. drmInfo: old.drmInfo,
  84. appMetadata: old.appMetadata,
  85. sequenceMode: false,
  86. };
  87. }
  88. /**
  89. * @param {shaka.extern.PeriodDBV1} old
  90. * @param {number} periodDuration
  91. * @return {!Array<shaka.extern.StreamDB>}
  92. * @private
  93. */
  94. static convertPeriod_(old, periodDuration) {
  95. const V1StorageCell = shaka.offline.indexeddb.V1StorageCell;
  96. // In the case that this is really old (like really old, like dinosaurs
  97. // roaming the Earth old) there may be no variants, so we need to add those.
  98. V1StorageCell.fillMissingVariants_(old);
  99. for (const stream of old.streams) {
  100. const message = 'After filling in missing variants, ' +
  101. 'each stream should have variant ids';
  102. goog.asserts.assert(stream.variantIds, message);
  103. }
  104. return old.streams.map((stream) => V1StorageCell.convertStream_(
  105. stream, old.startTime, periodDuration));
  106. }
  107. /**
  108. * @param {shaka.extern.StreamDBV1} old
  109. * @param {number} periodStart
  110. * @param {number} periodDuration
  111. * @return {shaka.extern.StreamDB}
  112. * @private
  113. */
  114. static convertStream_(old, periodStart, periodDuration) {
  115. const V1StorageCell = shaka.offline.indexeddb.V1StorageCell;
  116. const initSegmentKey = old.initSegmentUri ?
  117. V1StorageCell.getKeyFromSegmentUri_(old.initSegmentUri) : null;
  118. // timestampOffset in the new format is the inverse of
  119. // presentationTimeOffset in the old format. Also, PTO did not include the
  120. // period start, while TO does.
  121. const timestampOffset = periodStart + old.presentationTimeOffset;
  122. const appendWindowStart = periodStart;
  123. const appendWindowEnd = periodStart + periodDuration;
  124. return {
  125. id: old.id,
  126. originalId: null,
  127. groupId: null,
  128. primary: old.primary,
  129. type: old.contentType,
  130. mimeType: old.mimeType,
  131. codecs: old.codecs,
  132. frameRate: old.frameRate,
  133. pixelAspectRatio: undefined,
  134. hdr: undefined,
  135. colorGamut: undefined,
  136. videoLayout: undefined,
  137. kind: old.kind,
  138. language: old.language,
  139. originalLanguage: old.language || null,
  140. label: old.label,
  141. width: old.width,
  142. height: old.height,
  143. initSegmentKey: initSegmentKey,
  144. encrypted: old.encrypted,
  145. keyIds: new Set([old.keyId]),
  146. segments: old.segments.map((segment) => V1StorageCell.convertSegment_(
  147. segment, initSegmentKey, appendWindowStart, appendWindowEnd,
  148. timestampOffset)),
  149. variantIds: old.variantIds,
  150. roles: [],
  151. forced: false,
  152. audioSamplingRate: null,
  153. channelsCount: null,
  154. spatialAudio: false,
  155. closedCaptions: null,
  156. tilesLayout: undefined,
  157. external: false,
  158. fastSwitching: false,
  159. isAudioMuxedInVideo: false,
  160. };
  161. }
  162. /**
  163. * @param {shaka.extern.SegmentDBV1} old
  164. * @param {?number} initSegmentKey
  165. * @param {number} appendWindowStart
  166. * @param {number} appendWindowEnd
  167. * @param {number} timestampOffset
  168. * @return {shaka.extern.SegmentDB}
  169. * @private
  170. */
  171. static convertSegment_(
  172. old, initSegmentKey, appendWindowStart, appendWindowEnd,
  173. timestampOffset) {
  174. const V1StorageCell = shaka.offline.indexeddb.V1StorageCell;
  175. // Since we don't want to use the uri anymore, we need to parse the key
  176. // from it.
  177. const dataKey = V1StorageCell.getKeyFromSegmentUri_(old.uri);
  178. return {
  179. startTime: appendWindowStart + old.startTime,
  180. endTime: appendWindowStart + old.endTime,
  181. dataKey,
  182. initSegmentKey,
  183. appendWindowStart,
  184. appendWindowEnd,
  185. timestampOffset,
  186. tilesLayout: '',
  187. mimeType: null,
  188. codecs: null,
  189. thumbnailSprite: null,
  190. };
  191. }
  192. /**
  193. * @override
  194. * @param {shaka.extern.SegmentDataDBV1} old
  195. * @return {shaka.extern.SegmentDataDB}
  196. */
  197. convertSegmentData(old) {
  198. return {data: old.data};
  199. }
  200. /**
  201. * @param {string} uri
  202. * @return {number}
  203. * @private
  204. */
  205. static getKeyFromSegmentUri_(uri) {
  206. let parts = null;
  207. // Try parsing the uri as the original Shaka Player 2.0 uri.
  208. parts = /^offline:[0-9]+\/[0-9]+\/([0-9]+)$/.exec(uri);
  209. if (parts) {
  210. return Number(parts[1]);
  211. }
  212. // Just before Shaka Player 2.3 the uri format was changed to remove some
  213. // of the un-used information from the uri and make the segment uri and
  214. // manifest uri follow a similar format. However the old storage system
  215. // was still in place, so it is possible for Storage V1 Cells to have
  216. // Storage V2 uris.
  217. parts = /^offline:segment\/([0-9]+)$/.exec(uri);
  218. if (parts) {
  219. return Number(parts[1]);
  220. }
  221. throw new shaka.util.Error(
  222. shaka.util.Error.Severity.CRITICAL,
  223. shaka.util.Error.Category.STORAGE,
  224. shaka.util.Error.Code.MALFORMED_OFFLINE_URI,
  225. 'Could not parse uri ' + uri);
  226. }
  227. /**
  228. * Take a period and check if the streams need to have variants generated.
  229. * Before Shaka Player moved to its variants model, there were no variants.
  230. * This will fill missing variants into the given object.
  231. *
  232. * @param {shaka.extern.PeriodDBV1} period
  233. * @private
  234. */
  235. static fillMissingVariants_(period) {
  236. const AUDIO = shaka.util.ManifestParserUtils.ContentType.AUDIO;
  237. const VIDEO = shaka.util.ManifestParserUtils.ContentType.VIDEO;
  238. // There are three cases:
  239. // 1. All streams' variant ids are null
  240. // 2. All streams' variant ids are non-null
  241. // 3. Some streams' variant ids are null and other are non-null
  242. // Case 3 is invalid and should never happen in production.
  243. const audio = period.streams.filter((s) => s.contentType == AUDIO);
  244. const video = period.streams.filter((s) => s.contentType == VIDEO);
  245. // Case 2 - There is nothing we need to do, so let's just get out of here.
  246. if (audio.every((s) => s.variantIds) && video.every((s) => s.variantIds)) {
  247. return;
  248. }
  249. // Case 3... We don't want to be in case three.
  250. goog.asserts.assert(
  251. audio.every((s) => !s.variantIds),
  252. 'Some audio streams have variant ids and some do not.');
  253. goog.asserts.assert(
  254. video.every((s) => !s.variantIds),
  255. 'Some video streams have variant ids and some do not.');
  256. // Case 1 - Populate all the variant ids (putting us back to case 2).
  257. // Since all the variant ids are null, we need to first make them into
  258. // valid arrays.
  259. for (const s of audio) {
  260. s.variantIds = [];
  261. }
  262. for (const s of video) {
  263. s.variantIds = [];
  264. }
  265. let nextId = 0;
  266. // It is not possible in Shaka Player's pre-variant world to have audio-only
  267. // and video-only content mixed in with audio-video content. So we can
  268. // assume that there is only audio-only or video-only if one group is empty.
  269. // Everything is video-only content - so each video stream gets to be its
  270. // own variant.
  271. if (video.length && !audio.length) {
  272. shaka.log.debug('Found video-only content. Creating variants for video.');
  273. const variantId = nextId++;
  274. for (const s of video) {
  275. s.variantIds.push(variantId);
  276. }
  277. }
  278. // Everything is audio-only content - so each audio stream gets to be its
  279. // own variant.
  280. if (!video.length && audio.length) {
  281. shaka.log.debug('Found audio-only content. Creating variants for audio.');
  282. const variantId = nextId++;
  283. for (const s of audio) {
  284. s.variantIds.push(variantId);
  285. }
  286. }
  287. // Everything is audio-video content.
  288. if (video.length && audio.length) {
  289. shaka.log.debug('Found audio-video content. Creating variants.');
  290. for (const a of audio) {
  291. for (const v of video) {
  292. const variantId = nextId++;
  293. a.variantIds.push(variantId);
  294. v.variantIds.push(variantId);
  295. }
  296. }
  297. }
  298. }
  299. };