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