Source: lib/offline/indexeddb/storage_mechanism.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.StorageMechanism');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.device.DeviceFactory');
  9. goog.require('shaka.log');
  10. goog.require('shaka.offline.StorageMuxer');
  11. goog.require('shaka.offline.indexeddb.EmeSessionStorageCell');
  12. goog.require('shaka.offline.indexeddb.V1StorageCell');
  13. goog.require('shaka.offline.indexeddb.V2StorageCell');
  14. goog.require('shaka.offline.indexeddb.V5StorageCell');
  15. goog.require('shaka.util.Error');
  16. goog.require('shaka.util.PublicPromise');
  17. goog.require('shaka.util.Timer');
  18. /**
  19. * A storage mechanism to manage storage cells for an indexed db instance.
  20. * The cells are just for interacting with the stores that are found in the
  21. * database instance. The mechanism is responsible for creating new stores
  22. * when opening the database. If the database is too old of a version, a
  23. * cell will be added for the old stores but the cell won't support add
  24. * operations. The mechanism will create the new versions of the stores and
  25. * will allow add operations for those stores.
  26. *
  27. * @implements {shaka.extern.StorageMechanism}
  28. */
  29. shaka.offline.indexeddb.StorageMechanism = class {
  30. /** */
  31. constructor() {
  32. /** @private {IDBDatabase} */
  33. this.db_ = null;
  34. /** @private {shaka.extern.StorageCell} */
  35. this.v1_ = null;
  36. /** @private {shaka.extern.StorageCell} */
  37. this.v2_ = null;
  38. /** @private {shaka.extern.StorageCell} */
  39. this.v3_ = null;
  40. /** @private {shaka.extern.StorageCell} */
  41. this.v5_ = null;
  42. /** @private {shaka.extern.EmeSessionStorageCell} */
  43. this.sessions_ = null;
  44. }
  45. /**
  46. * @override
  47. */
  48. init() {
  49. const name = shaka.offline.indexeddb.StorageMechanism.DB_NAME;
  50. const version = shaka.offline.indexeddb.StorageMechanism.VERSION;
  51. const p = new shaka.util.PublicPromise();
  52. // Add a timeout mechanism, for the (rare?) case where no callbacks are
  53. // called at all, so that this method doesn't hang forever.
  54. let timedOut = false;
  55. const timeOutTimer = new shaka.util.Timer(() => {
  56. timedOut = true;
  57. p.reject(new shaka.util.Error(
  58. shaka.util.Error.Severity.CRITICAL,
  59. shaka.util.Error.Category.STORAGE,
  60. shaka.util.Error.Code.INDEXED_DB_INIT_TIMED_OUT));
  61. });
  62. const openTimeout = shaka.offline.indexeddb.StorageMechanismOpenTimeout;
  63. if (typeof openTimeout === 'number' && openTimeout > 0) {
  64. timeOutTimer.tickAfter(openTimeout);
  65. }
  66. const open = window.indexedDB.open(name, version);
  67. open.onsuccess = (event) => {
  68. if (timedOut) {
  69. // Too late, we have already given up on opening the storage mechanism.
  70. return;
  71. }
  72. timeOutTimer.stop();
  73. const db = open.result;
  74. this.db_ = db;
  75. this.v1_ = shaka.offline.indexeddb.StorageMechanism.createV1_(db);
  76. this.v2_ = shaka.offline.indexeddb.StorageMechanism.createV2_(db);
  77. this.v3_ = shaka.offline.indexeddb.StorageMechanism.createV3_(db);
  78. // NOTE: V4 of the database was when we introduced a special table to
  79. // store EME session IDs. It has no separate storage cell, so we skip to
  80. // V5.
  81. this.v5_ = shaka.offline.indexeddb.StorageMechanism.createV5_(db);
  82. this.sessions_ =
  83. shaka.offline.indexeddb.StorageMechanism.createEmeSessionCell_(db);
  84. p.resolve();
  85. };
  86. open.onupgradeneeded = (event) => {
  87. // Add object stores for the latest version only.
  88. this.createStores_(open.result);
  89. };
  90. open.onerror = (event) => {
  91. if (timedOut) {
  92. // Too late, we have already given up on opening the storage mechanism.
  93. return;
  94. }
  95. timeOutTimer.stop();
  96. p.reject(new shaka.util.Error(
  97. shaka.util.Error.Severity.CRITICAL,
  98. shaka.util.Error.Category.STORAGE,
  99. shaka.util.Error.Code.INDEXED_DB_ERROR,
  100. open.error));
  101. // Firefox will raise an error on the main thread unless we stop it here.
  102. event.preventDefault();
  103. };
  104. return p;
  105. }
  106. /**
  107. * @override
  108. */
  109. async destroy() {
  110. if (this.v1_) {
  111. await this.v1_.destroy();
  112. }
  113. if (this.v2_) {
  114. await this.v2_.destroy();
  115. }
  116. if (this.v3_) {
  117. await this.v3_.destroy();
  118. }
  119. if (this.v5_) {
  120. await this.v5_.destroy();
  121. }
  122. if (this.sessions_) {
  123. await this.sessions_.destroy();
  124. }
  125. // If we were never initialized, then |db_| will still be null.
  126. if (this.db_) {
  127. this.db_.close();
  128. }
  129. }
  130. /**
  131. * @override
  132. */
  133. getCells() {
  134. const map = new Map();
  135. if (this.v1_) {
  136. map.set('v1', this.v1_);
  137. }
  138. if (this.v2_) {
  139. map.set('v2', this.v2_);
  140. }
  141. if (this.v3_) {
  142. map.set('v3', this.v3_);
  143. }
  144. if (this.v5_) {
  145. map.set('v5', this.v5_);
  146. }
  147. return map;
  148. }
  149. /**
  150. * @override
  151. */
  152. getEmeSessionCell() {
  153. goog.asserts.assert(this.sessions_, 'Cannot be destroyed.');
  154. return this.sessions_;
  155. }
  156. /**
  157. * @override
  158. */
  159. async erase() {
  160. // Not all cells may have been created, so only destroy the ones that
  161. // were created.
  162. if (this.v1_) {
  163. await this.v1_.destroy();
  164. }
  165. if (this.v2_) {
  166. await this.v2_.destroy();
  167. }
  168. if (this.v3_) {
  169. await this.v3_.destroy();
  170. }
  171. if (this.v5_) {
  172. await this.v5_.destroy();
  173. }
  174. // |db_| will only be null if the muxer was not initialized. We need to
  175. // close the connection in order delete the database without it being
  176. // blocked.
  177. if (this.db_) {
  178. this.db_.close();
  179. }
  180. await shaka.offline.indexeddb.StorageMechanism.deleteAll_();
  181. // Reset before initializing.
  182. this.db_ = null;
  183. this.v1_ = null;
  184. this.v2_ = null;
  185. this.v3_ = null;
  186. this.v5_ = null;
  187. await this.init();
  188. }
  189. /**
  190. * @param {!IDBDatabase} db
  191. * @return {shaka.extern.StorageCell}
  192. * @private
  193. */
  194. static createV1_(db) {
  195. const StorageMechanism = shaka.offline.indexeddb.StorageMechanism;
  196. const segmentStore = StorageMechanism.V1_SEGMENT_STORE;
  197. const manifestStore = StorageMechanism.V1_MANIFEST_STORE;
  198. const stores = db.objectStoreNames;
  199. if (stores.contains(manifestStore) && stores.contains(segmentStore)) {
  200. shaka.log.debug('Mounting v1 idb storage cell');
  201. return new shaka.offline.indexeddb.V1StorageCell(
  202. db,
  203. segmentStore,
  204. manifestStore);
  205. }
  206. return null;
  207. }
  208. /**
  209. * @param {!IDBDatabase} db
  210. * @return {shaka.extern.StorageCell}
  211. * @private
  212. */
  213. static createV2_(db) {
  214. const StorageMechanism = shaka.offline.indexeddb.StorageMechanism;
  215. const segmentStore = StorageMechanism.V2_SEGMENT_STORE;
  216. const manifestStore = StorageMechanism.V2_MANIFEST_STORE;
  217. const stores = db.objectStoreNames;
  218. if (stores.contains(manifestStore) && stores.contains(segmentStore)) {
  219. shaka.log.debug('Mounting v2 idb storage cell');
  220. return new shaka.offline.indexeddb.V2StorageCell(
  221. db,
  222. segmentStore,
  223. manifestStore);
  224. }
  225. return null;
  226. }
  227. /**
  228. * @param {!IDBDatabase} db
  229. * @return {shaka.extern.StorageCell}
  230. * @private
  231. */
  232. static createV3_(db) {
  233. const StorageMechanism = shaka.offline.indexeddb.StorageMechanism;
  234. const segmentStore = StorageMechanism.V3_SEGMENT_STORE;
  235. const manifestStore = StorageMechanism.V3_MANIFEST_STORE;
  236. const stores = db.objectStoreNames;
  237. if (stores.contains(manifestStore) && stores.contains(segmentStore)) {
  238. shaka.log.debug('Mounting v3 idb storage cell');
  239. // Version 3 uses the same structure as version 2, so we can use the same
  240. // cells but it can support new entries.
  241. return new shaka.offline.indexeddb.V2StorageCell(
  242. db,
  243. segmentStore,
  244. manifestStore);
  245. }
  246. return null;
  247. }
  248. /**
  249. * @param {!IDBDatabase} db
  250. * @return {shaka.extern.StorageCell}
  251. * @private
  252. */
  253. static createV5_(db) {
  254. const StorageMechanism = shaka.offline.indexeddb.StorageMechanism;
  255. const segmentStore = StorageMechanism.V5_SEGMENT_STORE;
  256. const manifestStore = StorageMechanism.V5_MANIFEST_STORE;
  257. const stores = db.objectStoreNames;
  258. if (stores.contains(manifestStore) && stores.contains(segmentStore)) {
  259. shaka.log.debug('Mounting v5 idb storage cell');
  260. return new shaka.offline.indexeddb.V5StorageCell(
  261. db,
  262. segmentStore,
  263. manifestStore);
  264. }
  265. return null;
  266. }
  267. /**
  268. * @param {!IDBDatabase} db
  269. * @return {shaka.extern.EmeSessionStorageCell}
  270. * @private
  271. */
  272. static createEmeSessionCell_(db) {
  273. const StorageMechanism = shaka.offline.indexeddb.StorageMechanism;
  274. const store = StorageMechanism.SESSION_ID_STORE;
  275. if (db.objectStoreNames.contains(store)) {
  276. shaka.log.debug('Mounting session ID idb storage cell');
  277. return new shaka.offline.indexeddb.EmeSessionStorageCell(db, store);
  278. }
  279. return null;
  280. }
  281. /**
  282. * @param {!IDBDatabase} db
  283. * @private
  284. */
  285. createStores_(db) {
  286. const storeNames = [
  287. shaka.offline.indexeddb.StorageMechanism.V5_SEGMENT_STORE,
  288. shaka.offline.indexeddb.StorageMechanism.V5_MANIFEST_STORE,
  289. shaka.offline.indexeddb.StorageMechanism.SESSION_ID_STORE,
  290. ];
  291. for (const name of storeNames) {
  292. if (!db.objectStoreNames.contains(name)) {
  293. db.createObjectStore(name, {autoIncrement: true});
  294. }
  295. }
  296. }
  297. /**
  298. * Delete the indexed db instance so that all stores are deleted and cleared.
  299. * This will force the database to a like-new state next time it opens.
  300. *
  301. * @return {!Promise}
  302. * @private
  303. */
  304. static deleteAll_() {
  305. const name = shaka.offline.indexeddb.StorageMechanism.DB_NAME;
  306. const p = new shaka.util.PublicPromise();
  307. const del = window.indexedDB.deleteDatabase(name);
  308. del.onblocked = (event) => {
  309. shaka.log.warning('Deleting', name, 'is being blocked', event);
  310. };
  311. del.onsuccess = (event) => {
  312. p.resolve();
  313. };
  314. del.onerror = (event) => {
  315. p.reject(new shaka.util.Error(
  316. shaka.util.Error.Severity.CRITICAL,
  317. shaka.util.Error.Category.STORAGE,
  318. shaka.util.Error.Code.INDEXED_DB_ERROR,
  319. del.error));
  320. // Firefox will raise an error on the main thread unless we stop it here.
  321. event.preventDefault();
  322. };
  323. return p;
  324. }
  325. };
  326. /** @const {string} */
  327. shaka.offline.indexeddb.StorageMechanism.DB_NAME = 'shaka_offline_db';
  328. /** @const {number} */
  329. shaka.offline.indexeddb.StorageMechanism.VERSION = 5;
  330. /** @const {string} */
  331. shaka.offline.indexeddb.StorageMechanism.V1_SEGMENT_STORE = 'segment';
  332. /** @const {string} */
  333. shaka.offline.indexeddb.StorageMechanism.V2_SEGMENT_STORE = 'segment-v2';
  334. /** @const {string} */
  335. shaka.offline.indexeddb.StorageMechanism.V3_SEGMENT_STORE = 'segment-v3';
  336. /** @const {string} */
  337. shaka.offline.indexeddb.StorageMechanism.V5_SEGMENT_STORE = 'segment-v5';
  338. /** @const {string} */
  339. shaka.offline.indexeddb.StorageMechanism.V1_MANIFEST_STORE = 'manifest';
  340. /** @const {string} */
  341. shaka.offline.indexeddb.StorageMechanism.V2_MANIFEST_STORE = 'manifest-v2';
  342. /** @const {string} */
  343. shaka.offline.indexeddb.StorageMechanism.V3_MANIFEST_STORE = 'manifest-v3';
  344. /** @const {string} */
  345. shaka.offline.indexeddb.StorageMechanism.V5_MANIFEST_STORE = 'manifest-v5';
  346. /** @const {string} */
  347. shaka.offline.indexeddb.StorageMechanism.SESSION_ID_STORE = 'session-ids';
  348. /**
  349. * Timeout in seconds for opening the IndexedDB database,
  350. * or <code>false</code> to disable the timeout and wait indefinitely
  351. * for the database to open successfully or fail.
  352. * @type {number|boolean}
  353. * @export
  354. */
  355. shaka.offline.indexeddb.StorageMechanismOpenTimeout = 5;
  356. // Since this may be called before the polyfills remove indexeddb support from
  357. // some platforms (looking at you Chromecast), we need to check for support
  358. // when we create the mechanism.
  359. //
  360. // Thankfully the storage muxer api allows us to return a null mechanism
  361. // to indicate that the mechanism is not supported on this platform.
  362. shaka.offline.StorageMuxer.register(
  363. 'idb',
  364. () => {
  365. const device = shaka.device.DeviceFactory.getDevice();
  366. if (!device.supportsOfflineStorage()) {
  367. return null;
  368. }
  369. return new shaka.offline.indexeddb.StorageMechanism();
  370. });