Skip to content

Commit

Permalink
feat(local storage): support caching user preferences (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dan Ziv authored Sep 11, 2017
1 parent 81ba2bf commit 2e4280a
Show file tree
Hide file tree
Showing 9 changed files with 377 additions and 22 deletions.
1 change: 0 additions & 1 deletion src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
beUrl: "http://qa-apache-php7.dev.kaltura.com/api_v3"
}
};

try {
var kalturaPlayer = KalturaPlayer.setup('player-placeholder', config);
kalturaPlayer.loadMedia('0_wifqaipd');
Expand Down
26 changes: 19 additions & 7 deletions src/setup.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @flow
import {loadPlayer} from 'playkit-js'
import {loadPlayer, Utils} from 'playkit-js'
import KalturaPlayer from './kaltura-player'
import StorageManager from './storage/storage-manager'
import {
extractPlayerConfig,
extractProvidersConfig,
Expand All @@ -18,13 +19,24 @@ import {
function setup(targetId: string, options: Object): KalturaPlayer {
validateTargetId(targetId);
validateProvidersConfig(options);
let playerConfig = extractPlayerConfig(options);
let providersConfig = extractProvidersConfig(options);
let userPlayerConfig = extractPlayerConfig(options);
let userProvidersConfig = extractProvidersConfig(options);
let containerId = createKalturaPlayerContainer(targetId);
checkNativeHlsSupport(playerConfig);
let player = loadPlayer(containerId, playerConfig);
let kalturaPlayer = new KalturaPlayer(player, containerId, providersConfig);
return Object.assign(player, kalturaPlayer);
checkNativeHlsSupport(userPlayerConfig);
let player = loadPlayer(containerId, userPlayerConfig);
let kalturaPlayerApi = new KalturaPlayer(player, containerId, userProvidersConfig);
let kalturaPlayer = Object.assign(player, kalturaPlayerApi);
if (StorageManager.isLocalStorageAvailable()) {
let storageManager = new StorageManager();
storageManager.attach(kalturaPlayer);
if (!options.disableUserCache && storageManager.hasStorage()) {
let storageConfig = storageManager.getStorage();
let storageAndUserPlayerConfig = {};
Utils.Object.mergeDeep(storageAndUserPlayerConfig, storageConfig, userPlayerConfig);
kalturaPlayer.configure(storageAndUserPlayerConfig);
}
}
return kalturaPlayer;
}

export {setup};
93 changes: 93 additions & 0 deletions src/storage/storage-manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// @flow
import StorageWrapper from './storage-wrapper'
import LoggerFactory from '../utils/logger'
import {name} from '../../package.json'

const STORAGE_PREFIX = name + '_';

export default class StorageManager {
static StorageKeys = [
'muted',
'volume',
'textLanguage',
'audioLanguage'
];
_storage: StorageWrapper;
_player: Player;
_logger: any;

static isLocalStorageAvailable(): boolean {
return StorageWrapper.isLocalStorageAvailable();
}

constructor() {
this._storage = new StorageWrapper(STORAGE_PREFIX);
this._logger = LoggerFactory.getLogger('StorageManager');
}

/**
* Attaches the player listeners to the local storage wrapper.
* @param {Player} player - The player reference.
* @returns {void}
*/
attach(player: Player): void {
this._logger.debug('Attach local storage');
this._player = player;
this._player.addEventListener(player.Event.VOLUME_CHANGE, () => {
this._storage.setItem('muted', this._player.muted);
this._storage.setItem('volume', this._player.volume);
});
this._player.addEventListener(player.Event.AUDIO_TRACK_CHANGED, (event) => {
let audioTrack = event.payload.selectedAudioTrack;
this._storage.setItem('audioLanguage', audioTrack.language);
});
this._player.addEventListener(player.Event.TEXT_TRACK_CHANGED, (event) => {
let textTrack = event.payload.selectedTextTrack;
this._storage.setItem('textLanguage', textTrack.language);
});
}

/**
* Checks if we have previous storage.
* @return {boolean} - Whether we have previous storage.
*/
hasStorage(): boolean {
let storageSize = this._storage.size;
let hasStorage = (storageSize !== 0);
if (hasStorage) {
this._logger.debug('Storage found with size of ', storageSize);
} else {
this._logger.debug('No storage found');
}
return hasStorage;
}

/**
* Gets the storage in the structure of the player configuration.
* @return {Object} - Partial storageable player configuration.
*/
getStorage(): Object {
let values = this._getExistingValues();
let storageConfig = this._buildStorageConfig(values);
this._logger.debug('Gets storage config', storageConfig);
return storageConfig;
}

_getExistingValues(): Object {
let obj = {};
for (let i = 0; i < StorageManager.StorageKeys.length; i++) {
let key = StorageManager.StorageKeys[i];
let value = this._storage.getItem(key);
if (value != null) {
obj[key] = value;
}
}
return obj;
}

_buildStorageConfig(values: Object): Object {
return {
playback: values
};
}
}
103 changes: 103 additions & 0 deletions src/storage/storage-wrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// @flow
import LoggerFactory from '../utils/logger'

export default class StorageWrapper {
_prefix: string;
_logger: any;

static isLocalStorageAvailable(): boolean {
if (typeof Storage !== 'undefined') {
try {
localStorage.setItem('test', 'test');
localStorage.removeItem('test');
return true;
}
catch (e) {
return false;
}
} else {
return false;
}
}

constructor(prefix: string = '') {
this._logger = LoggerFactory.getLogger('StorageWrapper');
this._prefix = prefix;
}

/**
* @return {number} - The number of keys in the local storage started with wanted prefix.
*/
get size(): number {
return Object.keys(localStorage).filter((key) => key.startsWith(this._prefix)).length;
}

/**
* Sets an item in the local storage.
* @param {string} key - The key of the item.
* @param {any} item - The value of the item.
* @returns {void}
*/
setItem(key: string, item: any): void {
StorageWrapper._validateKey(key);
try {
this._logger.debug('Sets item for key: ' + key, item);
localStorage.setItem(this._prefix + key, item);
} catch (e) {
if (StorageWrapper._isQuotaExceeded(e)) {
this._logger.error('Quota exceeded: ' + e.message);
} else {
this._logger.error(e.message);
}
}
}

/**
* Gets an item from the local storage.
* @param {string} key - The item key.
* @returns {any} - The item value.
*/
getItem(key: string): any {
StorageWrapper._validateKey(key);
let item = null;
try {
item = localStorage.getItem(this._prefix + key);
if (typeof item === 'string') {
return JSON.parse(item);
} else {
return null;
}
} catch (e) {
return item;
}
}

static _isQuotaExceeded(e: any): boolean {
let quotaExceeded = false;
if (e) {
if (e.code) {
switch (e.code) {
case 22:
quotaExceeded = true;
break;
case 1014:
// Firefox
if (e.name === 'NS_ERROR_DOM_QUOTA_REACHED') {
quotaExceeded = true;
}
break;
}
// Internet Explorer 8
} else if (e.number === -2147024882) {
quotaExceeded = true;
}
}
return quotaExceeded;
}

static _validateKey(key: string): void {
if (typeof key !== 'string' || key.length === 0) {
throw new Error('Invalid key');
}
}
}
23 changes: 12 additions & 11 deletions src/utils/logger.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
//@flow
import * as JsLogger from 'js-logger';

class LoggerFactory {
const LOG_LEVEL: { [level: string]: Object } = {
"DEBUG": JsLogger.DEBUG,
"INFO": JsLogger.INFO,
"TIME": JsLogger.TIME,
"WARN": JsLogger.WARN,
"ERROR": JsLogger.ERROR,
"OFF": JsLogger.OFF
};

class Logger {
constructor(options?: Object) {
JsLogger.useDefaults(options || {});
}
Expand All @@ -14,15 +23,7 @@ class LoggerFactory {
}
}

const Logger = new LoggerFactory({defaultLevel: JsLogger.DEBUG});
const LOG_LEVEL: { [level: string]: Object } = {
"DEBUG": JsLogger.DEBUG,
"INFO": JsLogger.INFO,
"TIME": JsLogger.TIME,
"WARN": JsLogger.WARN,
"ERROR": JsLogger.ERROR,
"OFF": JsLogger.OFF
};
const LoggerFactory = new Logger({defaultLevel: JsLogger.DEBUG});

export default Logger;
export default LoggerFactory;
export {LOG_LEVEL};
100 changes: 100 additions & 0 deletions test/src/storage/storage-manager.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import StorageManager from '../../../src/storage/storage-manager'
import StorageWrapper from "../../../src/storage/storage-wrapper";

describe('StorageManager', function () {
let storageManager;
let sandbox;

beforeEach(function () {
sandbox = sinon.sandbox.create();
});

afterEach(function () {
storageManager = null;
sandbox.restore();
window.localStorage.clear();
});

it('should return it has no storage', function () {
sandbox.stub(StorageWrapper.prototype, '_testForLocalStorage').callsFake(() => {
StorageWrapper.prototype._isLocalStorageAvailable = true;
});
sandbox.stub(StorageWrapper.prototype, 'size').get(() => {
return 0;
});
storageManager = new StorageManager();
storageManager.hasStorage().should.be.false;
});

it('should return it has storage', function () {
sandbox.stub(StorageWrapper.prototype, '_testForLocalStorage').callsFake(() => {
StorageWrapper.prototype._isLocalStorageAvailable = true;
});
sandbox.stub(StorageWrapper.prototype, 'size').get(() => {
return 1;
});
storageManager = new StorageManager();
storageManager.hasStorage().should.be.true;
});

it('should return config for volume', function () {
sandbox.stub(StorageWrapper.prototype, '_testForLocalStorage').callsFake(() => {
StorageWrapper.prototype._isLocalStorageAvailable = true;
});
sandbox.stub(StorageWrapper.prototype, 'size').get(() => {
return 1;
});
sandbox.stub(StorageWrapper.prototype, 'getItem').withArgs('volume').returns(1);
storageManager = new StorageManager();
storageManager.getStorage().should.deep.equal({
playback: {
volume: 1
}
});
});

it('should return config for all properties', function () {
sandbox.stub(StorageWrapper.prototype, '_testForLocalStorage').callsFake(() => {
StorageWrapper.prototype._isLocalStorageAvailable = true;
});
let getItemStub = sandbox.stub(StorageWrapper.prototype, 'getItem');
getItemStub.withArgs('volume').returns(0.5);
getItemStub.withArgs('muted').returns(false);
getItemStub.withArgs('textLanguage').returns('heb');
getItemStub.withArgs('audioLanguage').returns('eng');
storageManager = new StorageManager();
storageManager.getStorage().should.deep.equal({
playback: {
volume: 0.5,
muted: false,
textLanguage: 'heb',
audioLanguage: 'eng'
}
});
});

it('should attaches listeners', function () {
sandbox.stub(StorageWrapper.prototype, '_testForLocalStorage').callsFake(() => {
StorageWrapper.prototype._isLocalStorageAvailable = true;
});
let fakePlayer = {
listeners: [],
Event: {
VOLUME_CHANGE: 'volumechange',
AUDIO_TRACK_CHANGED: 'audiotrackchanged',
TEXT_TRACK_CHANGED: 'texttrackchanged'
},
addEventListener: function (eventName) {
this.listeners.push(eventName);
}
};
storageManager = new StorageManager();
storageManager.attach(fakePlayer);
fakePlayer.listeners.should.have.length.of(3);
fakePlayer.listeners.should.deep.equal([
'volumechange',
'audiotrackchanged',
'texttrackchanged'
]);
});
});
Loading

0 comments on commit 2e4280a

Please sign in to comment.