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