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