nsplayer
Version:
NSPlayer, a player which supports quality list of dash and hls
1,788 lines (1,373 loc) • 55.8 kB
JavaScript
'use strict';
var global = typeof self !== 'undefined' ? self : globalThis;
var _defineProperty = require('@babel/runtime/helpers/defineProperty');
var delegates = require('delegates');
var common = require('@newstudios/common');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var _defineProperty__default = /*#__PURE__*/_interopDefaultLegacy(_defineProperty);
var delegates__default = /*#__PURE__*/_interopDefaultLegacy(delegates);
/* eslint-disable no-constant-condition */
const VideoEventNameMap = {
onEncypted: 'encrypted',
onWaitingForKey: 'waitingforkey',
onCanPlay: 'canplay',
onCanPlayThrough: 'canplaythrough',
onCueChange: 'cuechange',
onDurationChange: 'durationchange',
onEmptied: 'emptied',
onEnded: 'ended',
onError: 'error',
onLoad: 'load',
onLoadedData: 'loadeddata',
onLoadedMetaData: 'loadedmetadata',
onLoadStart: 'loadstart',
onPause: 'pause',
onPlay: 'play',
onPlaying: 'playing',
onProgress: 'progress',
onRateChange: 'ratechange',
onSeeked: 'seeked',
onSeeking: 'seeking',
onStalled: 'stalled',
onSuspend: 'suspend',
onTimeUpdate: 'timeupdate',
onVolumeChange: 'volumechange',
onWaiting: 'waiting',
onEnterPictureInPicture: 'enterpictureinpicture',
onLeavePictureInPicture: 'leavepictureinpicture',
onWebkitPlaybackTargetAvailabilityChanged: 'webkitplaybacktargetavailabilitychanged'
};
const VideoEventNameArray = Object.keys(VideoEventNameMap);
const MimeTypeMap = {
m3u8: ['application/x-mpegURL', 'application/vnd.apple.mpegURL'],
mpd: ['application/dash+xml'],
mp4: ['video/mp4'],
m4s: ['video/iso.segment'],
m4a: ['audio/mp4'],
mp3: ['audio/mpeg'],
aac: ['audio/aac'],
ts: ['video/mp2t']
};
[].concat(MimeTypeMap.m3u8).concat(MimeTypeMap.mpd).concat(MimeTypeMap.mp4);
Object.values(MimeTypeMap).reduce((arr, items) => arr.concat(items), []);
function isHls(mimeType) {
return MimeTypeMap.m3u8.indexOf(mimeType) >= 0;
}
function isDash(mimeType) {
return MimeTypeMap.mpd.indexOf(mimeType) >= 0;
}
function isMp4(mimeType) {
return MimeTypeMap.mp4.indexOf(mimeType) >= 0;
}
function getMimeType(src) {
const pureSrc = src.replace(/[?#].*$/, '');
const matched = pureSrc.match(/\.([^./\\]+)$/);
if (matched) {
const extension = matched[1].toLowerCase();
if (extension in MimeTypeMap) {
return MimeTypeMap[extension][0];
}
}
}
function isSafari() {
const chr = !!window.navigator.userAgent.match(/chrome/i);
const sfri = !!window.navigator.userAgent.match(/safari/i);
return !chr && sfri;
}
function isMobile() {
const mobile = !!window.navigator.userAgent.match(/mobile/i);
return mobile;
}
function assert(target, message) {
if (!target) {
throw new Error(message !== null && message !== void 0 ? message : `expect target but get [${target}]`);
}
}
function dup(char, count) {
return [...new Array(count)].reduce(l => l + char, '');
}
function prefix(num, len) {
const t = String(num);
if (t.length < len) {
return `${dup('0', len - t.length)}${t}`;
}
return t;
}
/**
* format the seconds time
* @param timeInSeconds the number of seconds
* @param format h:mm:ss.SSS
*/
function formatTime(timeInSeconds, format) {
const date = new Date(timeInSeconds * 1000);
let time = format;
let changed = false;
const hh = ~~(timeInSeconds / 3600);
{
const ma = time.match(/(h+)/);
if (ma) {
const h = ma[1];
time = time.replace(h, prefix(hh, h.length));
changed = true;
}
}
const mm = changed ? date.getUTCMinutes() : ~~(timeInSeconds / 60);
{
const ma = time.match(/(m+)/);
if (ma) {
const m = ma[1];
time = time.replace(m, prefix(mm, m.length));
changed = true;
}
}
const ss = changed ? date.getUTCSeconds() : ~~timeInSeconds;
{
const ma = time.match(/(s+)/);
if (ma) {
const s = ma[1];
time = time.replace(s, prefix(ss, s.length));
}
}
const SSS = changed ? date.getMilliseconds() : ~~(timeInSeconds * 1000);
{
const ma = time.match(/(S+)/);
if (ma) {
const S = ma[1];
time = time.replace(S, prefix(SSS, S.length));
}
}
return time;
}
/** fix auto play policy, when autoplay policy error happened, just set muted true and replay */
function fixAutoPlayPolicy(video, disposables) {
if (!('_fixedPlay' in video)) {
const play = video.play;
video.play = () => {
return play.call(video).catch(error => {
video.dispatchEvent(new ErrorEvent('error', {
error,
message: error.message
}));
});
};
Object.defineProperty(video, '_fixedPlay', {
value: true,
enumerable: false
});
}
const onAutoPlayError = common.Event.filter(common.Event.once(common.Event.fromDOMEventEmitter(video, 'error')), evt => {
const {
error: err = this.error
} = evt;
return this.autoplay && !this.muted && ((err === null || err === void 0 ? void 0 : err.name) == 'NotAllowedError' || (err === null || err === void 0 ? void 0 : err.name) == 'AbortError' && isSafari());
});
onAutoPlayError(() => {
const err = new window.Event('error', {
cancelable: true
});
const onAutoPlayError = this._onAutoPlayError;
onAutoPlayError.fire(err);
if (!err.defaultPrevented) {
this.muted = true;
this.play();
}
}, null, disposables);
}
/** fix safari pause event twice issue */
function fixPauseEvent(video, disposables) {
const target = this;
target._paused = video.paused;
const setPaused = (paused, evt) => {
if (target._paused !== paused) {
target._paused = paused;
if (paused) {
target._onPause.fire(evt);
} else {
target._onPlay.fire(evt);
}
}
};
const onPlay = common.Event.fromDOMEventEmitter(video, 'play');
onPlay(evt => setPaused(false, evt), null, disposables);
const onPause = common.Event.fromDOMEventEmitter(video, 'pause');
onPause(evt => setPaused(true, evt), null, disposables);
}
class BasePlayer extends common.Disposable {
constructor(...args) {
super(...args);
_defineProperty__default["default"](this, "_video", null);
_defineProperty__default["default"](this, "_disposableVideo", this._register(new common.MutableDisposable()));
_defineProperty__default["default"](this, "_onAutoPlayError", this._register(new common.Emitter()));
_defineProperty__default["default"](this, "onAutoPlayError", common.Event.once(this._onAutoPlayError.event));
_defineProperty__default["default"](this, "_onLoopChange", this._register(new common.Emitter()));
_defineProperty__default["default"](this, "onLoopChange", this._onLoopChange.event);
_defineProperty__default["default"](this, "_paused", false);
_defineProperty__default["default"](this, "_loop", false);
_defineProperty__default["default"](this, "defaultInitialBitrate", 0);
_defineProperty__default["default"](this, "_nextSeek", 0);
}
set loop(loop) {
const video = this.video;
if (video) {
this._loop = video.loop;
}
if (this._loop !== loop) {
this._loop = loop;
if (video) {
video.loop = loop;
}
this._onLoopChange.fire(new window.Event('loopchange'));
}
}
get loop() {
const video = this.video;
if (video) {
this._loop = video.loop;
}
return this._loop;
}
exitFullscreen({
fallback = 'never'
} = {}) {
if (this.supportFullscreen) {
if (this.fullscreen) {
return document.exitFullscreen();
}
} else if (fallback === 'native') {
if (this.nativeFullscreen) {
this.exitNativeFullscreen();
}
}
return Promise.resolve();
}
toggleFullscreen({
fallback = 'never'
} = {}) {
if (this.supportFullscreen) {
if (this.fullscreen) {
this.exitFullscreen().catch(error => {
console.warn(error, 'Video failed to exit fullscreen mode.');
});
} else {
this.requestFullscreen().catch(error => {
console.warn(error, 'Video failed to enter fullscreen mode.');
});
}
} else if (fallback === 'native') {
this.toggleNativeFullscreen();
} else {
console.warn('Fullscreen is not supported');
}
}
exitNativeFullscreen() {
if (this.nativeFullscreen) {
var _this$video;
if ((_this$video = this.video) !== null && _this$video !== void 0 && _this$video.webkitExitFullscreen) {
this.video.webkitExitFullscreen();
}
}
}
requestNativeFullscreen() {
if (!this.nativeFullscreen && this.supportNativeFullscreen) {
var _this$video2;
if ((_this$video2 = this.video) !== null && _this$video2 !== void 0 && _this$video2.webkitEnterFullscreen) {
this.video.webkitEnterFullscreen();
}
}
}
toggleNativeFullscreen() {
if (this.supportNativeFullscreen) {
if (this.nativeFullscreen) {
this.exitNativeFullscreen();
} else {
this.requestNativeFullscreen();
}
} else {
console.warn('Native fullscreen is not supported');
}
}
get supportFullscreen() {
return !!document.fullscreenEnabled;
}
get supportNativeFullscreen() {
var _this$video3;
return !!((_this$video3 = this.video) !== null && _this$video3 !== void 0 && _this$video3.webkitSupportsFullscreen);
}
get nativeFullscreen() {
var _this$video4;
return !!((_this$video4 = this.video) !== null && _this$video4 !== void 0 && _this$video4.webkitDisplayingFullscreen);
}
get pictureInPicture() {
return !!this.video && this.video === document.pictureInPictureElement;
}
requestPictureInPicture() {
const video = this.withVideo();
if (video.requestPictureInPicture) {
return video.requestPictureInPicture();
}
return Promise.reject(new Error('picture in picture not supported'));
}
exitPictureInPicture() {
if (this.pictureInPicture) {
return document.exitPictureInPicture();
}
return Promise.resolve();
}
get supportPictureInPicture() {
return !!document.pictureInPictureEnabled;
}
togglePictureInPicture() {
if (this.supportPictureInPicture) {
if (this.pictureInPicture) {
//关闭
this.exitPictureInPicture().catch(error => {
console.warn(error, 'Video failed to leave Picture-in-Picture mode.');
});
} else {
//开启
this.requestPictureInPicture().catch(error => {
console.warn(error, 'Video failed to enter Picture-in-Picture mode.');
});
}
} else {
console.warn('Picture in picture is not supported');
}
}
get video() {
return this._video;
}
set video(video) {
if (this.video === video) {
return;
}
this._disposableVideo.value = this._registerVideoListeners(video);
}
_registerVideoListeners(video) {
this._video = video;
if (video) {
// sync status with video
this._paused = video.paused; // sync status with base player
video.loop = this._loop;
const player = this;
const disposables = [];
VideoEventNameArray.forEach(key => {
// eliminate the 'on' upon the event type
const type = VideoEventNameMap[key];
if (key === 'onPause' || key === 'onPlay') {
// fix pause and play event in safari
return;
} // every video event should be fired to the player event emitter
const handler = ev => player[`_${key}`].fire(ev);
common.Event.fromDOMEventEmitter(video, type)(handler, this, disposables);
}); // fix auto play rejection issue
fixAutoPlayPolicy.call(this, video, disposables); // fix pause event twice issue in safari
fixPauseEvent.call(this, video, disposables);
return common.combinedDisposable(...disposables);
}
}
/**
* get inner HTMLVideoElement, throw error if video is null
*/
withVideo() {
const video = this.video;
assert(video);
return video;
}
toggle() {
if (this.paused) {
this.play();
} else {
this.pause();
}
}
reset() {
const video = this.video;
if (video && video.hasAttribute('src')) {
video.pause();
video.removeAttribute('src');
video.load();
if (!this._paused) {
// workaround to dispatch a pause event for completing the lifecycle
video.dispatchEvent(new window.Event('pause', {
cancelable: true
}));
}
}
}
get bufferedTime() {
const c = this.currentTime;
if (this.buffered.length === 0) {
return c;
}
let i = 0;
let j = this.buffered.length; // start0
while (i < j) {
const _idx = i + j >> 1;
const start = this.buffered.start(_idx);
if (c > start) {
i = _idx + 1;
} else if (c < start) {
j = _idx;
} else {
return this.buffered.end(i);
}
}
const idx = i - 1;
if (idx >= this.buffered.length || idx < 0) {
return c;
}
const end = this.buffered.end(idx);
return end < c ? c : end;
}
fastSeek(time) {
const video = this.withVideo();
if (!video.seekable) {
return;
}
if (time === video.currentTime) {
return;
}
this._nextSeek = time;
if (!video.seeking) {
video.currentTime = time;
this._nextSeek = video.currentTime;
common.Event.once(this.onSeeked)(() => {
if (this._nextSeek !== video.currentTime) {
this.fastSeek(this._nextSeek);
}
});
}
}
}
delegates__default["default"](BasePlayer.prototype, 'video').access('poster').access('playsInline').getter('videoHeight').getter('videoWidth').method('getVideoPlaybackQuality').access('autoplay').access('buffered').access('controls').access('crossOrigin').getter('currentSrc').access('currentTime').getter('duration').getter('ended').getter('error') // .access('loop')
.access('mediaKeys').access('muted').getter('networkState').getter('paused').access('playbackRate').access('defaultPlaybackRate').getter('played').access('preload').getter('readyState').getter('seekable').getter('seeking').access('volume').method('addTextTrack').method('canPlayType').method('load').method('pause').method('play').method('setMediaKeys');
const _noop = () => undefined;
const _internalEmitter = {
fire: _noop,
event: () => common.Disposable.None,
dispose: _noop
}; // register player listener disposable
const desc = VideoEventNameArray.reduce((desc, key) => {
const emitterKey = `_${key}`;
desc[emitterKey] = {
value: _internalEmitter
};
desc[key] = {
get() {
let emitter = this[emitterKey];
if (emitter === _internalEmitter) {
emitter = this._register(new common.Emitter());
Object.defineProperty(this, emitterKey, {
get: () => emitter
});
}
return emitter.event;
},
enumerable: true
};
return desc;
}, {});
Object.defineProperties(BasePlayer.prototype, desc);
const DefaultSorter = (s1, s2) => {
if (s1.bitrate && s2.bitrate) {
return s1.bitrate - s2.bitrate;
}
if (s1.width && s1.height && s2.width && s2.height) {
return Math.min(s1.width, s1.height) - Math.min(s2.width, s2.height);
}
if (s1.fps && s2.fps) {
return Number(s1.fps) - Number(s2.fps);
}
return 0;
};
function supportMediaSource() {
return typeof window !== undefined && typeof (MediaSource || WebKitMediaSource) === 'function';
}
/**
* 通过可播放资源,以及当前浏览器环境,推算出使用的资源
* @param sources 所有可播放的资源列表
*/
const DefaultSourcePolicy = sources => {
const sourceMap = {
dash: [],
hls: [],
mp4: []
};
for (const source of sources) {
const {
src
} = source;
let {
mime
} = source;
if (!mime) {
source.mime = mime = getMimeType(src);
}
if (mime) {
if (isHls(mime)) {
sourceMap.hls.push(source);
} else if (isDash(mime)) {
sourceMap.dash.push(source);
} else if (isMp4(mime)) {
sourceMap.mp4.push(source);
}
}
}
sourceMap.dash.sort(DefaultSorter);
sourceMap.hls.sort(DefaultSorter);
sourceMap.mp4.sort(DefaultSorter);
/**
* Workaround because android dose not support dash well
*/
if ((isSafari() || isMobile()) && sourceMap.hls.length) {
return sourceMap.hls[0];
}
if (sourceMap.dash.length && supportMediaSource()) {
return sourceMap.dash[0];
}
if (sourceMap.hls.length && supportMediaSource()) {
return sourceMap.hls[0];
}
if (sourceMap.mp4.length) {
return sourceMap.mp4[0];
}
};
const sourceKeys = ['src', 'bitrate', 'fps', 'height', 'width', 'mime'];
function isSourceEqual(s1, s2) {
return sourceKeys.every(k => s1[k] === s2[k]);
}
function areSourcesEqual(s1, s2) {
if (s1.length !== s2.length) {
return false;
}
return s1.every((s, idx) => isSourceEqual(s, s2[idx]));
}
function isLevelMatch(source, target) {
if (source.type && target.type && source.type !== target.type) {
return false;
}
if (Math.min(source.width, source.height) > Math.min(target.width, target.height)) {
return false;
}
return true;
}
/**
* 将 id 转变为 Quality Level
* @param id 指定的 ID,形如 br2000000-1920x1080-video / br1200000-1280x720
*/
function idToQualityLevel(id) {
const result = id.match(/^br(\d+)-(\d+)x(\d+)(?:-(video|audio))?(?:-(.*))?$/);
if (result) {
const level = {
bitrate: parseInt(result[1]),
width: parseInt(result[2]),
height: parseInt(result[3])
};
if (result[4]) {
level.type = result[4];
}
if (result[5]) {
try {
const fps = parseInt(result[5]);
if (fps) {
level.fps = fps;
}
} catch (_e) {//
}
}
return level;
}
}
/**
* 将播放质量级别转为为播放质量 ID
* @param level 播放质量级别
*/
function qualityLevelToId(level) {
let id = `br${~~level.bitrate}-${~~level.width}x${~~level.height}`;
if (level.type) {
id = `${id}-${level.type}`;
}
if (level.fps) {
id = `${id}-${level.fps}`;
}
return id;
}
/** 播放质量 ID 是否为 auto 自动切换 */
function isAutoQuality(id) {
return id === 'auto';
}
/** 将任意 fps 字段转化为整数 fps,如果失败则返回 NaN */
function computeFPS(fps) {
if (fps) {
if (typeof fps === 'number') {
return Math.round(fps);
}
if (fps.indexOf('/') > 0) {
const [a, b] = fps.split('/', 1);
try {
return Math.round(parseFloat(a) / parseFloat(b));
} catch (_e) {// do nothing
}
}
try {
return Math.round(parseFloat(fps));
} catch (_e) {// do nothing
}
}
return NaN;
}
/** 两个播放质量是否同级 */
function isSameLevel(level1, level2) {
if (!level1 || !level2) {
return level2 === level2;
}
return level1.bitrate === level2.bitrate && level1.width === level2.width && level1.height === level2.height && (!level1.type || !level2.type || level1.type === level2.type) && (!level1.fps || !level2.fps || level1.fps === level2.fps);
}
class CorePlayer extends common.Disposable {
/** 实现 video 和播放 src 对应的初始化关系,该 src 通常为一个 mp4 或 m3u8 或 mpd */
constructor(video, source) {
super();
_defineProperty__default["default"](this, "_onPlayListChange", this._register(new common.Emitter()));
_defineProperty__default["default"](this, "onPlayListChange", this._onPlayListChange.event);
_defineProperty__default["default"](this, "_onQualityIdSelect", this._register(new common.Emitter()));
_defineProperty__default["default"](this, "onQualityIdSelect", this._onQualityIdSelect.event);
_defineProperty__default["default"](this, "_onQualityChange", this._register(new common.Emitter()));
_defineProperty__default["default"](this, "onQualityChange", this._onQualityChange.event);
_defineProperty__default["default"](this, "_onQualitySwitching", this._register(new common.Emitter()));
_defineProperty__default["default"](this, "onQualitySwitching", this._onQualitySwitching.event);
_defineProperty__default["default"](this, "_onAutoChange", this._register(new common.Emitter()));
_defineProperty__default["default"](this, "onAutoChange", this._onAutoChange.event);
_defineProperty__default["default"](this, "_onReady", this._register(new common.Emitter()));
_defineProperty__default["default"](this, "onReady", this._onReady.event);
_defineProperty__default["default"](this, "onOncePlayListReady", (listener, thisArgs, disposables) => {
if (this.playList.length) {
listener.call(thisArgs, this.playList);
return common.Disposable.None;
} else {
return common.Event.once(this.onPlayListChange)(list => listener.call(thisArgs, list), null, disposables);
}
});
_defineProperty__default["default"](this, "_playList", []);
_defineProperty__default["default"](this, "_ready", false);
_defineProperty__default["default"](this, "_selectedQualityId", 'auto');
_defineProperty__default["default"](this, "_onPlayListMutable", this._register(new common.MutableDisposable()));
_defineProperty__default["default"](this, "debug", false);
this.video = video;
this.source = source;
this._register(common.disposableTimeout(() => (this.log('onInit'), this.onInit(video, source))));
}
log(...args) {
if (this.debug) console.log(`[${this.name}]`, ...args);
}
get playList() {
return this._playList;
}
/** 更新 PlayList 播放级别组,每当获取到新的 PlayList 时请调用此接口 */
updatePlayList() {
this.log('updatePlayList', 'new play list detected');
this.setPlayList(this.translatePlayList());
this.updateNextQualityLevel();
this.updateAutoQuality();
this.updateQualityLevel();
this.log('updatePlayList', 'current quality:', this.qualityId);
}
get nextQualityLevel() {
return this._nextQualityLevel;
}
/** 更新下一个播放质量级别,每当发生播放质量切换开始时请调用此接口 */
updateNextQualityLevel() {
const ql = this.translateNextQualityLevel();
if (ql) {
this.setNextQualityLevel(ql);
} else {
this._nextQualityLevel = undefined;
}
}
get qualityLevel() {
return this._qualityLevel;
}
/** 更新当前播放质量级别,每当发生播放质量切换结束时请调用此接口 */
updateQualityLevel() {
const ql = this.translateCurrentQuality();
if (ql) {
this.setQualityLevel(ql);
} else {
this._qualityLevel = undefined;
}
}
get autoQuality() {
return !!this._autoQuality;
}
/** 每当切换自动清晰度状态时,请调用此接口 */
updateAutoQuality() {
const support = this.supportAutoQuality;
const auto = support && this.autoQualityEnabled;
if (this._autoQuality !== auto) {
this.log('updateAutoQuality', 'state, new:', auto);
this._autoQuality = auto;
this._onAutoChange.fire(auto); // when enabling auto quality, manually fire quality change to the current
if (auto) {
this.updateNextQualityLevel();
const qualityLevel = this.translateCurrentQuality();
if (qualityLevel) {
this._onQualityChange.fire(qualityLevel);
}
}
}
}
get ready() {
return this._ready;
}
/** 更新当前播放器状态,每当初始化完成后情调用此接口 */
setReady() {
if (this._ready) return;
const selectedId = this.autoQuality ? 'auto' : this.qualityId;
this.log('setReady', 'with selected id:', selectedId);
this._selectedQualityId = selectedId;
this._onQualityIdSelect.fire(selectedId);
this._ready = true;
this._onReady.fire();
this.log('setReady finished');
}
get selectedQualityId() {
return this._selectedQualityId;
}
get qualityId() {
if (this.qualityLevel) {
return qualityLevelToId(this.qualityLevel);
} else {
return 'auto';
}
}
get nextQualityId() {
if (this.nextQualityLevel) {
return qualityLevelToId(this.nextQualityLevel);
} else {
return 'auto';
}
}
/**
* @FIXME should find a id similar to playlist in coreplayer
* @param id any quality id
*/
setQualityById(id) {
const auto = isAutoQuality(id);
if (auto && !this.supportAutoQuality) {
// when set auto in base player, there is no need to set any quality level
return;
}
this.log('setQualityById', 'new:', id);
this.setAutoQualityState(auto);
this.updateAutoQuality();
if (auto) {
// nothing to do
if (this.ready) {
this._selectedQualityId = 'auto';
this._onQualityIdSelect.fire('auto');
}
return;
} // once playlist updated, firstly try to update the next level index.
this._onPlayListMutable.value = this.onOncePlayListReady(levels => {
this.log('onOncePlayListReady', 'target quality', id);
const selectedIndex = this.findLevelIndexById(id);
this.log('find', levels.length, 'levels and select', selectedIndex);
this.setNextLevelIndex(selectedIndex);
this._selectedQualityId = this.levelIndexToQualityId(selectedIndex);
if (this.ready) {
this._onQualityIdSelect.fire(this.levelIndexToQualityId(selectedIndex)); // FIXME opt in?
// this.updateNextQualityLevel()
// this.updateQualityLevel()
}
});
}
levelIndexToQualityId(level) {
if (level < 0) {
return 'auto';
}
if (level < this.levels.length) {
return qualityLevelToId(this.levelToQuality(this.levels[level]));
}
console.warn('level is out of index bound or not ready');
return 'auto';
}
findLevelIndexById(id) {
if (this.playList.length) {
const level = idToQualityLevel(id);
if (level) {
const levels = this.playList;
let index = 0;
for (let i = 0; i < levels.length; i++) {
if (isLevelMatch(levels[i], level)) {
index = i;
} else {
break;
}
}
return index;
}
}
return -1;
}
/** 翻译当前 PlayList */
translatePlayList() {
return this.levels.map(level => this.levelToQuality(level)).sort((l1, l2) => l1.bitrate - l2.bitrate);
}
/** 翻译当前 QualityLevel */
translateCurrentQuality() {
const level = this.currentLevel;
if (level) {
return this.levelToQuality(level);
}
}
/** 翻译下一个 QualityLevel, 当前 autoQuality 打开时返回 undefined */
translateNextQualityLevel() {
const level = this.nextLevel;
if (level && !this.autoQualityEnabled) {
return this.levelToQuality(level);
}
}
setPlayList(playList) {
let changed = false;
if (playList.length !== this._playList.length) {
changed = true;
} else if (playList.some((level, index) => !isSameLevel(this._playList[index], level))) {
changed = true;
} // workaround: blank playlist should be notified
if (changed || playList.length === 0) {
this._playList = playList;
this._onPlayListChange.fire(playList);
}
}
setNextQualityLevel(nextQualityLevel) {
let changed = false;
if (this._nextQualityLevel) {
if (!isSameLevel(nextQualityLevel, this._nextQualityLevel)) {
changed = true;
}
} else {
changed = true;
}
if (changed) {
this._nextQualityLevel = nextQualityLevel;
this.log('setNextQualityLevel', 'onQualitySwitching:', this.nextQualityId);
this._onQualitySwitching.fire(nextQualityLevel);
}
}
setQualityLevel(qualityLevel) {
let changed = false;
if (this._qualityLevel) {
if (!isSameLevel(qualityLevel, this._qualityLevel)) {
changed = true;
}
} else {
changed = true;
}
if (changed) {
this._qualityLevel = qualityLevel;
if (!this.ready && !this.supportAutoQuality) {
// workaround base player selected quality id
this._selectedQualityId = this.qualityId;
}
this.log('setQualityLevel', 'onQualityChange:', this.qualityId);
this._onQualityChange.fire(qualityLevel);
}
}
}
function createCorePlayer(source, video, sources = [], fastSwitch = true, capLevelToPlayerSize = false, callback = id => id) {
if (isHls(source.mime)) {
return Promise.resolve().then(function () { return require('./hlsplayer-9c50da57.js'); }).then(module => module.HlsPlayer).then(HlsPlayer => callback(new HlsPlayer(video, source, fastSwitch, capLevelToPlayerSize)));
} else if (isDash(source.mime)) {
let use_dash_js = false; // shaka-player bundle size is smaller than dash.js
const manuallySetTarget = localStorage && localStorage.getItem('use_dash_js') || null;
if (manuallySetTarget !== null) {
use_dash_js = manuallySetTarget === 'true' || manuallySetTarget === '1';
}
if (use_dash_js) {
return Promise.resolve().then(function () { return require('./dashplayer-d15c72aa.js'); }).then(module => module.DashPlayer).then(DashPlayer => callback(new DashPlayer(video, source, fastSwitch, capLevelToPlayerSize)));
}
return Promise.resolve().then(function () { return require('./shakaplayer-b97c4bca.js'); }).then(module => module.ShakaPlayer).then(ShakaPlayer => callback(new ShakaPlayer(video, source, fastSwitch, capLevelToPlayerSize)));
} else if (isMp4(source.mime)) {
if (sources) {
return Promise.resolve().then(function () { return require('./baseplayer-a8eafe5b.js'); }).then(module => module.BasePlayer).then(BasePlayer => callback(new BasePlayer(video, sources)));
} else {
throw new Error('none video sources');
}
}
throw new Error('unsupported mime type');
}
/* eslint-disable prefer-rest-params */
/* eslint-disable prefer-spread */
const spec = ['fullscreen', 'fullscreenEnabled', 'fullscreenElement', 'fullscreenchange', 'fullscreenerror', 'exitFullscreen', 'requestFullscreen'];
const webkit = ['webkitIsFullScreen', 'webkitFullscreenEnabled', 'webkitFullscreenElement', 'webkitfullscreenchange', 'webkitfullscreenerror', 'webkitExitFullscreen', 'webkitRequestFullscreen'];
const moz = ['mozFullScreen', 'mozFullScreenEnabled', 'mozFullScreenElement', 'mozfullscreenchange', 'mozfullscreenerror', 'mozCancelFullScreen', 'mozRequestFullScreen'];
const ms = ['', 'msFullscreenEnabled', 'msFullscreenElement', 'MSFullscreenChange', 'MSFullscreenError', 'msExitFullscreen', 'msRequestFullscreen'];
function getFullscreenApi() {
const fullscreenEnabled = [spec[1], webkit[1], moz[1], ms[1]].find(prefix => document[prefix]);
return [spec, webkit, moz, ms].find(vendor => {
return vendor.find(prefix => prefix === fullscreenEnabled);
}) || [];
} // Get the vendor fullscreen prefixed api
let fsVendorKeywords = [];
function handleEvent(eventType, event) {
document[spec[0]] = document[fsVendorKeywords[0]] || !!document[fsVendorKeywords[2]] || false;
document[spec[1]] = document[fsVendorKeywords[1]] || false;
document[spec[2]] = document[fsVendorKeywords[2]] || null;
if (eventType !== event.type) {
const evt = new Event(eventType, {
cancelable: false,
bubbles: true
});
event.target.dispatchEvent(evt);
}
}
function setupShim() {
// fullscreen
// Defaults to false for cases like MS where they do not have this
// attribute. Another way to check whether fullscreen is active is to look
// at the fullscreenElement attribute.
document[spec[0]] = document[fsVendorKeywords[0]] || !!document[fsVendorKeywords[2]] || false; // fullscreenEnabled
document[spec[1]] = document[fsVendorKeywords[1]] || false; // fullscreenElement
document[spec[2]] = document[fsVendorKeywords[2]] || null; // onfullscreenchange
document.addEventListener(fsVendorKeywords[3], handleEvent.bind(document, spec[3]), false); // onfullscreenerror
document.addEventListener(fsVendorKeywords[4], handleEvent.bind(document, spec[4]), false); // exitFullscreen
document[spec[5]] = function () {
return document[fsVendorKeywords[5]]();
}; // requestFullscreen
Element.prototype[spec[6]] = function () {
return this[fsVendorKeywords[6]].apply(this, arguments);
};
}
if (typeof document !== 'undefined') {
fsVendorKeywords = getFullscreenApi(); // Don't polyfill if it already exist
const hasFullscreenEnabledProp = (spec[1] in document);
hasFullscreenEnabledProp || setupShim();
}
function isWebKit() {
// has not getVideoPlaybackQuality, but has webkit prefix method
return 'webkitDroppedFrameCount' in HTMLVideoElement.prototype;
}
function getVideoPlaybackQuality() {
if (isWebKit()) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const webKitVideo = this;
return {
droppedVideoFrames: webKitVideo.webkitDroppedFrameCount,
totalVideoFrames: webKitVideo.webkitDecodedFrameCount,
// Not provided by this polyfill:
corruptedVideoFrames: webKitVideo.corruptedVideoFrames || 0,
creationTime: webKitVideo.creationTime || NaN,
totalFrameDelay: webKitVideo.totalFrameDelay || 0 // Moz extension
};
} else {
return {
droppedVideoFrames: 0,
totalVideoFrames: 0,
corruptedVideoFrames: 0,
creationTime: NaN,
totalFrameDelay: 0 // Moz extension
};
}
}
if (typeof window !== 'undefined') {
if (!HTMLVideoElement.prototype.getVideoPlaybackQuality) {
HTMLVideoElement.prototype.getVideoPlaybackQuality = getVideoPlaybackQuality;
}
}
/**
* NSPlayer
*/
class NSPlayer extends BasePlayer {
get viewport() {
const el = this._el;
const video = this.video;
if (el && video) {
const {
offsetWidth,
offsetHeight
} = el;
const {
videoWidth,
videoHeight
} = video;
if (offsetWidth && offsetHeight && videoWidth && videoHeight) {
if (offsetWidth * videoHeight > offsetHeight * videoWidth) {
return {
width: ~~(offsetHeight * videoWidth / videoHeight),
height: offsetHeight
};
} else {
return {
width: offsetWidth,
height: ~~(offsetWidth * videoHeight / videoWidth)
};
}
}
}
return {
width: 0,
height: 0
};
}
get bandwidthEstimate() {
if (this.corePlayer) {
return this.corePlayer.bandwidthEstimate;
}
return NaN;
}
get currentQualityLevel() {
return idToQualityLevel(this.currentQualityId);
}
constructor(opt = {}) {
super();
_defineProperty__default["default"](this, "_el", null);
_defineProperty__default["default"](this, "_originalContainer", null);
_defineProperty__default["default"](this, "_originalBodyOverflow", '');
_defineProperty__default["default"](this, "_disposableParentElement", new common.MutableDisposable());
_defineProperty__default["default"](this, "_delayQualitySwitchRequest", new common.MutableDisposable());
_defineProperty__default["default"](this, "_delayContainerTimer", new common.MutableDisposable());
_defineProperty__default["default"](this, "_corePlayerRef", new common.MutableDisposable());
_defineProperty__default["default"](this, "_sources", []);
_defineProperty__default["default"](this, "_requestedQualityId", 'auto');
_defineProperty__default["default"](this, "_capLevelToPlayerSize", false);
_defineProperty__default["default"](this, "_sourcePolicy", DefaultSourcePolicy);
_defineProperty__default["default"](this, "_abrFastSwitch", true);
_defineProperty__default["default"](this, "_corePlayerCreateCounter", 0);
_defineProperty__default["default"](this, "_reset_call", false);
_defineProperty__default["default"](this, "_CMD_REQUEST_QUALITY", 0);
_defineProperty__default["default"](this, "_onFullscreenChange", this._register(new common.Emitter()));
_defineProperty__default["default"](this, "onFullscreenChange", this._onFullscreenChange.event);
_defineProperty__default["default"](this, "_onFullscreenError", this._register(new common.Emitter()));
_defineProperty__default["default"](this, "onFullscreenError", this._onFullscreenError.event);
_defineProperty__default["default"](this, "_onWindowFullscreenChange", this._register(new common.Emitter()));
_defineProperty__default["default"](this, "onWindowFullscreenChange", this._onWindowFullscreenChange.event);
_defineProperty__default["default"](this, "_onVideoAttach", this._register(new common.Emitter()));
_defineProperty__default["default"](this, "onVideoAttach", this._onVideoAttach.event);
_defineProperty__default["default"](this, "_onVideoDetach", this._register(new common.Emitter()));
_defineProperty__default["default"](this, "onVideoDetach", this._onVideoDetach.event);
_defineProperty__default["default"](this, "_onQualityChange", this._register(new common.Relay()));
_defineProperty__default["default"](this, "onQualityChange", this._onQualityChange.event);
_defineProperty__default["default"](this, "_onPlayListChange", this._register(new common.Relay()));
_defineProperty__default["default"](this, "onPlayListChange", this._onPlayListChange.event);
_defineProperty__default["default"](this, "_onAutoChange", this._register(new common.Relay()));
_defineProperty__default["default"](this, "onAutoChange", this._onAutoChange.event);
_defineProperty__default["default"](this, "_onQualityRequest", this._register(new common.Emitter()));
_defineProperty__default["default"](this, "onQualityRequest", this._onQualityRequest.event);
_defineProperty__default["default"](this, "_onQualityWillChange", this._register(new common.Emitter()));
_defineProperty__default["default"](this, "onQualityWillChange", this._onQualityWillChange.event);
_defineProperty__default["default"](this, "_onQualitySelect", this._register(new common.Relay()));
_defineProperty__default["default"](this, "onQualitySelect", this._onQualitySelect.event);
_defineProperty__default["default"](this, "_onLoad", this._register(new common.Emitter()));
_defineProperty__default["default"](this, "onLoad", this._onLoad.event);
_defineProperty__default["default"](this, "_onReset", this._register(new common.Emitter()));
_defineProperty__default["default"](this, "onReset", this._onReset.event);
_defineProperty__default["default"](this, "_emitterErrorMutable", this._register(new common.MutableDisposable()));
_defineProperty__default["default"](this, "_onEscKeyDownMutable", this._register(new common.MutableDisposable()));
_defineProperty__default["default"](this, "_onSwitchCallMutalbe", this._register(new common.MutableDisposable()));
_defineProperty__default["default"](this, "_onQualitySwitchStart", this._register(new common.PauseableEmitter({
merge: levels => levels[levels.length - 1]
})));
_defineProperty__default["default"](this, "onQualitySwitchStart", common.Event.filter(this._onQualitySwitchStart.event, qualityLevel => {
const currentQualityLevel = idToQualityLevel(this.currentQualityId);
if (isSameLevel(qualityLevel, currentQualityLevel)) {
this._onSwitchCallMutalbe.value = common.disposableTimeout(() => this._onQualitySwitchEnd.fire(qualityLevel));
return false;
}
return true;
}));
_defineProperty__default["default"](this, "_onQualitySwitchEnd", this._register(new common.PauseableEmitter({
merge: levels => levels[levels.length - 1]
})));
_defineProperty__default["default"](this, "onQualitySwitchEnd", common.Event.filter(this._onQualitySwitchEnd.event, qualityLevel => {
const selectedQualityLevel = idToQualityLevel(this.selectedQualityId);
return isSameLevel(qualityLevel, selectedQualityLevel);
}));
this.opt = opt;
this._register(this._disposableParentElement);
this._register(this._delayQualitySwitchRequest);
this._register(this._corePlayerRef);
this._register(this._delayContainerTimer);
this._register(common.toDisposable(() => this._corePlayerCreateCounter++));
this.onPause(this._onQualitySwitchStart.pause, this._onQualitySwitchStart);
this.onPlay(this._onQualitySwitchStart.resume, this._onQualitySwitchStart);
this.onPause(this._onQualitySwitchEnd.pause, this._onQualitySwitchEnd);
this.onPlay(this._onQualitySwitchEnd.resume, this._onQualitySwitchEnd);
this.onQualitySwitchEnd(() => this._delayQualitySwitchRequest.value = undefined);
this.onWindowFullscreenChange(e => {
if (e.detail) {
const onEscKeydown = common.Event.filter(common.Event.fromDOMEventEmitter(window, 'keydown'), evt => evt.code === 'Escape');
this._onEscKeyDownMutable.value = onEscKeydown(this.exitWindowFullscreen, this);
} else {
this._onEscKeyDownMutable.value = undefined;
}
});
this._onQualitySwitchStart.pause();
this._onQualitySwitchEnd.pause();
if (typeof document !== 'undefined') {
this.video = this.initHTMLVideoElement();
if (opt.el) {
this.container = opt.el;
} else if (opt.selector) {
this.container = document.querySelector(opt.selector);
}
if (opt.autoplay) {
this.autoplay = opt.autoplay;
}
if (opt.preload) {
this.preload = opt.preload;
}
if (opt.loop) {
this.loop = opt.loop;
}
if (opt.muted) {
this.muted = opt.muted;
}
if (opt.volume) {
this.volume = opt.volume;
}
if (opt.controls) {
this.controls = opt.controls;
}
if (opt.abrFastSwitch === false) {
this._abrFastSwitch = false;
}
if (opt.capLevelToPlayerSize === true) {
this._capLevelToPlayerSize = true;
}
if (opt.playbackRate) {
this.video.defaultPlaybackRate = opt.playbackRate;
}
if (opt.initialBitrate) {
this.defaultInitialBitrate = opt.initialBitrate;
}
if (opt.source) {
this.setSource(opt.source);
}
}
}
/** 根据当前的 source 形态获取底层的 CorePlayer,可能为 undefined */
get corePlayer() {
return this._corePlayerRef.value;
}
get currentPlayerName() {
var _this$corePlayer;
return (_this$corePlayer = this.corePlayer) === null || _this$corePlayer === void 0 ? void 0 : _this$corePlayer.name;
}
get fullscreen() {
if (this._el) {
return this._el === document.fullscreenElement;
}
return false;
}
requestFullscreen(options) {
if (this.supportFullscreen) {
if (this.fullscreen) {
return Promise.resolve();
}
if (this._el) {
return Promise.resolve(this._el.requestFullscreen(options));
}
const error = new Error('container not initialized');
const promise = Promise.reject(error);
promise.catch(() => {
const evt = new window.Event('fullscreenerror');
Object.defineProperty(evt, 'error', {
value: error
});
this._onFullscreenError.fire(evt);
});
return promise;
} else if ((options === null || options === void 0 ? void 0 : options.fallback) === 'native') {
if (!this.nativeFullscreen) {
this.requestNativeFullscreen();
}
}
return Promise.resolve();
}
get windowFullscreen() {
const container = document.querySelector('.xpcplayer-window-fullscreen');
if (container && container === this.container) {
const style = getComputedStyle(container);
return style.display !== 'none' && style.visibility !== 'hidden';
}
return false;
}
requestWindowFullscreen() {
if (this.windowFullscreen) {
return;
}
if (this.fullscreen) {
this.exitFullscreen();
}
let container = document.querySelector('.xpcplayer-window-fullscreen');
if (!container) {
container = document.createElement('div');
container.style.left = '0';
container.style.top = '0';
container.style.right = '0';
container.style.bottom = '0';
container.style.zIndex = '99999999';
document.body.appendChild(container);
}
container.style.visibility = 'visible';
container.style.position = 'fixed';
container.classList.add('xpcplayer-window-fullscreen');
this._originalContainer = this.container;
this.container = container;
this._originalBodyOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
const event = new CustomEvent('windowfullscreenchange', {
detail: true
});
this._onWindowFullscreenChange.fire(event);
}
toggleWindowFullscreen() {
if (this.windowFullscreen) {
this.exitWindowFullscreen();
} else {
this.requestWindowFullscreen();
}
}
exitWindowFullscreen() {
if (!this.windowFullscreen) {
return;
}
if (this.fullscreen) {
this.exitFullscreen();
}
const container = this.container;
if (container) {
container.style.visibility = 'hidden';
this.container = this._originalContainer;
this._originalContainer = null;
if (this._originalBodyOverflow) {
document.body.style.overflow = this._originalBodyOverflow;
} else {
document.body.style.removeProperty('overflow');
}
this._originalBodyOverflow = '';
const event = new CustomEvent('windowfullscreenchange', {
detail: false
});
this._onWindowFullscreenChange.fire(event);
}
}
initHTMLVideoElement() {
const video = document.createElement('video');
video.controls = false;
return video;
}
set container(el) {
if (this._el === el) {
return;
}
this._el = el;
this._delayContainerTimer.value = undefined;
this._delayContainerTimer.value = common.disposableTimeout(() => {
this._disposableParentElement.value = undefined;
this._disposableParentElement.value = this._registerContainerListeners(el);
});
}
get container() {
return this._el;
}
set sourcePolicy(sourcePolicy) {
if (this._sourcePolicy !== sourcePolicy) {
this._sourcePolicy = sourcePolicy;
this.setSource(this._sources);
}
}
get sourcePolicy() {
return this._sourcePolicy;
}
/** when attaching the video, call super.doAttach for just append the video to the child */
doAttach(video) {
const el = this._el;
if (el) {
el.innerHTML = '';
el.appendChild(video);
}
}
/** when detaching the video from container */
doDetach(video) {
// do nothing
video.remove();
}
_registerContainerListeners(el) {
const video = this.withVideo();
if (el) {
const fullscreenChangeHandler = e => this._onFullscreenChange.fire(e);
const fullscreenErrorHandler = e => this._onFullscreenError.fire(e);
const detachVideoHandler = () => this._onVideoDetach.fire(video);
const onFullscreenChange = common.Event.fromDOMEventEmitter(el, 'fullscreenchange');
const onFullscreenError = common.Event.fromDOMEventEmitter(el, 'fullscreenerror');
const disposables = [];
onFullscreenChange(fullscreenChangeHandler, null, disposables);
onFullscreenError(fullscreenErrorHandler, null, disposables);
if (!this.supportFullscreen) {
const onNativeFullscreenChange = common.Event.fromDOMEventEmitter(video, ['webkitbeginfullscreen', 'webkitendfullscreen']);
onNativeFullscreenChange(fullscreenChangeHandler, null, disposables);
}
this.doAttach(video);
this._onVideoAttach.fire(video);
return common.toDisposable(() => {
this.doDetach(video);
detachVideoHandler();
common.dispose(disposables);
});
}
}
/** update the quality to requested, reenter-safe */
_updateQuality() {
const id = this._requestedQualityId;
const corePlayer = this.corePlayer;
if (corePlayer) {
const disposableStore = new common.DisposableStore();
this._delayQualitySwitchRequest.value = disposableStore;
if (!isAutoQuality(id)) {
corePlayer.onQualitySwitching(this._onQualitySwitchStart.fire, this._onQualitySwitchStart, disposableStore);
}
corePlayer.onQualityChange(this._onQualitySwitchEnd.fire, this._onQualitySwitchEnd, disposableStore);
corePlayer.setQualityById(id);
}
} // /**
// * 当请求选择 quality 时触发
// * @param id quality id 字符串
// */
// protected onSelectQualityIndex(corePlayer: ICorePlayer, id: string): number {
// return corePlayer.playList.findIndex(level => qualityLevelToId(level) === id)
// }
get src() {
var _this$video;
return ((_this$video = this.video) === null || _this$video === void 0 ? void 0 : _this$video.src) || '';
}
get srcObject() {
var _this$video2;
return ((_this$video2 = this.video) === null || _this$video2 === void 0 ? void 0 : _this$video2.srcObject) || null;
}
get currentQualityId() {
var _this$corePlayer2;
return ((_this$corePlayer2 = this.corePlayer) === null || _this$corePlayer2 === void 0 ? void 0 : _this$corePlayer2.qualityId) || 'auto';
}
get currentPlayList() {
var _this$corePlayer3;
return ((_this$corePlayer3 = this.corePlayer) === null || _this$corePlayer3 === void 0 ? void 0 : _this$corePlayer3.playList) || [];
}
get requestedQualityId() {
return this._requestedQualityId;
}
get selectedQualityId() {
var _this$corePlayer$sele, _this$corePlayer4;
return (_this$corePlayer$sele = (_this$corePlayer4 = this.corePlayer) === null || _this$corePlayer4 === void 0 ? void 0 : _this$corePlayer4.selectedQualityId) !== null && _this$corePlayer$sele !== void 0 ? _this$corePlayer$sele : 'auto';
}
get autoQuality() {
var _this$corePlayer5;
const autoQuality = (_this$corePlayer5 = this.corePlayer) === null || _this$corePlayer5 === void 0 ? void 0 : _this$corePlayer5.autoQuality;
if (typeof autoQuality === 'boolean') {
return autoQuality;
}
return this.selectedQualityId === 'auto';
}
get supportAutoQuality() {
var _this$corePlayer6;
return ((_this$corePlayer6 = this.corePlayer) === null || _this$corePlayer6 === void 0 ? void 0 : _this$corePlayer6.supportAutoQuality) || false;
}