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