offline_storage_IndexDBStore.js

/**
 * The copyright in this software is being made available under the BSD License,
 * included below. This software may be subject to other third party and contributor
 * rights, including patent rights, and no such rights are granted under this license.
 *
 * Copyright (c) 2013, Dash Industry Forum.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *  * Redistributions of source code must retain the above copyright notice, this
 *  list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright notice,
 *  this list of conditions and the following disclaimer in the documentation and/or
 *  other materials provided with the distribution.
 *  * Neither the name of Dash Industry Forum nor the names of its
 *  contributors may be used to endorse or promote products derived from this software
 *  without specific prior written permission.
 *
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
 *  EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 *  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 *  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 *  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 *  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 *  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 *  POSSIBILITY OF SUCH DAMAGE.
 */

/**
 * @ignore
 */
const localforage = require('localforage');
const entities = require('html-entities').XmlEntities;

function IndexDBStore() {

    let instance,
        manifestStore,
        fragmentStores;

    function setup() {
        fragmentStores = {};

        if (typeof window === 'undefined') {
            return;
        }

        localforage.config({
            driver: localforage.INDEXEDDB,
            name: 'dash_offline_db'
        });

        manifestStore = localforage.createInstance({
            driver: localforage.INDEXEDDB,
            name: 'dash_offline_db',
            version: 1.0,
            storeName: 'manifest'
        });
    }

    /////////////////////////////////////////
    //
    // GET/SET Methods
    //
    ////////////////////////////////////////

    /**
     * Creates an instance of localforage to store fragments in indexed db
     * @param {string} storeName
     * @instance
     */
    function createFragmentStore(storeName) {

        if (!fragmentStores[storeName]) {
            console.log('setStore  ' + storeName);
            let fragmentStore = localforage.createInstance({
                driver: localforage.INDEXEDDB,
                name: 'dash_offline_db',
                version: 1.0,
                storeName: storeName
            });
            fragmentStores[storeName] = fragmentStore;
        }
    }

    /**
     * Update download status
     * @param {number} manifestId
     * @param {string} newStatus
     * @returns {Promise} promise
     * @instance
     */
    function setDownloadingStatus(manifestId, newStatus) {
        return getManifestById(manifestId).then(function (item) {
            item.status = newStatus;
            return updateManifest(item).catch(function () {
                return Promise.reject('Cannot set status ' + newStatus + ' for this stream !');
            });
        }).catch(function (err) {
            return Promise.reject(err);
        });
    }

    /**
     * Updat last downloaded fragment index for representationId
     * @param {number} manifestId - manifest id
      * @param {string} representationId - representation
     * @param {number} state - representation state
     * @returns {Promise} promise
     * @instance
     */
    function setRepresentationCurrentState(manifestId, representationId, state) {
        return getManifestById(manifestId).then(function (item) {
            if (!item.state) {
                item.state = {};
            }

            if (!item.state[representationId]) {
                item.state[representationId] = {
                    index: -1,
                    downloaded: 0
                };
            }

            item.state[representationId] = state;
            return updateManifest(item).catch(function () {
                return Promise.reject('Cannot set current index for represenation id ' + representationId);
            });
        }).catch(function (err) {
            return Promise.reject(err);
        });
    }

    /**
     * Returns current downloaded segment index for representation
     * @param {number} manifestId - manifest id
     * @param {string} representationId - representation
     * @returns {Promise} promise
     * @instance
     */
    function getRepresentationCurrentState(manifestId, representationId) {
        return getManifestById(manifestId).then(function (item) {
            let state = {
                index: -1,
                downloaded: 0
            };
            if (item.state && item.state[representationId]) {
                state = item.state[representationId];
            }
            return Promise.resolve(state);
        }).catch(function (err) {
            return Promise.reject(err);
        });
    }

    /**
     * Returns a fragment from its key
     * @param {number} manifestId
     * @param {number} key
     * @returns {Promise} fragment
     * @instance
     */
    function getFragmentByKey(manifestId, key) {
        let fragmentStore = fragmentStores[manifestId];

        if (!fragmentStore) {
            return Promise.reject(new Error (`No fragment store found for manifest ${manifestId}`));
        }

        return fragmentStore.getItem(key).then(function (value) {
            return Promise.resolve(value);
        }).catch(function (err) {
            return Promise.reject(err);
        });

    }

    /**
     * Returns a manifest from its identifier
     * @param {number} id
     * @returns {Promise} {Object[]} manifests
     * @instance
     */
    function getManifestById(id) {
        return getAllManifests().then(function (array) {
            if (array) {
                let item = null;
                for (let i = 0; i < array.manifests.length; i++) {
                    if (array.manifests[i].manifestId === parseInt(id)) {
                        item = array.manifests[i];
                    }
                }
                if (item !== null) {
                    item.manifest = entities.decode(item.manifest);
                    return Promise.resolve(item);
                } else {
                    return Promise.reject('Cannot found manifest with this manifestId : ' + id);
                }
            } else {
                return Promise.reject('Any manifests stored in DB !');
            }
        }).catch(function (err) {
            return Promise.reject(err);
        });
    }

    /**
     * Returns all offline manifests
     * @returns {Promise} {Object[]} manifests
     * @instance
     */
    function getAllManifests() {
        return manifestStore.getItem('manifest').then(function (array) {
            return Promise.resolve(array ? array : {
                'manifests': []
            });
        }).catch(function (err) {
            return Promise.reject(err);
        });
    }

    /**
     * Return higher manifest id
     * @returns {Promise} number
     * @instance
     */
    function getCurrentHigherManifestId() {
        return getAllManifests().then(function (array) {
            let higherManifestId = 0;
            if (array) {
                for (let i = 0; i < array.manifests.length; i++) {
                    if (array.manifests[i].manifestId > higherManifestId) {
                        higherManifestId = array.manifests[i].manifestId;
                    }
                }
                return Promise.resolve(higherManifestId);
            } else {
                return Promise.resolve(higherManifestId);
            }
        }).catch(function (err) {
            return Promise.reject(err);
        });
    }

    /**
     * Update manifest
     * @param {Object} manifest updated manifest
     * @returns {Promise} promise asynchronously resolved
     * @instance
     */
    function updateManifest(manifest) {
        return getAllManifests().then(function (array) {
            try {
                for (let i = 0; i < array.manifests.length; i++) {
                    if (array.manifests[i].manifestId === manifest.manifestId) {
                        array.manifests[i] = manifest;
                    }
                }
                return manifestStore.setItem('manifest', array);
            } catch (err) {
                throw new Error('Any results found !');
            }
        });
    }

    /**
     * save selected representation by user
     * @param {Object} manifest updated manifest
     * @param {Object} selected selected representations
     * @returns {Promise} promise asynchronously resolved
     * @instance
     */
    function saveSelectedRepresentations(manifest, selected) {
        return getManifestById(manifest).then(function (item) {
            if (!item.selected) {
                item.selected = {};
            }

            item.selected = selected;
            return updateManifest(item).catch(function () {
                return Promise.reject('Cannot save selected representations');
            });
        }).catch(function (err) {
            return Promise.reject(err);
        });
    }

    /**
     * Store a manifest in manifest array
     * @param {Object} manifest
     * @instance
     */
    function storeManifest(manifest) {
        return manifestStore.getItem('manifest').then(function (results) {
            let array = results ? results : {
                'manifests': []
            };
            array.manifests.push(manifest);
            return manifestStore.setItem('manifest', array);
        });
    }

    /**
     * Store a fragment in fragment store
     * @param {number} manifestId
     * @param {number} fragmentId
     * @param {Object} fragmentData
     * @returns {Promise} promise asynchronously resolved
     * @instance
     */
    function storeFragment(manifestId, fragmentId, fragmentData) {
        let fragmentStore = fragmentStores[manifestId];

        if (!fragmentStore) {
            return Promise.reject(new Error (`No fragment store found for manifest ${manifestId}`));
        }

        return fragmentStore.setItem(fragmentId, fragmentData, function () {
            return Promise.resolve();
        }).catch(function (err) {
            return Promise.reject(err);
        });
    }

    /////////////////////////////////////////
    //
    // DROP Methods
    //
    ////////////////////////////////////////

    /**
     * Remove all manifest and fragment store
     * @returns {Promise} promise asynchronously resolved
     * @instance
     */
    function dropAll() {
        return localforage.clear().then(function () {
            return Promise.resolve();
        }).catch(function (err) {
            return Promise.reject(err);
        });
    }

    /**
     * Remove framgent store given its name
     * @param {string} storeName
     * @instance
     */
    function dropFragmentStore(storeName) {
        localforage.dropInstance({
            driver: localforage.INDEXEDDB,
            name: 'dash_offline_db',
            version: 1.0,
            storeName: storeName
        }).then(function () {
            delete fragmentStores[storeName];
        }).catch(function (err) {
            console.log('dropFragmentStore failed ' + err);
        });
        return;
    }

    /**
     * Remove download given its id (fragmentStore + manifest entry in manifest array)
     * @param {number} manifestId
     * @returns {Promise} promise asynchronously resolved
     * @instance
     */
    function deleteDownloadById(manifestId) {
        return manifestStore.getItem('manifest').then(function (array) {
            if (array) {
                return deleteFragmentStore(manifestId).then(function () {
                    for (let i = 0; i < array.manifests.length; i++) {
                        if (array.manifests[i].manifestId === parseInt(manifestId)) {
                            array.manifests.splice(i, 1);
                        }
                    }
                    return manifestStore.setItem('manifest', array).then(function () {
                        return Promise.resolve('This stream has been successfull removed !');
                    }).catch(function () {
                        return Promise.reject('An error occured when trying to delete this manifest');
                    });
                });
            } else {
                return Promise.resolve('Nothing to delete !');
            }
        }).catch(function (err) {
            return Promise.reject(err);
        });
    }

    /**
     * Remove fragment store
     * @param {string} storeName
     * @returns {Promise} promise asynchronously resolved
     * @instance
     */
    function deleteFragmentStore(storeName) {
        localforage.createInstance({
            name: 'dash_offline_db',
            storeName: storeName
        });
        return localforage.dropInstance({
            name: 'dash_offline_db',
            storeName: storeName
        }).then(function () {
            delete fragmentStores[storeName];
            return Promise.resolve();
        }).catch(function (err) {
            console.log(err);
            return Promise.reject(err);
        });

    }


    setup();

    instance = {
        dropAll: dropAll,
        getFragmentByKey: getFragmentByKey,
        getManifestById: getManifestById,
        storeFragment: storeFragment,
        storeManifest: storeManifest,
        updateManifest: updateManifest,
        saveSelectedRepresentations: saveSelectedRepresentations,
        createFragmentStore: createFragmentStore,
        setDownloadingStatus: setDownloadingStatus,
        setRepresentationCurrentState: setRepresentationCurrentState,
        getRepresentationCurrentState: getRepresentationCurrentState,
        getCurrentHigherManifestId: getCurrentHigherManifestId,
        getAllManifests: getAllManifests,
        dropFragmentStore: dropFragmentStore,
        deleteDownloadById: deleteDownloadById
    };

    return instance;
}

IndexDBStore.__dashjs_factory_name = 'IndexDBStore';
export default dashjs.FactoryMaker.getSingletonFactory(IndexDBStore); /* jshint ignore:line */