anghami-audio-manager
Version:
Web audio manager
1,408 lines (1,222 loc) • 56.1 kB
text/typescript
declare var module: any;
declare var define :any;
let w: any = this;
let d: any = w.document;
const a: string = 'audio';
const v: string = 'video';
const s: string = 'script';
"use strict";
class PlatformDetector {
private regex: Object = {
platform: {
playstation: new RegExp('^Mozilla/[0-9].[0-9]\\s.(PlayStation [3-5]).*\\sSCEE/[1-9].[0-9]\\sNuanti/[1-9].[0-9]'),
mobile: new RegExp('Mobile|Android|Tab|Tablet|GT|SM-(T|P)|Silk-Accelerated', 'i'),
tablet: new RegExp('Tab|Tablet|iPad|SM-(T|P)|GT|Silk-Accelerated', 'i'),
desktop: new RegExp('(?:(?!Mobile|Android|Tab|Tablet|GT|SM-(T|P)|Silk-Accelerated).)', 'i')
},
os: {
android: new RegExp('Linux.*Android'),
ios: new RegExp('like\\sMac\\sOS\\sX'),
mac: new RegExp('Intel\\sMac\\sOS\\sX'),
windows: new RegExp('Windows\\sNT')
},
browser: {
ie: new RegExp('msie|trident', 'i'),
edge: new RegExp('Edge'),
chrome: new RegExp('Chrome'),
firefox: new RegExp('Firefox'),
safari: new RegExp('Safari')
}
};
private canInit: boolean = false;
constructor() {
if (w.navigator && w.navigator.userAgent && w['RegExp'] && typeof w['RegExp'] === 'function') {
this.canInit = true;
} else {
this.throwErr();
}
}
public detectDevice(): Object {
return {'platform': this.detect('platform'), 'os': this.detect('os'), 'browser': this.detect('browser')};
}
public detect(type): string {
if (this.canInit) {
let obj = this.regex[type];
let regxName: string;
for (regxName in obj) {
if (obj[regxName].test(w.navigator.userAgent)) {
return regxName;
}
}
} else {
this.throwErr();
}
}
private throwErr(): void {
if (w['Error'] && typeof w['Error'] === 'function') {
throw new Error("This browser doesn't supprot platform detection.");
} else {
console.error("This browser doesn't supprot platform detection.");
}
}
}
interface GalacticInterface {
list: any;
shuffleList: any;
playNextList: Array<any>;
currentIndex: number;
shuffleIndex: number;
options: any;
currentMediaItem: Object;
audioElement: HTMLAudioElement
//audioSourceElement: HTMLSourceElement;
currentSrc: string;
audioElementEvents: {};
progress: number;
position: number;
remainingTime: number;
lastPosition: number;
bufferProgress: number;
lastBufferProgress: number;
audioTrackPlaying: boolean
audioTrackOnload: boolean;
errorTimeout: any;
interval: any;
error: boolean;
metadata: boolean;
isRepeating: boolean;
isShuffling: boolean;
isPaused: boolean;
eventMap: Object;
duration: number;
isReloading: boolean;
mimeRegx: RegExp;
device: any;
windows: boolean;
playstation: boolean;
onlinecbreload: boolean;
playState: boolean;
emptyWaveFileBase64: string;
emptyVideoFileBase64: string;
playPromise: any;
forceDurationChangeForAudioAd: boolean;
script: HTMLScriptElement;
hasHlsScript: HTMLScriptElement;
forcePause: boolean;
setup(options?: any): any;
init(): any;
destroy(): any;
isValid(): boolean;
load(withSrc?: any, afterReload?: boolean): any;
reload(time: number): any;
onBufferChange(e: Event): void;
onDurationChange(e: Event): void;
play(): any;
pause(): any;
stop(callback?: Function): any;
next(prop: string, callback?: Function): any;
previous(prop: string, callback?: Function): any;
repeat(callback?: Function): any;
flush(): any;
loadAudio(): any;
setSrc(src: string): any;
setPosition(position: number): any;
setTimeValues(progress: number, remainingTime: number, position: number): void;
setVolume(volume: number): any;
dispatchEvent(type: string, args: Object): void;
catchErrorVulnerability(e: Event): void;
setNextPlayingListItem(mediaItem: Object): any;
getNextPlayingListItem(): Object;
removeNextPlayingListItem(): any;
hasNextPlayingListItem(): boolean;
applyNextAction(): any;
on(type: string, callback: Function): void;
off(type: string, filler: any): void;
shuffle(callback?: Function): any;
createShuffleList(list: Array<any>): Array<any>;
toggle(prop: string): any;
isValid(): boolean;
isBufferValid(): boolean;
hasBuffer(): boolean;
waitUntilOnline(): any;
clearWaiters(): any;
setAttribute(type: string, value: string): any;
loadEmpty(): any;
addToPlaylist(data: any, type?: string, callback?: Function): any;
createElement (parent: HTMLElement, type: string): any;
addEventListeners (element: HTMLElement, events: Object): any;
setOptions(options: Object): any;
//createMediaHelper(): void;
}
class Galactic implements GalacticInterface {
"use strict";
/*
* Media Item & Lists
*/
public currentMediaItem: Object;
public list: any;
public shuffleList: any;
public playNextList: Array<any>;
/*
* Main Used Indexes
*/
public currentIndex: number;
public shuffleIndex: number;
/*
* Main Elements
*/
public audioElement: HTMLAudioElement;
//public audioSourceElement: HTMLSourceElement;
/*
* Audio Source URL
*/
public currentSrc: string;
/*
* Main Events Objects
*/
public audioElementEvents: Object;
public eventMap: Object;
/*
* Main Options
*/
public options: any;
/*
* To Handle the case of loading for mobile and handle all mobile browsers exceptions & warnings
*/
public loadEmpty: any;
private emptyWaveFileBase64: string;
/*
* Main Regex for to check for audio Type
*/
private mimeRegx: RegExp;
/*
* Audio State Flags
*/
private playState: boolean;
public audioTrackPlaying: boolean;
private audioTrackOnload: boolean;
public isRepeating: boolean;
public isShuffling: boolean;
public isPaused: boolean;
private isReloading: boolean;
private metadata: boolean;
/*
* Handle Error and interval for reloading and on online callback
*/
private errorTimeout: any;
private interval :any;
private onlinecbreload: boolean;
private online: boolean;
private error: boolean;
/*
* Devices & Platfornms Falgs
*/
private device: any;
private windows: boolean;
private playstation: boolean;
/*
* Handle Data About Time, Duration, Position, etc
*/
private progress: number;
private position: number;
private remainingTime: number;
private bufferProgress: number;
private lastBufferProgress: number;
private lastPosition: number;
private duration: number;
/*
* This promise object that is retuned when the browser has safely played the media element
* It is mainly used to play() pause() load() safely and to handle exceptions thrown by the browser
*/
private playPromise: any;
/*
* Audio Ads Data Management
* Used to handle sending duration for the audio ad of type m4a
***************************
TO-DO: check this
***************************
*/
public forceDurationChangeForAudioAd: boolean;
private hlsjsErrorHandler: any;
private hlsVideoTagErrorHandler: any;
private isUsingHLSPolyfill: boolean;
private hls: any;
private script: HTMLScriptElement;
private hasHlsScript: HTMLScriptElement;
private tempElRef: HTMLElement;
private emptyVideoFileBase64: string;
private forcePause: boolean;
constructor() {
this.setup();
}
/****************************************************************************************************************************
* Boilerplate: set default values
*/
private setup(): any {
//Main Audio Events
this.audioElementEvents = {
abort: (e) => {
console.log('The abort event occurs when the loading of an audio/video is aborted. Sent when playback is aborted; for example, if the media is playing and is restarted from the beginning, this event is sent.');
this.checkError(e);
},
stalled: (e) => {
console.log('browser is trying to get media data, but data is not available');
this.checkError(e); //when it's not paused
},
suspend: (e) => {
console.log('loading of the media is suspended (prevented from continuing). This can happen when the download has completed, or because it has been paused for some reason.');
if (this.bufferProgress <= 98) {
this.checkError(e);
}
},
error: (e) => {
console.log('an error occurred during the loading of an audio error: ' + e.target.error.code + ' ' + e.target.error.message);
this.checkError(e);
if (this.options.video && this.isUsingHLSPolyfill) {
const mediaError: any = e.currentTarget.error;
if (mediaError.code === mediaError.MEDIA_ERR_DECODE) {
this.hlsVideoTagErrorHandler();
}
}
},
emptied: (e) => {
// *** TO DO *** // check for error in this state
this.audioTrackPlaying = undefined;
},
waiting: (e) => {
this.audioTrackPlaying = false;
console.log('audio stops because it needs to buffer the next frame.');
this.checkError(e);
this.hasBuffer() && this.onBufferChange(e);
this.dispatchEvent('wait', {status: this.audioTrackPlaying});
},
loadedmetadata: (e) => {
console.log('loaded meta data for audio/video consists of: duration, dimensions (video only) and text tracks.');
this.metadata = true;
},
loadstart: (e) => {
this.audioTrackPlaying = false;
console.log('the browser is currently looking for the specified audio/video');
this.dispatchEvent('wait', {status: this.audioTrackPlaying});
this.hasBuffer() && this.onBufferChange(e);
},
durationchange: (e) => {
// if (this.forcePause && !this.audioElement.paused) {
// this.pause(true);
// }
console.log('when an audio/video is loaded, the duration will change from "NaN" to the actual duration of the audio/video');
this.isValid() && this.onDurationChange(e);
if (this.lastPosition) {
this.setPosition(this.lastPosition);
this.lastPosition = undefined;
}
this.duration = e.target.duration;
},
timeupdate: (e) => {
// if (this.forcePause && !this.audioElement.paused) {
// this.pause(true);
// }
// if (this.progress > 3 && this.progress < 5 && this.audioElement.muted) {
// console.log('UNMUTING UNMUTING UNMUTING UNMUTING UNMUTING UNMUTING UNMUTING', this.audioElement.volume, this.audioElement.muted, this.progress, this);
// try { this.removeAttribute('muted')
// .setAttribute('muted', 'false')
// .setVolume(1);
// this.tempElRef = d.getElementsByTagName(v)[0];
// this.tempElRef.muted = false;
// this.tempElRef.play();
// delete this.tempElRef;
// } catch(e){}
// }
// if (this.options.video && this.audioElement.muted) {
// // this.removeAttribute('muted')
// // .setAttribute('muted', 'false')
// // .setVolume(1);
// // this.tempElRef = d.getElementsByTagName(v)[0];
// // this.tempElRef.muted = false;
// // delete this.tempElRef;
// }
console.log('playing position of an audio/video has changed.', this);
this.playState = true;
if (this.isValid()) {
this.onDurationChange(e);
this.audioTrackPlaying = true;
}
this.hasBuffer() && this.onBufferChange(e);
if (this.isReloading) {
this.isReloading = this.error = false;
}
this.bufferProgress === 100 && this.clearWaiters(); //kill waiters in case it is fully downloaded
},
canplay: (e) => {
if (this.forcePause && !this.audioElement.paused) {
this.pause(undefined, true);
}
console.log('can start playing the audio/video, enough has loaded to play...');
this.dispatchEvent('readyToPlay', {track: this.currentMediaItem, src: this.currentSrc, progress: this.progress});
if (this.options.playOnLoad) {
// alert('play from can play');
this.play();
}
// if (!this.options.video) {
// // this.audioTrackPlaying = true;
// console.log('playing from canplay');
// this.play();
// }
},
playing: (e) => {
if (this.forcePause && !this.audioElement.paused) {
this.pause(undefined, true);
}
console.log('audio is playing after having been paused or stopped for buffering');
//this.audioTrackPlaying = true;
this.isValid() && this.dispatchEvent('play', {status: this.audioTrackPlaying});
this.hasBuffer() && this.onBufferChange(e);
},
pause: (e) => {
this.forcePause = false;
this.isPaused = true;
this.audioTrackPlaying = false;
this.isValid() && this.dispatchEvent('pause', {status: this.audioTrackPlaying});
},
ended: (e) => {
this.isValid() && this.dispatchEvent('end', {progress: this.progress});
},
progress: (e) => {
console.log('downloading the specified audio/video... ');
this.hasBuffer() && this.onBufferChange(e);
},
ratechange: (e) => {
this.isValid() && this.dispatchEvent('rateChange');
},
volumechange: (e) => {
this.isValid() && this.dispatchEvent('volumeChange');
},
seeking: (e) => {
this.pause();
},
seeked: (e) => {
if (this.options.playOnLoad) {
this.play();
}
}
// loadeddata: (e) => {
// console.log('data for the current frame is loaded, but not enough data to play next frame of the specified audio/video ');
// },
// play: (e) => {
// console.log('audio has been started or is no longer paused');
// },
// canplaythrough: (e) => {
// console.log('enough has loaded to play without buffering...');
// },
};
//Default options
this.options = {
html5: false,
reloadOnInit: 10000,
reloadOnError: 5000,
volume: 1,
fadeIn: false,
hls: false
};
this.emptyVideoFileBase64 = 'data:application/x-mpegURL;base64,UklGRjIAAABXQVZFZm10IBIAAAABAAEAQB8AAEAfAAABAAgAAABmYWN0BAAAAAAAAABkYXRhAAAAAA==';
this.emptyWaveFileBase64 = 'data:audio/wave;base64,UklGRjIAAABXQVZFZm10IBIAAAABAAEAQB8AAEAfAAABAAgAAABmYWN0BAAAAAAAAABkYXRhAAAAAA==';
this.mimeRegx = new RegExp(/\.m4a|mp4|mpeg4|aac|flv|mov|m4v|f4v|.m4b|mp4v|3gp|3g2|mp4|m3u8/);
//Objects & arrays
this.eventMap = {};
this.list = this.playNextList = this.shuffleList = [];
this.currentMediaItem = {};
//Default values
this.online = true;
this.bufferProgress = this.progress = this.lastBufferProgress = this.position = this.remainingTime = this.currentIndex = this.shuffleIndex = this.duration = 0;
this.audioElement = this.errorTimeout = this.lastPosition = this.error = this.metadata = this.isRepeating = this.isShuffling = this.isPaused = this.isReloading = this.interval = this.onlinecbreload = this.playState = this.forceDurationChangeForAudioAd = this.playPromise = undefined;
//Device detections
this.device = new PlatformDetector().detectDevice();
this.windows = (this.device.browser === 'ie'); //(this.device.os === 'windows');
this.playstation = (this.device.platform === 'playstation');
//Load empty for mobile browsers
this.loadEmpty = (() => {
return () => {
this.forcePause = false;
this.load(false);//.play();
//return this;
};
})();
if (!this.windows && !this.playstation) {
w.addEventListener('offline', this.offlinecb, false);
w.addEventListener('online', this.onlinecb, false);
}
}
/****************************************************************************************************************************/
private appendHLSEvents (): void {
// update live status on level load
// this.hls.on(w.Hls.Events.LEVEL_LOADED, function(event, data) {
// duration = data.details.live ? Infinity : data.details.totalduration;
// });
this.hls.on(w.Hls.Events.MANIFEST_PARSED, () => {
console.log('playing from manifest');
if (this.options.playOnLoad) {
// alert('play from appendHLSEvents');
this.play();
}
});
// try to recover on fatal errors
this.hls.on(w.Hls.Events.ERROR, (event: Object, data: any) => {
if (data.fatal) {
switch (data.type) {
case w.Hls.ErrorTypes.NETWORK_ERROR:
this.hls.startLoad();
break;
case w.Hls.ErrorTypes.MEDIA_ERROR:
this.hlsjsErrorHandler();
break;
default:
console.error('Error loading media: File could not be played');
this.destoryHLS();
break;
}
}
});
}
/****************************************************************************************************************************
* Main Initializer
*/
public init(options?: any): any {
this.setOptions(options);
this.createMedia();
return this;
}
/****************************************************************************************************************************/
/****************************************************************************************************************************
* Connecitivty Manager: Methods Needed
*/
private isOnline(): boolean {
// Handle IE and more capable browsers
let xhr: XMLHttpRequest = new XMLHttpRequest();// (w.XMLHttpRequest)( "Microsoft.XMLHTTP" );
// Open new request as a HEAD to the root hostname with a random param to bust the cache
xhr.open('HEAD', `//${w.location.hostname}/?rand=${w.Math.floor((1 + w.Math.random()) * 0x10000)}`, false); // secon argument async :
// Issue request and handle response
try {
xhr.send();
return (xhr.status >= 200 && (xhr.status < 300 || xhr.status === 304));
} catch (error) {
return false;
}
}
private waitUntilOnline(): any {
if (this.windows || this.playstation) {
this.interval = this.interval ? this.interval : w.setInterval(() => {
this.online = this.isOnline();
if (this.online) {
this.reload(0);
}
}, 2000);
} else {
this.onlinecbreload = true;
}
return this;
}
private onlinecb(): void {
console.log('is online...onlinecbreload: ', this.onlinecbreload);
this.online = true;
this.onlinecbreload && this.reload(0);
}
private offlinecb(): void {
console.log('is offline...');
this.online = false;
}
/****************************************************************************************************************************/
/****************************************************************************************************************************
* Buffer Progress Management
*/
private onBufferChange(e: any): void {
try {
let duration: number = e.target.duration;
let buffered: any = e.target.buffered;
let currentTime: number = e.target.currentTime;
let i: number = 0;
let buffLen: number = buffered.length;
this.lastBufferProgress = this.bufferProgress;
if (duration > 0 && buffLen > 0) {
for (; i < buffLen; i++) {
if (buffered.start(buffLen - 1 - i) < currentTime) {
this.bufferProgress = w.Math.ceil((buffered.end(buffLen - 1 - i) / duration) * 100);
break;
}
}
} else {
this.bufferProgress = undefined;
}
this.isBufferValid() && this.dispatchEvent('bufferChange', { bufferProgress: this.bufferProgress });
}
catch (e){}
}
/****************************************************************************************************************************/
/****************************************************************************************************************************
* Duration Management
*/
private onDurationChange(e: any): void {
let d: number = e.target.duration ? e.target.duration*1000 : 0;
let c: number = e.target.currentTime ? e.target.currentTime*1000 : 0;
let p: number = (c/d)*100;
//p: progress , d-c: remainingTime, c: position
this.setTimeValues(p, d-c, c);
}
private setTimeValues(progress: number, remainingTime: number, position: number): void {
this.progress = progress;
this.remainingTime = remainingTime;
this.position = position;
this.position > 0 && this.dispatchEvent('durationChange', { progress: this.progress, remainingTime: this.remainingTime, position: this.position, duration: this.audioElement.duration, status: this.audioTrackPlaying });
}
/****************************************************************************************************************************/
/****************************************************************************************************************************
* Seek To A Certain Position
*/
public setPosition(position: number): any {
if (this.playState) {
try { //for ie
this.audioElement.currentTime = Number(position) / 1000;
} catch(e) {}
}
return this;
}
/****************************************************************************************************************************/
/****************************************************************************************************************************
* Reload Management
*/
private reload(time: number, force?: boolean): any {
if(this.position > 1000) {
this.lastPosition = this.position;
}
this.errorTimeout = this.errorTimeout ? this.errorTimeout : w.setTimeout(() => {
if (this.online && !this.audioTrackPlaying && this.error) {
if (this.audioElement.readyState >= 2 && this.audioElement.networkState === 2) {
this.isReloading = true;
this.clearWaiters().play(true);
} else {
this.load(this.currentSrc, true);
}
} else {
this.error = false;
this.isReloading = false;
this.clearWaiters();
}
}, time);
}
/****************************************************************************************************************************/
/****************************************************************************************************************************
* Volume Management
*/
public setVolume(volume: number): any { // from 0 to 100
try {
if (volume > 1.0) {
return;
}
this.audioElement.volume = volume;
this.dispatchEvent('volumeChange', {"volume":this.audioElement.volume});
return this;
} catch(e) {}
}
public getVolume(): number {
return this.audioElement.volume;
}
/***************************************************************************************************************************/
/****************************************************************************************************************************
* Main Audio Functionality: Play, Pause, Shuffle, Next, Previous, Repeat
*/
private loadAudio(): any {
if (this.options.video && this.isUsingHLSPolyfill) {
this.createNewHlsContext();
this.hls.loadSource(this.currentSrc);
} else {
this.audioElement.load();
}
return this;
}
public load(withSrc?: any, afterReload?: boolean): any {
//dispatchEvent('wait', {status: this.audioTrackPlaying})
this.flush(true);
//if (!this.isUsingHLSPolyfill) {
afterReload && this.loadAudio();
//}
if (withSrc) {
this.setSrc(withSrc);
console.log('THE SOURCE IS: ', withSrc);
} else {
this.setSrc(this.options.video ? this.emptyVideoFileBase64 : this.emptyWaveFileBase64);
console.log('THE SOURCE IS: ', this.options.video ? this.emptyVideoFileBase64 : this.emptyWaveFileBase64);
}
// if (!this.isUsingHLSPolyfill) {
this.setAttribute('src', this.currentSrc); //this.options.html5 ? this.audioElement : this.audioSourceElement
// }
this.loadAudio();
return this;
}
public fadeIn (start: number, end: number, duration: number, cb?: Function): void {
end = Math.round(end);
let delta: number = end - start;
let startTime: number = null;
let t: number;
let factor: number;
let frame: any;
this.setVolume(0);
let tweenLoop: FrameRequestCallback = (time?: number) => {
if (!time) {
time = new Date().getTime();
}
if (startTime === null) {
startTime = time;
}
t = time - startTime;
factor = t / duration;
this.setVolume(start + delta * factor);
if (t < duration && this.getVolume() < end) {
console.log('animating ', this.getVolume());
frame = w.requestAnimationFrame(tweenLoop);
return;
}
w.cancelAnimationFrame(frame);
console.log('done animating ', this.getVolume());
if (cb) {
cb();
}
tweenLoop = delta = startTime = t = time = start = frame = null;
};
frame = w.requestAnimationFrame(tweenLoop);
}
public play(): any {
this.isPaused = false;
//this.audioTrackPlaying = undefined;
// if (!this.playstation && !this.options.video && !this.isReloading) { // //
// let isPlayingToPreventRaceCondition: boolean = this.audioElement.currentTime > 0 && !this.audioElement.paused && !this.audioElement.ended && this.audioElement.readyState >= 2;
// if (!isPlayingToPreventRaceCondition) {
// try {
// this.checkFadeIn();
// //https://developers.google.com/web/updates/2017/06/play-request-was-interrupted
// this.playPromise = this.audioElement.play();
// if (this.playPromise) {
// this.playPromise.then(_ => {
// //this.audioTrackPlaying = true;
// // if (this.options.video) {
// // this.audioElement.play();
// // }
// console.log('should play');
// })
// .catch(error => {});
// }
// } catch (e) {}
// }
// }
// else {
try {
this.checkFadeIn();
this.audioElement.play().catch(e=>{});
} catch(e) {}
// }
return this;
}
private checkFadeIn(): void {
if (this.options.fadeIn) {
this.fadeIn(0.0, 0.9, 5000);
}
}
public pause(forcePause?: boolean, force?: boolean): any {
if (forcePause) {
this.forcePause = true;
}
if (this.audioTrackPlaying || force) {
// if (!this.playstation && !this.options.video && !this.device.browser.match(/safari|edge/)) { //this.device.browser !== 'safari'
// if (this.playPromise) {
// //https://developers.google.com/web/updates/2017/06/play-request-was-interrupted
// this.playPromise.then(_ => {
// this.isPaused = true;
// this.audioElement.pause();
// })
// .catch(error => {
// this.isPaused = false;
// });
// }
// }
// else {
this.audioElement.pause();
// this.isPaused = true;
//}
}
return this;
}
public stop(callback?: Function): any {
this.pause().flush(true).dispatchEvent('durationChange', { progress: 0, remainingTime: 0, position: 0 });
this.lastPosition = undefined;
callback && callback();
return this;
}
public next(prop: string, callback?: Function): any {
this[prop] = (this[prop] === this.list.length - 1) ? 0 : ++this[prop];
this.currentMediaItem = this.list[this[prop]];
callback && callback({'track':this.currentMediaItem});
return this;
}
public previous(prop: string, callback?: Function): any {
this[prop] = (this[prop] <= 0) ? this.list.length - 1 : --this[prop];
this.currentMediaItem = this.list[this[prop]];
callback && callback({'track':this.currentMediaItem});
return this;
}
public repeat(callback?: Function): any {
!this.isRepeating && this.toggle('repeat');
this.currentMediaItem = this.list[this.currentIndex];
callback && callback({'track':this.currentMediaItem});
this.dispatchEvent('repeatStatusChange', {repeat:this.isRepeating,"track": this.currentMediaItem});
return this;
}
public createShuffleList(list): Array<any> {
let currentIndex = list.length, temporaryValue, randomIndex;
// While there remain elements to shuffle...
while (0 !== currentIndex) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
// And swap it with the current element.
temporaryValue = list[currentIndex];
list[currentIndex] = list[randomIndex];
list[randomIndex] = temporaryValue;
}
return list;
}
public shuffle(callback?: Function): any {
!this.isShuffling && this.toggle('shuffle');
this.shuffleList = (this.shuffleIndex === 0) ? this.createShuffleList(this.list) : this.shuffleList;
if (callback) {
this.currentMediaItem = this.shuffleList[this.shuffleIndex];
callback({ "track": this.currentMediaItem, "list": this.shuffleList });
}
this.dispatchEvent('shuffleStatusChange', {"shuffle":this.isShuffling,"track": this.currentMediaItem});
return this;
}
public addToPlaylist(data: any, type?: string, callback?: Function): any {
let copy: Array<any>;
if (typeof arguments[1] === 'string') {
if (data.constructor === Array) {
this[type] = copy = (data.length > 0) ? data : this[type];
} else {
this[type].push(data);
copy = this[type];
}
}
else {
if (data.constructor === Array) {
this.list = this.shuffleList = copy = (data.length > 0) ? data : this.list;
} else {
this.list.push(data);
this.shuffleList = copy = this.list;
}
}
callback && callback({"list":copy});
this.dispatchEvent('playlistChange', {"playlist":this.list,"shuffleList":this.shuffleList});
return this;
}
public clearPlaylist(): any {
this.list = this.shuffleList = [];
return this;
}
public setNextPlayingListItem(media: any): any {
if (media.constructor === Array) {
this.playNextList = this.playNextList.concat(media);
}
else {
this.playNextList.push(media);
}
return this;
}
public getNextPlayingListItem(): Object {
return this.playNextList[this.playNextList.length - 1];
}
// Remove the item played which is the last item
public removeNextPlayingListItem(): any {
// Remove the first item since the queue always plays the first
this.playNextList.splice(this.playNextList.length - 1, 1);
return this;
}
public hasNextPlayingListItem(): boolean {
return (this.playNextList.length > 0) ? true : false;
}
public applyNextAction(callback?: Function): any {
// this.stop();
if (this.hasNextPlayingListItem()) {
let call: Function = (mediaItem: Object) => {
this.removeNextPlayingListItem();
this.currentMediaItem = mediaItem;
callback({"track":this.currentMediaItem});
};
let track: any = this.getNextPlayingListItem();
if (this.isShuffling) {
this.shuffleList.forEach((mediaItem) => {
if (mediaItem.id == track.id) {
call(mediaItem);
}
});
} else {
call(track);
}
}
else if (this.isRepeating) {
this.repeat(callback);
}
else if (this.isShuffling) {
this.shuffle().next('shuffleIndex', callback);
}
else {
this.next('currentIndex', callback);
}
return this;
}
// This will be called from a click event: since the player always moves forward so that's why we toggle repeat by default
public applyPreviousAction(callback?: Function): any {
// this.stop();
this.isRepeating && this.toggle('repeat', false);
if (this.isShuffling) {
this.shuffle().previous('shuffleIndex', callback);
}
else {
this.previous('currentIndex', callback);
}
return this;
}
// Force toggle: toggle('repeat', true|false);
// Regular toggle toggles the property and the opposite:
// Toggle('repeat'); sets repeat to true and shuffle to false
// Toggle('shuffle'); sets shuffle to true and repeat to false
public toggle(prop: string, forceTrue?: boolean): any {
let propName: string = (prop === 'shuffle') ? 'isShuffling' : (prop === 'repeat') ? 'isRepeating' : '';
let oppositePropName: string = (prop === 'shuffle') ? 'isRepeating' :( prop === 'repeat') ? 'isShuffling' : '';
this[propName] = (forceTrue !== undefined) ? forceTrue : !this[propName];
if (forceTrue === undefined) {
this[oppositePropName] = false;
}
if (propName === 'shuffle') {
this.shuffleIndex = 0;
}
return this;
}
/****************************************************************************************************************************/
/****************************************************************************************************************************
* Flush Everyting
* Reset all attributes to defaults
* Clear everythihg
*/
private flush(reset?: boolean): any {
if (reset) {
this.isReloading = true;
this.clearWaiters();
}
if (this.options.video && this.isUsingHLSPolyfill && this.hls) {
this.destoryHLS();
}
this.bufferProgress = this.lastBufferProgress = this.progress = this.position = this.remainingTime = this.duration = 0;
this.error = this.metadata = this.playState = undefined;
this.currentMediaItem = {};
if (!this.isUsingHLSPolyfill) {
this.setAttribute('src', this.emptyWaveFileBase64); //this.options.html5 ? this.audioElement : this.audioSourceElement
}
this.setPosition(0);
return this;
}
private clearWaiters(): any {
w.clearTimeout(this.errorTimeout);
this.errorTimeout = undefined;
if (this.windows || this.playstation) {
w.clearInterval(this.interval);
this.interval = undefined;
}
else {
this.onlinecbreload = false;
}
return this;
}
/****************************************************************************************************************************/
/****************************************************************************************************************************
* Utility Methods Everyting
*/
private setOptions (options: Object): any {
if (options) {
let opt: string;
for (opt in options) {
this.options[opt] = options[opt];
if (opt === 'volume') {
this.setVolume(options[opt]);
}
}
}
return this;
}
private createMedia (): any {
if (this.options.html5) {
this.audioElement = new Audio();
this.updateDomElement();
}
else {
if (this.options.hls) {
this.hasHlsScript = d.getElementById('gl-hls');
if (!!this.hasHlsScript) {
this.createElement();
} else {
console.log('appending script');
this.script = d.createElement(s);
this.script.src = 'https://anghamiwebcdn.akamaized.net/web/vendor/hls.min.js';
this.script.id = 'gl-hls';
this.script.async = false;
d.head.appendChild(this.script);
this.script.onload = () => {
console.log('script hls.js latest has loaded');
this.createElement();
delete this.script
};
}
delete this.hasHlsScript;
//if (this.device.browser) {
} else {
this.createElement();
}
// }
// else {
// this.createElement();
// }
}
return this;
}
private createNewHlsContext (): void {
this.hls = new w.Hls({
autoStartLoad: true,
startPosition : -1,
capLevelToPlayerSize: true, //false,
debug: false,
defaultAudioCodec: undefined,
initialLiveManifestSize: 1,
maxBufferLength: 30,
maxMaxBufferLength: 600,
maxBufferSize: 60*1000*1000,
maxBufferHole: 0.5,
lowBufferWatchdogPeriod: 0.5,
highBufferWatchdogPeriod: 3,
nudgeOffset: 0.1,
nudgeMaxRetry : 5,
maxFragLookUpTolerance: 0.2,
liveSyncDurationCount: 3,
liveMaxLatencyDurationCount: 10,
enableWorker: true,
enableSoftwareAES: true,
manifestLoadingTimeOut: 15000,
manifestLoadingMaxRetry: 4,
manifestLoadingRetryDelay: 500,
manifestLoadingMaxRetryTimeout : 64000,
startLevel: undefined,
levelLoadingTimeOut: 10000,
levelLoadingMaxRetry: 4,
levelLoadingRetryDelay: 500,
levelLoadingMaxRetryTimeout: 64000,
fragLoadingTimeOut: 20000,
fragLoadingMaxRetry: 6,
fragLoadingRetryDelay: 500,
fragLoadingMaxRetryTimeout: 64000,
startFragPrefetch: false,
appendErrorMaxRetry: 3,
enableWebVTT: true,
enableCEA708Captions: true,
stretchShortVideoTrack: true,//false,
maxAudioFramesDrift : 1,
forceKeyFrameOnDiscontinuity: true,
abrEwmaFastLive: 5.0,
abrEwmaSlowLive: 9.0,
abrEwmaFastVoD: 4.0,
abrEwmaSlowVoD: 15.0,
abrEwmaDefaultEstimate: 500000,
abrBandWidthFactor: 0.95,
abrBandWidthUpFactor: 0.7,
minAutoBitrate: 0
});
// attach hlsjs to videotag
this.hls.attachMedia(this.audioElement);
this.appendHLSEvents();
}
private createElement (): any {
let element: any;
try {
if (this.options.video) {
// Any DOM element where the newely created video element will be injected
let videoElementParent: HTMLElement = d.getElementById(v);
this.audioElement = videoElementParent.appendChild(d.createElement(v));
videoElementParent = null;
//either chrome or fireflox
if (this.device.browser && this.options.hls) { //!element.canPlayType('application/vnd.apple.mpegurl')
this.isUsingHLSPolyfill = true;
// let script: HTMLScriptElement = d.createElement(s);
// script.src = 'https://cdn.jsdelivr.net/npm/hls.js@latest';
// //script.async = true;
// d.head.appendChild(script);
//script.onload = () => {
//script = null;
//};
}
this.updateDomElement();
} else {
this.audioElement = d.body.appendChild(d.createElement(a));
this.updateDomElement();
}
}
catch(e) {}
}
private updateDomElement (): void {
//alert('finally');
this.addEventListeners();
this.setAttribute('preload', 'none');
// this.audioElement.preload = 'none';
if (this.options.video) {
// if (this.device.browser !== 'safari') {
this.audioElement.playsInline = true;
this.audioElement.setAttribute('webkit-playsinline', 'true');
// }
this.audioElement.style.width = '100%';
this.audioElement.style.height = '100%';// '100%';
this.audioElement.style.display = 'block';
this.audioElement.style.position = 'absolute';
this.audioElement.style.top = '0';
this.audioElement.style.left = '0';
this.audioElement.style.right = '0';
this.audioElement.style.bottom = '0';
this.audioElement.style.backgroundColor = 'black';
this.setSrc(this.emptyVideoFileBase64)
.setAttribute('type', this.options.hls ? 'application/x-mpegURL' : 'video/mp4');
//.setAttribute('webkit-playsinline', true)
//.setAttribute('playsinline', true)
//.setAttribute('src', this.currentSrc);
} else {
// console.log('regular init without callback');
//kthis.audioElement.preload = 'none';
this.setSrc(this.emptyWaveFileBase64)
.setAttribute('type', 'audio/mp4; codecs="mp4a.40.2"');
//.setAttribute('src', this.currentSrc);//this.options.html5 ? this.audioElement : this.audioSourceElement
}
if (this.options.controls) {
this.audioElement.controls = true;
}
if (this.options.poster) {
this.setAttribute('poster', this.options.poster);
}
if (typeof this.options.onInit === 'function') {
//this.dispatchEvent('init');
this.options.onInit();
}
}
private addEventListeners (): any {
if (this.audioElement) {
let e: string;
for (e in this.audioElementEvents) {
this.audioElement.addEventListener(e, this.audioElementEvents[e], false);
}
if (this.options.video && this.isUsingHLSPolyfill) {
// create separate error handlers for hlsjs and the video tag
this.hlsjsErrorHandler = this.hlsPolyfillVideoTagErrorHandlerFactory();
this.hlsVideoTagErrorHandler = this.hlsPolyfillVideoTagErrorHandlerFactory();
}
console.log('adding event listeners', this);
}
return this;
}
private isValid(): boolean {
return this.forceDurationChangeForAudioAd ? true : (this.currentSrc.match(this.mimeRegx) !== null);// > 0;
}
private isBufferValid(): boolean {
return (this.bufferProgress > 0) && (this.bufferProgress !== this.lastBufferProgress);
}
private hasBuffer(): boolean {
return !this.playstation && this.bufferProgress !== 100;
}
private setSrc(src: string): any {
try {
this.currentSrc = src;
} catch(e) {}
return this;
}
private setAttribute(type: string, value?: string): any {
if (typeof value !== 'undefined') {
try {
this.audioElement[type] = value;
}
catch(e) {}
}
return this;
}
private removeAttribute(type: string): any {
try {
this.audioElement.removeAttribute(type);
// delete this.audioElement[type];
}
catch(e) {}
return this;
}
/****************************************************************************************************************************/
/***************************************************************************************************************************
* Events Dispatcher
*/
public on(type: string, callback: Function): void {
if (!this.eventMap[type]) {
this.eventMap[type] = [];
}
this.eventMap[type].push(callback);
}
public off(type: string, filler: any): void {
this.eventMap[type] = filler;
}
private dispatchEvent(type: any, args?: Object): any {
let objValue: any = this.eventMap[type];
if (!objValue) {
return this;
}
objValue.forEach(function(item: Function) {
(typeof item === 'function') && item(args);
});
return this;
}
/***************************************************************************************************************************/
/***************************************************************************************************************************
* On Destory
*/
public destroy(): any {
let e: string;
if(this.audioElement) {
for (e in this.audioElementEvents) {
this.audioElement.removeEventListener(e, this.audioElementEvents[e], false);
}
}
for (e in this.eventMap) {
delete this.eventMap[e];
}
if (!this.options.html5 && this.audioElement) {
this.audioElement.parentNode.removeChild(this.audioElement);
}
if (this.options.video && this.isUsingHLSPolyfill) {
this.destoryHLS();
this.options = {};
this.isUsingHLSPolyfill = undefined;
delete this.hlsjsErrorHandler;
delete this.hlsVideoTagErrorHandler;
delete this.hls;
}
delete this.eventMap;
if (this.audioElement) {
delete this.audioElement;
}
this.eventMap = {};
this.audioElement = undefined;
return this;
}
private destoryHLS (): void {
if (this.hls) {
this.hls.destroy();
this.hls.detachMedia();
}
}
/***************************************************************************************************************************/
/****************************************************************************************************************************
* Error Police Management
*/
private hlsPolyfillVideoTagErrorHandlerFactory (): Funct