UNPKG

nsplayer

Version:

NSPlayer, a player which supports quality list of dash and hls

1,788 lines (1,373 loc) 55.8 kB
'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; }