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. // falsey 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. };
  86. }
  87. /**
  88. * @param {shaka.extern.PeriodDBV1} old
  89. * @param {number} periodDuration
  90. * @return {!Array.<shaka.extern.StreamDB>}
  91. * @private
  92. */
  93. static convertPeriod_(old, periodDuration) {
  94. const V1StorageCell = shaka.offline.indexeddb.V1StorageCell;
  95. // In the case that this is really old (like really old, like dinosaurs
  96. // roaming the Earth old) there may be no variants, so we need to add those.
  97. V1StorageCell.fillMissingVariants_(old);
  98. for (const stream of old.streams) {
  99. const message = 'After filling in missing variants, ' +
  100. 'each stream should have variant ids';
  101. goog.asserts.assert(stream.variantIds, message);
  102. }
  103. return old.streams.map((stream) => V1StorageCell.convertStream_(
  104. stream, old.startTime, periodDuration));
  105. }
  106. /**
  107. * @param {shaka.extern.StreamDBV1} old
  108. * @param {number} periodStart
  109. * @param {number} periodDuration
  110. * @return {shaka.extern.StreamDB}
  111. * @private
  112. */
  113. static convertStream_(old, periodStart, periodDuration) {
  114. const V1StorageCell = shaka.offline.indexeddb.V1StorageCell;
  115. const initSegmentKey = old.initSegmentUri ?
  116. V1StorageCell.getKeyFromSegmentUri_(old.initSegmentUri) : null;
  117. // timestampOffset in the new format is the inverse of
  118. // presentationTimeOffset in the old format. Also, PTO did not include the
  119. // period start, while TO does.
  120. const timestampOffset = periodStart + old.presentationTimeOffset;
  121. const appendWindowStart = periodStart;
  122. const appendWindowEnd = periodStart + periodDuration;
  123. return {
  124. id: old.id,
  125. originalId: null,
  126. primary: old.primary,
  127. type: old.contentType,
  128. mimeType: old.mimeType,
  129. codecs: old.codecs,
  130. frameRate: old.frameRate,
  131. pixelAspectRatio: undefined,
  132. hdr: undefined,
  133. kind: old.kind,
  134. language: old.language,
  135. label: old.label,
  136. width: old.width,
  137. height: old.height,
  138. initSegmentKey: initSegmentKey,
  139. encrypted: old.encrypted,
  140. keyIds: new Set([old.keyId]),
  141. segments: old.segments.map((segment) => V1StorageCell.convertSegment_(
  142. segment, initSegmentKey, appendWindowStart, appendWindowEnd,
  143. timestampOffset)),
  144. variantIds: old.variantIds,
  145. roles: [],
  146. forced: false,
  147. audioSamplingRate: null,
  148. channelsCount: null,
  149. spatialAudio: false,
  150. closedCaptions: null,
  151. tilesLayout: undefined,
  152. };
  153. }
  154. /**
  155. * @param {shaka.extern.SegmentDBV1} old
  156. * @param {?number} initSegmentKey
  157. * @param {number} appendWindowStart
  158. * @param {number} appendWindowEnd
  159. * @param {number} timestampOffset
  160. * @return {shaka.extern.SegmentDB}
  161. * @private
  162. */
  163. static convertSegment_(
  164. old, initSegmentKey, appendWindowStart, appendWindowEnd,
  165. timestampOffset) {
  166. const V1StorageCell = shaka.offline.indexeddb.V1StorageCell;
  167. // Since we don't want to use the uri anymore, we need to parse the key
  168. // from it.
  169. const dataKey = V1StorageCell.getKeyFromSegmentUri_(old.uri);
  170. return {
  171. startTime: appendWindowStart + old.startTime,
  172. endTime: appendWindowStart + old.endTime,
  173. dataKey,
  174. initSegmentKey,
  175. appendWindowStart,
  176. appendWindowEnd,
  177. timestampOffset,
  178. tilesLayout: '',
  179. };
  180. }
  181. /**
  182. * @override
  183. * @param {shaka.extern.SegmentDataDBV1} old
  184. * @return {shaka.extern.SegmentDataDB}
  185. */
  186. convertSegmentData(old) {
  187. return {data: old.data};
  188. }
  189. /**
  190. * @param {string} uri
  191. * @return {number}
  192. * @private
  193. */
  194. static getKeyFromSegmentUri_(uri) {
  195. let parts = null;
  196. // Try parsing the uri as the original Shaka Player 2.0 uri.
  197. parts = /^offline:[0-9]+\/[0-9]+\/([0-9]+)$/.exec(uri);
  198. if (parts) {
  199. return Number(parts[1]);
  200. }
  201. // Just before Shaka Player 2.3 the uri format was changed to remove some
  202. // of the un-used information from the uri and make the segment uri and
  203. // manifest uri follow a similar format. However the old storage system
  204. // was still in place, so it is possible for Storage V1 Cells to have
  205. // Storage V2 uris.
  206. parts = /^offline:segment\/([0-9]+)$/.exec(uri);
  207. if (parts) {
  208. return Number(parts[1]);
  209. }
  210. throw new shaka.util.Error(
  211. shaka.util.Error.Severity.CRITICAL,
  212. shaka.util.Error.Category.STORAGE,
  213. shaka.util.Error.Code.MALFORMED_OFFLINE_URI,
  214. 'Could not parse uri ' + uri);
  215. }
  216. /**
  217. * Take a period and check if the streams need to have variants generated.
  218. * Before Shaka Player moved to its variants model, there were no variants.
  219. * This will fill missing variants into the given object.
  220. *
  221. * @param {shaka.extern.PeriodDBV1} period
  222. * @private
  223. */
  224. static fillMissingVariants_(period) {
  225. const AUDIO = shaka.util.ManifestParserUtils.ContentType.AUDIO;
  226. const VIDEO = shaka.util.ManifestParserUtils.ContentType.VIDEO;
  227. // There are three cases:
  228. // 1. All streams' variant ids are null
  229. // 2. All streams' variant ids are non-null
  230. // 3. Some streams' variant ids are null and other are non-null
  231. // Case 3 is invalid and should never happen in production.
  232. const audio = period.streams.filter((s) => s.contentType == AUDIO);
  233. const video = period.streams.filter((s) => s.contentType == VIDEO);
  234. // Case 2 - There is nothing we need to do, so let's just get out of here.
  235. if (audio.every((s) => s.variantIds) && video.every((s) => s.variantIds)) {
  236. return;
  237. }
  238. // Case 3... We don't want to be in case three.
  239. goog.asserts.assert(
  240. audio.every((s) => !s.variantIds),
  241. 'Some audio streams have variant ids and some do not.');
  242. goog.asserts.assert(
  243. video.every((s) => !s.variantIds),
  244. 'Some video streams have variant ids and some do not.');
  245. // Case 1 - Populate all the variant ids (putting us back to case 2).
  246. // Since all the variant ids are null, we need to first make them into
  247. // valid arrays.
  248. for (const s of audio) {
  249. s.variantIds = [];
  250. }
  251. for (const s of video) {
  252. s.variantIds = [];
  253. }
  254. let nextId = 0;
  255. // It is not possible in Shaka Player's pre-variant world to have audio-only
  256. // and video-only content mixed in with audio-video content. So we can
  257. // assume that there is only audio-only or video-only if one group is empty.
  258. // Everything is video-only content - so each video stream gets to be its
  259. // own variant.
  260. if (video.length && !audio.length) {
  261. shaka.log.debug('Found video-only content. Creating variants for video.');
  262. const variantId = nextId++;
  263. for (const s of video) {
  264. s.variantIds.push(variantId);
  265. }
  266. }
  267. // Everything is audio-only content - so each audio stream gets to be its
  268. // own variant.
  269. if (!video.length && audio.length) {
  270. shaka.log.debug('Found audio-only content. Creating variants for audio.');
  271. const variantId = nextId++;
  272. for (const s of audio) {
  273. s.variantIds.push(variantId);
  274. }
  275. }
  276. // Everything is audio-video content.
  277. if (video.length && audio.length) {
  278. shaka.log.debug('Found audio-video content. Creating variants.');
  279. for (const a of audio) {
  280. for (const v of video) {
  281. const variantId = nextId++;
  282. a.variantIds.push(variantId);
  283. v.variantIds.push(variantId);
  284. }
  285. }
  286. }
  287. }
  288. };