Source: lib/offline/indexeddb/v1_storage_cell.js

  1. /**
  2. * @license
  3. * Copyright 2016 Google Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. goog.provide('shaka.offline.indexeddb.V1StorageCell');
  18. goog.require('goog.asserts');
  19. goog.require('shaka.log');
  20. goog.require('shaka.offline.indexeddb.DBConnection');
  21. goog.require('shaka.util.Error');
  22. goog.require('shaka.util.ManifestParserUtils');
  23. goog.require('shaka.util.PublicPromise');
  24. /**
  25. * The V1StorageCell is for all stores that follow the shakaExterns V2 offline
  26. * types. This storage cell will only work for version 1 indexed db database
  27. * schemes.
  28. *
  29. * @implements {shakaExtern.StorageCell}
  30. */
  31. shaka.offline.indexeddb.V1StorageCell = class {
  32. /**
  33. * @param {IDBDatabase} connection
  34. * @param {string} segmentStore
  35. * @param {string} manifestStore
  36. */
  37. constructor(connection, segmentStore, manifestStore) {
  38. /** @private {!shaka.offline.indexeddb.DBConnection} */
  39. this.connection_ = new shaka.offline.indexeddb.DBConnection(connection);
  40. /** @private {string} */
  41. this.segmentStore_ = segmentStore;
  42. /** @private {string} */
  43. this.manifestStore_ = manifestStore;
  44. }
  45. /**
  46. * @override
  47. */
  48. destroy() { return this.connection_.destroy(); }
  49. /**
  50. * @override
  51. */
  52. hasFixedKeySpace() {
  53. // We do not allow adding new values to V1 databases.
  54. return true;
  55. }
  56. /**
  57. * @override
  58. */
  59. addSegments(segments) { return this.rejectAdd_(this.segmentStore_); }
  60. /**
  61. * @override
  62. */
  63. removeSegments(keys, onRemove) {
  64. return this.remove_(this.segmentStore_, keys, onRemove);
  65. }
  66. /**
  67. * @override
  68. */
  69. getSegments(keys) {
  70. const convertSegmentData =
  71. shaka.offline.indexeddb.V1StorageCell.convertSegmentData_;
  72. return this.get_(this.segmentStore_, keys).then((segments) => {
  73. return segments.map(convertSegmentData);
  74. });
  75. }
  76. /**
  77. * @override
  78. */
  79. addManifests(manifests) { return this.rejectAdd_(this.manifestStore_); }
  80. /**
  81. * @override
  82. */
  83. updateManifestExpiration(key, newExpiration) {
  84. let op = this.connection_.startReadWriteOperation(this.manifestStore_);
  85. let store = op.store();
  86. let p = new shaka.util.PublicPromise();
  87. store.get(key).onsuccess = (event) => {
  88. // Make sure a defined value was found. Indexeddb treats "no value found"
  89. // as a success with an undefined result.
  90. let manifest = event.target.result;
  91. // Indexeddb does not fail when you get a value that is not in the
  92. // database. It will return an undefined value. However, we expect
  93. // the value to never be null, so something is wrong if we get a
  94. // falsey value.
  95. if (manifest) {
  96. // Since this store's scheme uses in-line keys, we don't need to specify
  97. // the key with |put|.
  98. goog.asserts.assert(
  99. manifest.key == key,
  100. 'With in-line keys, the keys should match');
  101. manifest.expiration = newExpiration;
  102. store.put(manifest);
  103. p.resolve();
  104. } else {
  105. p.reject(new shaka.util.Error(
  106. shaka.util.Error.Severity.CRITICAL,
  107. shaka.util.Error.Category.STORAGE,
  108. shaka.util.Error.Code.KEY_NOT_FOUND,
  109. 'Could not find values for ' + key));
  110. }
  111. };
  112. // Only return our promise after the operation completes.
  113. return op.promise().then(() => p);
  114. }
  115. /**
  116. * @override
  117. */
  118. removeManifests(keys, onRemove) {
  119. return this.remove_(this.manifestStore_, keys, onRemove);
  120. }
  121. /**
  122. * @override
  123. */
  124. getManifests(keys) {
  125. const convertManifest =
  126. shaka.offline.indexeddb.V1StorageCell.convertManifest_;
  127. return this.get_(this.manifestStore_, keys).then((manifests) => {
  128. return manifests.map(convertManifest);
  129. });
  130. }
  131. /**
  132. * @override
  133. */
  134. getAllManifests() {
  135. const convertManifest =
  136. shaka.offline.indexeddb.V1StorageCell.convertManifest_;
  137. let op = this.connection_.startReadOnlyOperation(this.manifestStore_);
  138. let store = op.store();
  139. let values = {};
  140. store.openCursor().onsuccess = (event) => {
  141. // When we reach the end of the data that the cursor is iterating
  142. // over, |event.target.result| will be null to signal the end of the
  143. // iteration.
  144. // https://developer.mozilla.org/en-US/docs/Web/API/IDBCursor/continue
  145. let cursor = event.target.result;
  146. if (!cursor) {
  147. return;
  148. }
  149. values[cursor.key] = convertManifest(cursor.value);
  150. // Go to the next item in the store, which will cause |onsuccess| to be
  151. // called again.
  152. cursor.continue();
  153. };
  154. // Wait until the operation completes or else values may be missing from
  155. // |values|.
  156. return op.promise().then(() => values);
  157. }
  158. /**
  159. * @param {string} storeName
  160. * @return {!Promise}
  161. * @private
  162. */
  163. rejectAdd_(storeName) {
  164. return Promise.reject(new shaka.util.Error(
  165. shaka.util.Error.Severity.CRITICAL,
  166. shaka.util.Error.Category.STORAGE,
  167. shaka.util.Error.Code.NEW_KEY_OPERATION_NOT_SUPPORTED,
  168. 'Cannot add new value to ' + storeName));
  169. }
  170. /**
  171. * @param {string} storeName
  172. * @param {!Array.<number>} keys
  173. * @param {function(number)} onRemove
  174. * @return {!Promise}
  175. * @private
  176. */
  177. remove_(storeName, keys, onRemove) {
  178. let op = this.connection_.startReadWriteOperation(storeName);
  179. let store = op.store();
  180. keys.forEach((key) => {
  181. store.delete(key).onsuccess = () => onRemove(key);
  182. });
  183. return op.promise();
  184. }
  185. /**
  186. * @param {string} storeName
  187. * @param {!Array.<number>} keys
  188. * @return {!Promise.<!Array.<T>>}
  189. * @template T
  190. * @private
  191. */
  192. get_(storeName, keys) {
  193. let op = this.connection_.startReadOnlyOperation(storeName);
  194. let store = op.store();
  195. let values = {};
  196. let missing = [];
  197. // Use a map to store the objects so that we can reorder the results to
  198. // match the order of |keys|.
  199. keys.forEach((key) => {
  200. store.get(key).onsuccess = (event) => {
  201. let value = event.target.result;
  202. // Make sure a defined value was found. Indexeddb treats no-value found
  203. // as a success with an undefined result.
  204. if (value == undefined) {
  205. missing.push(key);
  206. }
  207. values[key] = value;
  208. };
  209. });
  210. // Wait until the operation completes or else values may be missing from
  211. // |values|. Use the original key list to convert the map to a list so that
  212. // the order will match.
  213. return op.promise().then(() => {
  214. if (missing.length) {
  215. return Promise.reject(new shaka.util.Error(
  216. shaka.util.Error.Severity.CRITICAL,
  217. shaka.util.Error.Category.STORAGE,
  218. shaka.util.Error.Code.KEY_NOT_FOUND,
  219. 'Could not find values for ' + missing
  220. ));
  221. }
  222. return keys.map((key) => values[key]);
  223. });
  224. }
  225. /**
  226. * @param {!Object} old
  227. * @return {shakaExtern.ManifestDB}
  228. * @private
  229. */
  230. static convertManifest_(old) {
  231. // Old Manifest Format:
  232. // {
  233. // key: number,
  234. // originalManifestUri: string,
  235. // duration: number,
  236. // size: number,
  237. // expiration: number,
  238. // periods: !Array.<shakaExtern.PeriodDB>,
  239. // sessionIds: !Array.<string>,
  240. // drmInfo: ?shakaExtern.DrmInfo,
  241. // appMetadata: Object
  242. // }
  243. goog.asserts.assert(
  244. old.originalManifestUri != null,
  245. 'Old manifest format should have an originalManifestUri field');
  246. goog.asserts.assert(
  247. old.duration != null,
  248. 'Old manifest format should have a duration field');
  249. goog.asserts.assert(
  250. old.size != null,
  251. 'Old manifest format should have a size field');
  252. goog.asserts.assert(
  253. old.periods != null,
  254. 'Old manifest format should have a periods field');
  255. goog.asserts.assert(
  256. old.sessionIds != null,
  257. 'Old manifest format should have a session ids field');
  258. goog.asserts.assert(
  259. old.appMetadata != null,
  260. 'Old manifest format should have an app metadata field');
  261. const convertPeriod = shaka.offline.indexeddb.V1StorageCell.convertPeriod_;
  262. return {
  263. originalManifestUri: old.originalManifestUri,
  264. duration: old.duration,
  265. size: old.size,
  266. expiration: old.expiration == null ? Infinity : old.expiration,
  267. periods: old.periods.map(convertPeriod),
  268. sessionIds: old.sessionIds,
  269. drmInfo: old.drmInfo,
  270. appMetadata: old.appMetadata
  271. };
  272. }
  273. /**
  274. * @param {!Object} old
  275. * @return {shakaExtern.PeriodDB}
  276. * @private
  277. */
  278. static convertPeriod_(old) {
  279. // Old Period Format:
  280. // {
  281. // startTime: number,
  282. // streams: !Array.<shakaExtern.StreamDB>
  283. // }
  284. goog.asserts.assert(
  285. old.startTime != null,
  286. 'Old period format should have a start time field');
  287. goog.asserts.assert(
  288. old.streams != null,
  289. 'Old period format should have a streams field');
  290. const convertStream = shaka.offline.indexeddb.V1StorageCell.convertStream_;
  291. const fillMissingVariants =
  292. shaka.offline.indexeddb.V1StorageCell.fillMissingVariants_;
  293. // In the case that this is really old (like really old, like dinosaurs
  294. // roaming the Earth old) there may be no variants, so we need to add those.
  295. fillMissingVariants(old);
  296. old.streams.forEach((stream) => {
  297. const message = 'After filling in missing variants, ' +
  298. 'each stream should have variant ids';
  299. goog.asserts.assert(stream.variantIds, message);
  300. });
  301. return {
  302. startTime: old.startTime,
  303. streams: old.streams.map(convertStream)
  304. };
  305. }
  306. /**
  307. * @param {!Object} old
  308. * @return {shakaExtern.StreamDB}
  309. * @private
  310. */
  311. static convertStream_(old) {
  312. // Old Stream Format
  313. // {
  314. // id: number,
  315. // primary: boolean,
  316. // presentationTimeOffset: number,
  317. // contentType: string,
  318. // mimeType: string,
  319. // codecs: string,
  320. // frameRate: (number|undefined),
  321. // kind: (string|undefined),
  322. // language: string,
  323. // label: ?string,
  324. // width: ?number,
  325. // height: ?number,
  326. // initSegmentUri: ?string,
  327. // encrypted: boolean,
  328. // keyId: ?string,
  329. // segments: !Array.<shakaExtern.SegmentDB>,
  330. // variantIds: ?Array.<number>
  331. // }
  332. goog.asserts.assert(
  333. old.id != null,
  334. 'Old stream format should have an id field');
  335. goog.asserts.assert(
  336. old.primary != null,
  337. 'Old stream format should have a primary field');
  338. goog.asserts.assert(
  339. old.presentationTimeOffset != null,
  340. 'Old stream format should have a presentation time offset field');
  341. goog.asserts.assert(
  342. old.contentType != null,
  343. 'Old stream format should have a content type field');
  344. goog.asserts.assert(
  345. old.mimeType != null,
  346. 'Old stream format should have a mime type field');
  347. goog.asserts.assert(
  348. old.codecs != null,
  349. 'Old stream format should have a codecs field');
  350. goog.asserts.assert(
  351. old.language != null,
  352. 'Old stream format should have a language field');
  353. goog.asserts.assert(
  354. old.encrypted != null,
  355. 'Old stream format should have an encrypted field');
  356. goog.asserts.assert(
  357. old.segments != null,
  358. 'Old stream format should have a segments field');
  359. const getKeyFromUri =
  360. shaka.offline.indexeddb.V1StorageCell.getKeyFromSegmentUri_;
  361. const convertSegment =
  362. shaka.offline.indexeddb.V1StorageCell.convertSegment_;
  363. let initSegmentKey = old.initSegmentUri ?
  364. getKeyFromUri(old.initSegmentUri) :
  365. null;
  366. return {
  367. id: old.id,
  368. primary: old.primary,
  369. presentationTimeOffset: old.presentationTimeOffset,
  370. contentType: old.contentType,
  371. mimeType: old.mimeType,
  372. codecs: old.codecs,
  373. frameRate: old.frameRate,
  374. kind: old.kind,
  375. language: old.language,
  376. label: old.label,
  377. width: old.width,
  378. height: old.height,
  379. initSegmentKey: initSegmentKey,
  380. encrypted: old.encrypted,
  381. keyId: old.keyId,
  382. segments: old.segments.map(convertSegment),
  383. variantIds: old.variantIds
  384. };
  385. }
  386. /**
  387. * @param {!Object} old
  388. * @return {shakaExtern.SegmentDB}
  389. * @private
  390. */
  391. static convertSegment_(old) {
  392. // Old Segment Format
  393. // {
  394. // startTime: number,
  395. // endTime: number,
  396. // uri: string
  397. // }
  398. goog.asserts.assert(
  399. old.startTime != null,
  400. 'The old segment format should have a start time field');
  401. goog.asserts.assert(
  402. old.endTime != null,
  403. 'The old segment format should have an end time field');
  404. goog.asserts.assert(
  405. old.uri != null,
  406. 'The old segment format should have a uri field');
  407. const V1StorageCell = shaka.offline.indexeddb.V1StorageCell;
  408. const getKeyFromUri = V1StorageCell.getKeyFromSegmentUri_;
  409. // Since we don't want to use the uri anymore, we need to parse the key
  410. // from it.
  411. let dataKey = getKeyFromUri(old.uri);
  412. return {
  413. startTime: old.startTime,
  414. endTime: old.endTime,
  415. dataKey: dataKey
  416. };
  417. }
  418. /**
  419. * @param {!Object} old
  420. * @return {shakaExtern.SegmentDataDB}
  421. * @private
  422. */
  423. static convertSegmentData_(old) {
  424. // Old Segment Format:
  425. // {
  426. // key: number,
  427. // data: ArrayBuffer
  428. // }
  429. goog.asserts.assert(
  430. old.key != null,
  431. 'The old segment data format should have a key field');
  432. goog.asserts.assert(
  433. old.data != null,
  434. 'The old segment data format should have a data field');
  435. return {data: old.data};
  436. }
  437. /**
  438. * @param {string} uri
  439. * @return {number}
  440. * @private
  441. */
  442. static getKeyFromSegmentUri_(uri) {
  443. let parts = null;
  444. // Try parsing the uri as the original Shaka Player 2.0 uri.
  445. parts = /^offline:[0-9]+\/[0-9]+\/([0-9]+)$/.exec(uri);
  446. if (parts) {
  447. return Number(parts[1]);
  448. }
  449. // Just before Shaka Player 2.3 the uri format was changed to remove some
  450. // of the un-used information from the uri and make the segment uri and
  451. // manifest uri follow a similar format. However the old storage system
  452. // was still in place, so it is possible for Storage V1 Cells to have
  453. // Storage V2 uris.
  454. parts = /^offline:segment\/([0-9]+)$/.exec(uri);
  455. if (parts) {
  456. return Number(parts[1]);
  457. }
  458. throw new shaka.util.Error(
  459. shaka.util.Error.Severity.CRITICAL,
  460. shaka.util.Error.Category.STORAGE,
  461. shaka.util.Error.Code.MALFORMED_OFFLINE_URI,
  462. 'Could not parse uri ' + uri);
  463. }
  464. /**
  465. * Take a period and check if the streams need to have variants generated.
  466. * Before Shaka Player moved to its variants model, there were no variants.
  467. * This will fill missing variants into the given object.
  468. *
  469. * @param {!Object} period
  470. * @private
  471. */
  472. static fillMissingVariants_(period) {
  473. const AUDIO = shaka.util.ManifestParserUtils.ContentType.AUDIO;
  474. const VIDEO = shaka.util.ManifestParserUtils.ContentType.VIDEO;
  475. // There are three cases:
  476. // 1. All streams' variant ids are null
  477. // 2. All streams' variant ids are non-null
  478. // 3. Some streams' variant ids are null and other are non-null
  479. // Case 3 is invalid and should never happen in production.
  480. let audio = period.streams.filter((s) => s.contentType == AUDIO);
  481. let video = period.streams.filter((s) => s.contentType == VIDEO);
  482. // Case 2 - There is nothing we need to do, so let's just get out of here.
  483. if (audio.every((s) => s.variantIds) && video.every((s) => s.variantIds)) {
  484. return;
  485. }
  486. // Case 3... We don't want to be in case three.
  487. goog.asserts.assert(
  488. audio.every((s) => !s.variantIds),
  489. 'Some audio streams have variant ids and some do not.');
  490. goog.asserts.assert(
  491. video.every((s) => !s.variantIds),
  492. 'Some video streams have variant ids and some do not.');
  493. // Case 1 - Populate all the variant ids (putting us back to case 2).
  494. // Since all the variant ids are null, we need to first make them into
  495. // valid arrays.
  496. audio.forEach((s) => { s.variantIds = []; });
  497. video.forEach((s) => { s.variantIds = []; });
  498. let nextId = 0;
  499. // It is not possible in Shaka Player's pre-variant world to have audio-only
  500. // and video-only content mixed in with audio-video content. So we can
  501. // assume that there is only audio-only or video-only if one group is empty.
  502. // Everything is video-only content - so each video stream gets to be its
  503. // own variant.
  504. if (video.length && !audio.length) {
  505. shaka.log.debug('Found video-only content. Creating variants for video.');
  506. let variantId = nextId++;
  507. video.forEach((s) => { s.variantIds.push(variantId); });
  508. }
  509. // Everything is audio-only content - so each audio stream gets to be its
  510. // own variant.
  511. if (!video.length && audio.length) {
  512. shaka.log.debug('Found audio-only content. Creating variants for audio.');
  513. let variantId = nextId++;
  514. audio.forEach((s) => { s.variantIds.push(variantId); });
  515. }
  516. // Everything is audio-video content.
  517. if (video.length && audio.length) {
  518. shaka.log.debug('Found audio-video content. Creating variants.');
  519. audio.forEach((a) => {
  520. video.forEach((v) => {
  521. let variantId = nextId++;
  522. a.variantIds.push(variantId);
  523. v.variantIds.push(variantId);
  524. });
  525. });
  526. }
  527. }
  528. };