cloudinary-video-player
Version:
Cloudinary Video Player
397 lines (290 loc) • 9.2 kB
JavaScript
import VideoSource from './models/video-source';
import { isInteger } from 'utils/type-inference';
const DEFAULT_AUTO_ADVANCE = 0;
const DEFAULT_PRESENT_UPCOMING = 10;
const UPCOMING_VIDEO_TRANSITION = 1;
class Playlist {
constructor(context, sources = [], { repeat = false, autoAdvance = false, presentUpcoming = false } = {}) {
const _context = context;
let _sources = [];
let _currentIndex = null;
let _autoAdvance = null;
let _presentUpcoming = null;
let _defaultRecResolverCache = {};
let _recommendationsHandler = null;
this.enqueue = (source, options = {}) => {
const src = source instanceof VideoSource ? source
: buildSource(source, options);
_sources.push(src);
return src;
};
this.currentIndex = (index) => {
if (index === undefined) {
return _currentIndex;
}
if (index >= this.length() || index < 0) {
throw new Error('Invalid playlist index.');
}
_currentIndex = index;
const current = this.currentSource();
const itemBuilder = recommendationItemBuilder(current);
if (!current.recommendations()) {
current.recommendations(defaultRecommendationsResolver(current));
}
_context.source(current, { recommendationOptions: { disableAutoShow: true, itemBuilder } });
const eventData = { playlist: this, current, next: this.next() };
this.player().trigger('playlistitemchanged', eventData);
refreshRecommendations();
return current;
};
this.presentUpcoming = (delay) => {
_presentUpcoming = _presentUpcoming || {};
if (delay === undefined) {
return _presentUpcoming.delay;
}
if (delay === true) {
delay = DEFAULT_PRESENT_UPCOMING;
} else if (delay === false) {
delay = false;
} else if (!isInteger(delay) || delay < 0) {
throw new Error('presentUpcoming \'delay\' must be either a boolean or a positive integer.');
}
_presentUpcoming.delay = delay;
setupPresentUpcoming();
return _presentUpcoming.delay;
};
this.autoAdvance = (delay) => {
_autoAdvance = _autoAdvance || {};
if (delay === undefined) {
return _autoAdvance.delay;
}
if (delay === true) {
delay = DEFAULT_AUTO_ADVANCE;
} else if (delay === false) {
delay = false;
} else if (!isInteger(delay) || delay < 0) {
throw new Error('Auto advance \'delay\' must be either a boolean or a positive integer.');
}
_autoAdvance.delay = delay;
setupAutoAdvance();
return _autoAdvance.delay;
};
this.list = () => _sources;
this.player = () => _context.player;
this.dispose = () => {
resetAutoAdvance();
resetPresentUpcoming();
resetRecommendations();
};
this.resetState = () => {
this.repeat(repeat);
this.autoAdvance(autoAdvance);
this.presentUpcoming(presentUpcoming);
};
const setupAutoAdvance = () => {
resetAutoAdvance();
const delay = _autoAdvance.delay;
if (delay === false) {
return;
}
const trigger = () => {
if (this.player().ended()) {
_autoAdvance.timeout = setTimeout(() => {
this.playNext();
}, delay * 1000);
}
};
_autoAdvance = { delay, trigger };
_context.on('ended', _autoAdvance.trigger);
};
const resetAutoAdvance = () => {
if (!_autoAdvance) {
_autoAdvance = {};
}
if (_autoAdvance.timeout) {
clearTimeout(_autoAdvance.timeout);
}
if (_autoAdvance.trigger) {
_context.off('ended', _autoAdvance.trigger);
}
_autoAdvance.timeout = null;
_autoAdvance.trigger = null;
};
const setupPresentUpcoming = () => {
resetPresentUpcoming();
const delay = _presentUpcoming.delay;
if (delay === false) {
return;
}
_presentUpcoming.trigger = () => {
const currentTime = this.player().currentTime();
const duration = this.player().duration();
const remainingTime = duration - currentTime;
if (remainingTime < UPCOMING_VIDEO_TRANSITION + 0.5) {
if (_presentUpcoming.showTriggered) {
this.player().trigger('upcomingvideohide');
_presentUpcoming.showTriggered = false;
}
} else if (remainingTime <= _presentUpcoming.delay && !_presentUpcoming.showTriggered && !this.player().loop()) {
this.player().trigger('upcomingvideoshow');
_presentUpcoming.showTriggered = true;
} else if (_presentUpcoming.showTriggered && (remainingTime > _presentUpcoming.delay || this.player().loop())) {
this.player().trigger('upcomingvideohide');
_presentUpcoming.showTriggered = false;
}
};
_context.on('timeupdate', _presentUpcoming.trigger);
};
const resetPresentUpcoming = () => {
this.player().trigger('upcomingvideohide');
if (!_presentUpcoming) {
_presentUpcoming = {};
}
if (_presentUpcoming.trigger) {
_context.off('timeupdate', _presentUpcoming.trigger);
}
_presentUpcoming.trigger = null;
_presentUpcoming.showTriggered = false;
};
const resetRecommendations = () => {
if (_recommendationsHandler) {
_context.off('ended', _recommendationsHandler);
}
};
const refreshRecommendations = () => {
resetRecommendations();
_recommendationsHandler = () => {
if (this.autoAdvance() === false && _context.autoShowRecommendations()) {
this.player().trigger('recommendationsshow');
}
};
_context.on('ended', _recommendationsHandler);
};
const recommendationItemBuilder = (source) => {
const defaultResolver = _defaultRecResolverCache[source.objectId];
if (source.recommendations() && (!defaultResolver || source.recommendations() !== defaultResolver)) {
return;
}
return (source) => ({ source, action: () => this.playItem(source) });
};
const defaultRecommendationsResolver = (source) => {
const defaultResolver = _defaultRecResolverCache[source.objectId];
if (defaultResolver) {
return defaultResolver;
}
_defaultRecResolverCache[source.objectId] = () => {
let index = this.list().indexOf(source);
const items = [];
const numOfItems = Math.min(4, this.length() - 1);
while (items.length < numOfItems) {
index = this.nextIndex(index);
if (index === -1) {
break;
}
const source = this.list()[index];
items.push(source);
}
return items;
};
return _defaultRecResolverCache[source.objectId];
};
const buildSource = (source, options = {}) => _context.buildSource(source, options);
sources.forEach((source) => this.enqueue(source));
this.resetState();
}
playItem(item) {
let index = this.list().indexOf(item);
if (index === -1) {
throw new Error('Invalid playlist item.');
}
this.playAtIndex(index);
}
playAtIndex(index) {
this.currentIndex(index);
this.player().play();
return this.currentSource();
}
currentSource() {
return this.list()[this.currentIndex()];
}
removeAt(index) {
if (index >= this.length() || index < 0) {
throw new Error('Invalid playlist index.');
}
this._sources.splice(index, 1);
return this;
}
repeat(repeat) {
if (repeat === undefined) {
return this._repeat;
}
this._repeat = !!repeat;
return this._repeat;
}
first() {
return this.list()[0];
}
last() {
return this.list()[this.length() - 1];
}
next() {
let nextIndex = this.nextIndex();
if (nextIndex === -1) {
return null;
}
return this.list()[nextIndex];
}
nextIndex(index) {
index = index !== undefined ? index : this.currentIndex();
if (index >= this.length() || index < 0) {
throw new Error('Invalid playlist index.');
}
let isLast = index === this.length() - 1;
let nextIndex = index + 1;
if (isLast) {
if (this.repeat()) {
nextIndex = 0;
} else {
return -1;
}
}
return nextIndex;
}
previousIndex() {
if (this.isFirst()) {
return -1;
}
return this.currentIndex() - 1;
}
playFirst() {
return this.playAtIndex(0);
}
playLast() {
const lastIndex = this.list().length - 1;
return this.playAtIndex(lastIndex);
}
isLast() {
return this.currentIndex() >= this.length() - 1;
}
isFirst() {
return this.currentIndex() === 0;
}
length() {
return this.list().length;
}
playNext() {
let nextIndex = this.nextIndex();
if (nextIndex === -1) {
return null;
}
return this.playAtIndex(nextIndex);
}
playPrevious() {
let previousIndex = this.previousIndex();
if (previousIndex === -1) {
return null;
}
return this.playAtIndex(previousIndex);
}
}
export default Playlist;