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 shaka.externs V2 offline
  26. * types. This storage cell will only work for version 1 indexed db database
  27. * schemes.
  28. *
  29. * @implements {shaka.extern.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 {shaka.extern.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.<shaka.extern.PeriodDB>,
  239. // sessionIds: !Array.<string>,
  240. // drmInfo: ?shaka.extern.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 {shaka.extern.PeriodDB}
  276. * @private
  277. */
  278. static convertPeriod_(old) {
  279. // Old Period Format:
  280. // {
  281. // startTime: number,
  282. // streams: !Array.<shaka.extern.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 {shaka.extern.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.<shaka.extern.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. originalId: null,
  369. primary: old.primary,
  370. presentationTimeOffset: old.presentationTimeOffset,
  371. contentType: old.contentType,
  372. mimeType: old.mimeType,
  373. codecs: old.codecs,
  374. frameRate: old.frameRate,
  375. kind: old.kind,
  376. language: old.language,
  377. label: old.label,
  378. width: old.width,
  379. height: old.height,
  380. initSegmentKey: initSegmentKey,
  381. encrypted: old.encrypted,
  382. keyId: old.keyId,
  383. segments: old.segments.map(convertSegment),
  384. variantIds: old.variantIds,
  385. };
  386. }
  387. /**
  388. * @param {!Object} old
  389. * @return {shaka.extern.SegmentDB}
  390. * @private
  391. */
  392. static convertSegment_(old) {
  393. // Old Segment Format
  394. // {
  395. // startTime: number,
  396. // endTime: number,
  397. // uri: string
  398. // }
  399. goog.asserts.assert(
  400. old.startTime != null,
  401. 'The old segment format should have a start time field');
  402. goog.asserts.assert(
  403. old.endTime != null,
  404. 'The old segment format should have an end time field');
  405. goog.asserts.assert(
  406. old.uri != null,
  407. 'The old segment format should have a uri field');
  408. const V1StorageCell = shaka.offline.indexeddb.V1StorageCell;
  409. const getKeyFromUri = V1StorageCell.getKeyFromSegmentUri_;
  410. // Since we don't want to use the uri anymore, we need to parse the key
  411. // from it.
  412. let dataKey = getKeyFromUri(old.uri);
  413. return {
  414. startTime: old.startTime,
  415. endTime: old.endTime,
  416. dataKey: dataKey,
  417. };
  418. }
  419. /**
  420. * @param {!Object} old
  421. * @return {shaka.extern.SegmentDataDB}
  422. * @private
  423. */
  424. static convertSegmentData_(old) {
  425. // Old Segment Format:
  426. // {
  427. // key: number,
  428. // data: ArrayBuffer
  429. // }
  430. goog.asserts.assert(
  431. old.key != null,
  432. 'The old segment data format should have a key field');
  433. goog.asserts.assert(
  434. old.data != null,
  435. 'The old segment data format should have a data field');
  436. return {data: old.data};
  437. }
  438. /**
  439. * @param {string} uri
  440. * @return {number}
  441. * @private
  442. */
  443. static getKeyFromSegmentUri_(uri) {
  444. let parts = null;
  445. // Try parsing the uri as the original Shaka Player 2.0 uri.
  446. parts = /^offline:[0-9]+\/[0-9]+\/([0-9]+)$/.exec(uri);
  447. if (parts) {
  448. return Number(parts[1]);
  449. }
  450. // Just before Shaka Player 2.3 the uri format was changed to remove some
  451. // of the un-used information from the uri and make the segment uri and
  452. // manifest uri follow a similar format. However the old storage system
  453. // was still in place, so it is possible for Storage V1 Cells to have
  454. // Storage V2 uris.
  455. parts = /^offline:segment\/([0-9]+)$/.exec(uri);
  456. if (parts) {
  457. return Number(parts[1]);
  458. }
  459. throw new shaka.util.Error(
  460. shaka.util.Error.Severity.CRITICAL,
  461. shaka.util.Error.Category.STORAGE,
  462. shaka.util.Error.Code.MALFORMED_OFFLINE_URI,
  463. 'Could not parse uri ' + uri);
  464. }
  465. /**
  466. * Take a period and check if the streams need to have variants generated.
  467. * Before Shaka Player moved to its variants model, there were no variants.
  468. * This will fill missing variants into the given object.
  469. *
  470. * @param {!Object} period
  471. * @private
  472. */
  473. static fillMissingVariants_(period) {
  474. const AUDIO = shaka.util.ManifestParserUtils.ContentType.AUDIO;
  475. const VIDEO = shaka.util.ManifestParserUtils.ContentType.VIDEO;
  476. // There are three cases:
  477. // 1. All streams' variant ids are null
  478. // 2. All streams' variant ids are non-null
  479. // 3. Some streams' variant ids are null and other are non-null
  480. // Case 3 is invalid and should never happen in production.
  481. let audio = period.streams.filter((s) => s.contentType == AUDIO);
  482. let video = period.streams.filter((s) => s.contentType == VIDEO);
  483. // Case 2 - There is nothing we need to do, so let's just get out of here.
  484. if (audio.every((s) => s.variantIds) && video.every((s) => s.variantIds)) {
  485. return;
  486. }
  487. // Case 3... We don't want to be in case three.
  488. goog.asserts.assert(
  489. audio.every((s) => !s.variantIds),
  490. 'Some audio streams have variant ids and some do not.');
  491. goog.asserts.assert(
  492. video.every((s) => !s.variantIds),
  493. 'Some video streams have variant ids and some do not.');
  494. // Case 1 - Populate all the variant ids (putting us back to case 2).
  495. // Since all the variant ids are null, we need to first make them into
  496. // valid arrays.
  497. audio.forEach((s) => { s.variantIds = []; });
  498. video.forEach((s) => { s.variantIds = []; });
  499. let nextId = 0;
  500. // It is not possible in Shaka Player's pre-variant world to have audio-only
  501. // and video-only content mixed in with audio-video content. So we can
  502. // assume that there is only audio-only or video-only if one group is empty.
  503. // Everything is video-only content - so each video stream gets to be its
  504. // own variant.
  505. if (video.length && !audio.length) {
  506. shaka.log.debug('Found video-only content. Creating variants for video.');
  507. let variantId = nextId++;
  508. video.forEach((s) => { s.variantIds.push(variantId); });
  509. }
  510. // Everything is audio-only content - so each audio stream gets to be its
  511. // own variant.
  512. if (!video.length && audio.length) {
  513. shaka.log.debug('Found audio-only content. Creating variants for audio.');
  514. let variantId = nextId++;
  515. audio.forEach((s) => { s.variantIds.push(variantId); });
  516. }
  517. // Everything is audio-video content.
  518. if (video.length && audio.length) {
  519. shaka.log.debug('Found audio-video content. Creating variants.');
  520. audio.forEach((a) => {
  521. video.forEach((v) => {
  522. let variantId = nextId++;
  523. a.variantIds.push(variantId);
  524. v.variantIds.push(variantId);
  525. });
  526. });
  527. }
  528. }
  529. };