You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
13125 lines
461 KiB
13125 lines
461 KiB
(function (global, factory) {
|
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
|
typeof define === 'function' && define.amd ? define(factory) :
|
|
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.jessibuca = factory());
|
|
})(this, (function () { 'use strict';
|
|
|
|
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
|
|
|
function unwrapExports (x) {
|
|
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
}
|
|
|
|
function createCommonjsModule(fn, module) {
|
|
return module = { exports: {} }, fn(module, module.exports), module.exports;
|
|
}
|
|
|
|
var defineProperty = createCommonjsModule(function (module) {
|
|
function _defineProperty(obj, key, value) {
|
|
if (key in obj) {
|
|
Object.defineProperty(obj, key, {
|
|
value: value,
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true
|
|
});
|
|
} else {
|
|
obj[key] = value;
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
|
|
module.exports = _defineProperty, module.exports.__esModule = true, module.exports["default"] = module.exports;
|
|
});
|
|
|
|
var _defineProperty = unwrapExports(defineProperty);
|
|
|
|
// 播放协议
|
|
const PLAYER_PLAY_PROTOCOL = {
|
|
websocket: 0,
|
|
fetch: 1,
|
|
webrtc: 2
|
|
};
|
|
const DEMUX_TYPE = {
|
|
flv: 'flv',
|
|
m7s: 'm7s'
|
|
};
|
|
const FILE_SUFFIX = {
|
|
mp4: 'mp4',
|
|
webm: 'webm'
|
|
};
|
|
|
|
const DEFAULT_PLAYER_OPTIONS = {
|
|
videoBuffer: 1000,
|
|
//1000ms 1 second
|
|
videoBufferDelay: 1000,
|
|
// 1000ms
|
|
isResize: true,
|
|
isFullResize: false,
|
|
//
|
|
isFlv: false,
|
|
debug: false,
|
|
hotKey: false,
|
|
// 快捷键
|
|
loadingTimeout: 10,
|
|
// loading timeout
|
|
heartTimeout: 5,
|
|
// heart timeout
|
|
timeout: 10,
|
|
// second
|
|
loadingTimeoutReplay: true,
|
|
// loading timeout replay. default is true
|
|
heartTimeoutReplay: true,
|
|
// heart timeout replay.
|
|
loadingTimeoutReplayTimes: 3,
|
|
// loading timeout replay fail times
|
|
heartTimeoutReplayTimes: 3,
|
|
// heart timeout replay fail times
|
|
supportDblclickFullscreen: false,
|
|
// support double click toggle fullscreen
|
|
showBandwidth: false,
|
|
// show band width
|
|
keepScreenOn: false,
|
|
//
|
|
isNotMute: false,
|
|
//
|
|
hasAudio: true,
|
|
// has audio
|
|
hasVideo: true,
|
|
// has video
|
|
operateBtns: {
|
|
fullscreen: false,
|
|
screenshot: false,
|
|
play: false,
|
|
audio: false,
|
|
record: false
|
|
},
|
|
controlAutoHide: false,
|
|
// control auto hide
|
|
hasControl: false,
|
|
loadingText: '',
|
|
// loading Text
|
|
background: '',
|
|
decoder: '/assets/js/jessibuca/decoder.js',
|
|
url: '',
|
|
// play url
|
|
rotate: 0,
|
|
//
|
|
// text: '',
|
|
forceNoOffscreen: true,
|
|
// 默认是不采用
|
|
hiddenAutoPause: false,
|
|
//
|
|
protocol: PLAYER_PLAY_PROTOCOL.fetch,
|
|
demuxType: DEMUX_TYPE.flv,
|
|
// demux type
|
|
useWCS: false,
|
|
//
|
|
wcsUseVideoRender: true,
|
|
// 默认设置为true
|
|
useMSE: false,
|
|
//
|
|
useOffscreen: false,
|
|
//
|
|
autoWasm: true,
|
|
// 自动降级到 wasm 模式
|
|
wasmDecodeErrorReplay: true,
|
|
// 解码失败重新播放。
|
|
openWebglAlignment: false,
|
|
// https://github.com/langhuihui/jessibuca/issues/152
|
|
wasmDecodeAudioSyncVideo: false,
|
|
// wasm 解码之后音视频同步
|
|
recordType: FILE_SUFFIX.webm,
|
|
useWebFullScreen: false // use web full screen
|
|
|
|
};
|
|
const WORKER_CMD_TYPE = {
|
|
init: 'init',
|
|
initVideo: 'initVideo',
|
|
render: 'render',
|
|
playAudio: 'playAudio',
|
|
initAudio: 'initAudio',
|
|
kBps: 'kBps',
|
|
decode: 'decode',
|
|
audioCode: 'audioCode',
|
|
videoCode: 'videoCode',
|
|
wasmError: 'wasmError'
|
|
};
|
|
const WASM_ERROR = {
|
|
invalidNalUnitSize: 'Invalid NAL unit size' // errorSplittingTheInputIntoNALUnits: 'Error splitting the input into NAL units'
|
|
|
|
};
|
|
const MEDIA_TYPE = {
|
|
audio: 1,
|
|
video: 2
|
|
};
|
|
const FLV_MEDIA_TYPE = {
|
|
audio: 8,
|
|
video: 9
|
|
};
|
|
const WORKER_SEND_TYPE = {
|
|
init: 'init',
|
|
decode: 'decode',
|
|
audioDecode: 'audioDecode',
|
|
videoDecode: 'videoDecode',
|
|
close: 'close',
|
|
updateConfig: 'updateConfig'
|
|
}; //
|
|
|
|
const EVENTS = {
|
|
fullscreen: 'fullscreen$2',
|
|
webFullscreen: 'webFullscreen',
|
|
decoderWorkerInit: 'decoderWorkerInit',
|
|
play: 'play',
|
|
playing: 'playing',
|
|
pause: 'pause',
|
|
mute: 'mute',
|
|
load: 'load',
|
|
loading: 'loading',
|
|
videoInfo: 'videoInfo',
|
|
timeUpdate: 'timeUpdate',
|
|
audioInfo: "audioInfo",
|
|
log: 'log',
|
|
error: "error",
|
|
kBps: 'kBps',
|
|
timeout: 'timeout',
|
|
delayTimeout: 'delayTimeout',
|
|
loadingTimeout: 'loadingTimeout',
|
|
stats: 'stats',
|
|
performance: "performance",
|
|
record: 'record',
|
|
recording: 'recording',
|
|
recordingTimestamp: 'recordingTimestamp',
|
|
recordStart: 'recordStart',
|
|
recordEnd: 'recordEnd',
|
|
recordCreateError: 'recordCreateError',
|
|
buffer: 'buffer',
|
|
videoFrame: 'videoFrame',
|
|
start: 'start',
|
|
metadata: 'metadata',
|
|
resize: 'resize',
|
|
streamEnd: 'streamEnd',
|
|
streamSuccess: 'streamSuccess',
|
|
streamMessage: 'streamMessage',
|
|
streamError: 'streamError',
|
|
volumechange: 'volumechange',
|
|
destroy: 'destroy',
|
|
mseSourceOpen: 'mseSourceOpen',
|
|
mseSourceClose: 'mseSourceClose',
|
|
mseSourceBufferError: 'mseSourceBufferError',
|
|
mseSourceBufferBusy: 'mseSourceBufferBusy',
|
|
mseSourceBufferFull: 'mseSourceBufferFull',
|
|
videoWaiting: 'videoWaiting',
|
|
videoTimeUpdate: 'videoTimeUpdate',
|
|
videoSyncAudio: 'videoSyncAudio',
|
|
playToRenderTimes: 'playToRenderTimes'
|
|
};
|
|
const JESSIBUCA_EVENTS = {
|
|
load: EVENTS.load,
|
|
timeUpdate: EVENTS.timeUpdate,
|
|
videoInfo: EVENTS.videoInfo,
|
|
audioInfo: EVENTS.audioInfo,
|
|
error: EVENTS.error,
|
|
kBps: EVENTS.kBps,
|
|
log: EVENTS.log,
|
|
start: EVENTS.start,
|
|
timeout: EVENTS.timeout,
|
|
loadingTimeout: EVENTS.loadingTimeout,
|
|
delayTimeout: EVENTS.delayTimeout,
|
|
fullscreen: 'fullscreen',
|
|
webFullscreen: EVENTS.webFullscreen,
|
|
play: EVENTS.play,
|
|
pause: EVENTS.pause,
|
|
mute: EVENTS.mute,
|
|
stats: EVENTS.stats,
|
|
volumechange: EVENTS.volumechange,
|
|
performance: EVENTS.performance,
|
|
recordingTimestamp: EVENTS.recordingTimestamp,
|
|
recordStart: EVENTS.recordStart,
|
|
recordEnd: EVENTS.recordEnd,
|
|
playToRenderTimes: EVENTS.playToRenderTimes
|
|
};
|
|
const EVENTS_ERROR = {
|
|
playError: 'playIsNotPauseOrUrlIsNull',
|
|
fetchError: "fetchError",
|
|
websocketError: 'websocketError',
|
|
webcodecsH265NotSupport: 'webcodecsH265NotSupport',
|
|
webcodecsDecodeError: 'webcodecsDecodeError',
|
|
webcodecsWidthOrHeightChange: 'webcodecsWidthOrHeightChange',
|
|
mediaSourceH265NotSupport: 'mediaSourceH265NotSupport',
|
|
mediaSourceFull: EVENTS.mseSourceBufferFull,
|
|
mseSourceBufferError: EVENTS.mseSourceBufferError,
|
|
mediaSourceAppendBufferError: 'mediaSourceAppendBufferError',
|
|
mediaSourceBufferListLarge: 'mediaSourceBufferListLarge',
|
|
mediaSourceAppendBufferEndTimeout: 'mediaSourceAppendBufferEndTimeout',
|
|
wasmDecodeError: 'wasmDecodeError',
|
|
webglAlignmentError: 'webglAlignmentError'
|
|
};
|
|
const WEBSOCKET_STATUS = {
|
|
notConnect: 'notConnect',
|
|
open: 'open',
|
|
close: 'close',
|
|
error: 'error'
|
|
};
|
|
const SCREENSHOT_TYPE = {
|
|
download: 'download',
|
|
base64: 'base64',
|
|
blob: 'blob'
|
|
};
|
|
const VIDEO_ENC_TYPE = {
|
|
7: 'H264(AVC)',
|
|
//
|
|
12: 'H265(HEVC)' //
|
|
|
|
};
|
|
const VIDEO_ENC_CODE = {
|
|
h264: 7,
|
|
h265: 12
|
|
};
|
|
const AUDIO_ENC_TYPE = {
|
|
10: 'AAC',
|
|
7: 'ALAW',
|
|
8: 'MULAW'
|
|
};
|
|
const CONTROL_HEIGHT = 38;
|
|
const SCALE_MODE_TYPE = {
|
|
full: 0,
|
|
// 视频画面完全填充canvas区域,画面会被拉伸
|
|
auto: 1,
|
|
// 视频画面做等比缩放后,高或宽对齐canvas区域,画面不被拉伸,但有黑边
|
|
fullAuto: 2 // 视频画面做等比缩放后,完全填充canvas区域,画面不被拉伸,没有黑边,但画面显示不全
|
|
|
|
};
|
|
const CANVAS_RENDER_TYPE = {
|
|
webcodecs: 'webcodecs',
|
|
webgl: 'webgl',
|
|
offscreen: 'offscreen'
|
|
};
|
|
const ENCODED_VIDEO_TYPE = {
|
|
key: 'key',
|
|
delta: 'delta'
|
|
};
|
|
const MP4_CODECS = {
|
|
avc: 'video/mp4; codecs="avc1.64002A"',
|
|
hev: 'video/mp4; codecs="hev1.1.6.L123.b0"'
|
|
};
|
|
const MEDIA_SOURCE_STATE = {
|
|
ended: 'ended',
|
|
open: 'open',
|
|
closed: 'closed'
|
|
}; // frag duration
|
|
const AUDIO_SYNC_VIDEO_DIFF = 1000;
|
|
const HOT_KEY = {
|
|
esc: 27,
|
|
//
|
|
arrowUp: 38,
|
|
//
|
|
arrowDown: 40 //
|
|
|
|
};
|
|
const WCS_ERROR = {
|
|
keyframeIsRequiredError: 'A key frame is required after configure() or flush()',
|
|
canNotDecodeClosedCodec: "Cannot call 'decode' on a closed codec"
|
|
};
|
|
const FETCH_ERROR = {
|
|
abortError1: 'The user aborted a request',
|
|
abortError2: 'AbortError',
|
|
abort: 'AbortError'
|
|
};
|
|
|
|
class Debug {
|
|
constructor(master) {
|
|
this.log = function (name) {
|
|
if (master._opt.debug) {
|
|
for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
|
args[_key - 1] = arguments[_key];
|
|
}
|
|
|
|
console.log(`Jessibuca: [${name}]`, ...args);
|
|
}
|
|
};
|
|
|
|
this.warn = function (name) {
|
|
if (master._opt.debug) {
|
|
for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
|
|
args[_key2 - 1] = arguments[_key2];
|
|
}
|
|
|
|
console.warn(`Jessibuca: [${name}]`, ...args);
|
|
}
|
|
};
|
|
|
|
this.error = function (name) {
|
|
for (var _len3 = arguments.length, args = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {
|
|
args[_key3 - 1] = arguments[_key3];
|
|
}
|
|
|
|
console.error(`Jessibuca: [${name}]`, ...args);
|
|
};
|
|
}
|
|
|
|
}
|
|
|
|
class Events {
|
|
constructor(master) {
|
|
this.destroys = [];
|
|
this.proxy = this.proxy.bind(this);
|
|
this.master = master;
|
|
}
|
|
|
|
proxy(target, name, callback) {
|
|
let option = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
|
|
|
|
if (!target) {
|
|
return;
|
|
}
|
|
|
|
if (Array.isArray(name)) {
|
|
return name.map(item => this.proxy(target, item, callback, option));
|
|
}
|
|
|
|
target.addEventListener(name, callback, option);
|
|
|
|
const destroy = () => target.removeEventListener(name, callback, option);
|
|
|
|
this.destroys.push(destroy);
|
|
return destroy;
|
|
}
|
|
|
|
destroy() {
|
|
this.master.debug && this.master.debug.log(`Events`, 'destroy');
|
|
this.destroys.forEach(event => event());
|
|
}
|
|
|
|
}
|
|
|
|
var property$1 = (player => {
|
|
Object.defineProperty(player, 'rect', {
|
|
get: () => {
|
|
const clientRect = player.$container.getBoundingClientRect();
|
|
clientRect.width = Math.max(clientRect.width, player.$container.clientWidth);
|
|
clientRect.height = Math.max(clientRect.height, player.$container.clientHeight);
|
|
return clientRect;
|
|
}
|
|
});
|
|
['bottom', 'height', 'left', 'right', 'top', 'width'].forEach(key => {
|
|
Object.defineProperty(player, key, {
|
|
get: () => {
|
|
return player.rect[key];
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
var screenfull = createCommonjsModule(function (module) {
|
|
/*!
|
|
* screenfull
|
|
* v5.1.0 - 2020-12-24
|
|
* (c) Sindre Sorhus; MIT License
|
|
*/
|
|
(function () {
|
|
|
|
var document = typeof window !== 'undefined' && typeof window.document !== 'undefined' ? window.document : {};
|
|
var isCommonjs = module.exports;
|
|
|
|
var fn = (function () {
|
|
var val;
|
|
|
|
var fnMap = [
|
|
[
|
|
'requestFullscreen',
|
|
'exitFullscreen',
|
|
'fullscreenElement',
|
|
'fullscreenEnabled',
|
|
'fullscreenchange',
|
|
'fullscreenerror'
|
|
],
|
|
// New WebKit
|
|
[
|
|
'webkitRequestFullscreen',
|
|
'webkitExitFullscreen',
|
|
'webkitFullscreenElement',
|
|
'webkitFullscreenEnabled',
|
|
'webkitfullscreenchange',
|
|
'webkitfullscreenerror'
|
|
|
|
],
|
|
// Old WebKit
|
|
[
|
|
'webkitRequestFullScreen',
|
|
'webkitCancelFullScreen',
|
|
'webkitCurrentFullScreenElement',
|
|
'webkitCancelFullScreen',
|
|
'webkitfullscreenchange',
|
|
'webkitfullscreenerror'
|
|
|
|
],
|
|
[
|
|
'mozRequestFullScreen',
|
|
'mozCancelFullScreen',
|
|
'mozFullScreenElement',
|
|
'mozFullScreenEnabled',
|
|
'mozfullscreenchange',
|
|
'mozfullscreenerror'
|
|
],
|
|
[
|
|
'msRequestFullscreen',
|
|
'msExitFullscreen',
|
|
'msFullscreenElement',
|
|
'msFullscreenEnabled',
|
|
'MSFullscreenChange',
|
|
'MSFullscreenError'
|
|
]
|
|
];
|
|
|
|
var i = 0;
|
|
var l = fnMap.length;
|
|
var ret = {};
|
|
|
|
for (; i < l; i++) {
|
|
val = fnMap[i];
|
|
if (val && val[1] in document) {
|
|
for (i = 0; i < val.length; i++) {
|
|
ret[fnMap[0][i]] = val[i];
|
|
}
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
})();
|
|
|
|
var eventNameMap = {
|
|
change: fn.fullscreenchange,
|
|
error: fn.fullscreenerror
|
|
};
|
|
|
|
var screenfull = {
|
|
request: function (element, options) {
|
|
return new Promise(function (resolve, reject) {
|
|
var onFullScreenEntered = function () {
|
|
this.off('change', onFullScreenEntered);
|
|
resolve();
|
|
}.bind(this);
|
|
|
|
this.on('change', onFullScreenEntered);
|
|
|
|
element = element || document.documentElement;
|
|
|
|
var returnPromise = element[fn.requestFullscreen](options);
|
|
|
|
if (returnPromise instanceof Promise) {
|
|
returnPromise.then(onFullScreenEntered).catch(reject);
|
|
}
|
|
}.bind(this));
|
|
},
|
|
exit: function () {
|
|
return new Promise(function (resolve, reject) {
|
|
if (!this.isFullscreen) {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
var onFullScreenExit = function () {
|
|
this.off('change', onFullScreenExit);
|
|
resolve();
|
|
}.bind(this);
|
|
|
|
this.on('change', onFullScreenExit);
|
|
|
|
var returnPromise = document[fn.exitFullscreen]();
|
|
|
|
if (returnPromise instanceof Promise) {
|
|
returnPromise.then(onFullScreenExit).catch(reject);
|
|
}
|
|
}.bind(this));
|
|
},
|
|
toggle: function (element, options) {
|
|
return this.isFullscreen ? this.exit() : this.request(element, options);
|
|
},
|
|
onchange: function (callback) {
|
|
this.on('change', callback);
|
|
},
|
|
onerror: function (callback) {
|
|
this.on('error', callback);
|
|
},
|
|
on: function (event, callback) {
|
|
var eventName = eventNameMap[event];
|
|
if (eventName) {
|
|
document.addEventListener(eventName, callback, false);
|
|
}
|
|
},
|
|
off: function (event, callback) {
|
|
var eventName = eventNameMap[event];
|
|
if (eventName) {
|
|
document.removeEventListener(eventName, callback, false);
|
|
}
|
|
},
|
|
raw: fn
|
|
};
|
|
|
|
if (!fn) {
|
|
if (isCommonjs) {
|
|
module.exports = {isEnabled: false};
|
|
} else {
|
|
window.screenfull = {isEnabled: false};
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
Object.defineProperties(screenfull, {
|
|
isFullscreen: {
|
|
get: function () {
|
|
return Boolean(document[fn.fullscreenElement]);
|
|
}
|
|
},
|
|
element: {
|
|
enumerable: true,
|
|
get: function () {
|
|
return document[fn.fullscreenElement];
|
|
}
|
|
},
|
|
isEnabled: {
|
|
enumerable: true,
|
|
get: function () {
|
|
// Coerce to boolean in case of old WebKit
|
|
return Boolean(document[fn.fullscreenEnabled]);
|
|
}
|
|
}
|
|
});
|
|
|
|
if (isCommonjs) {
|
|
module.exports = screenfull;
|
|
} else {
|
|
window.screenfull = screenfull;
|
|
}
|
|
})();
|
|
});
|
|
screenfull.isEnabled;
|
|
|
|
function noop() {}
|
|
function supportOffscreen($canvas) {
|
|
return typeof $canvas.transferControlToOffscreen === 'function';
|
|
}
|
|
function supportOffscreenV2() {
|
|
return typeof OffscreenCanvas !== "undefined";
|
|
}
|
|
function createContextGL($canvas) {
|
|
let gl = null;
|
|
const validContextNames = ["webgl", "experimental-webgl", "moz-webgl", "webkit-3d"];
|
|
let nameIndex = 0;
|
|
|
|
while (!gl && nameIndex < validContextNames.length) {
|
|
const contextName = validContextNames[nameIndex];
|
|
|
|
try {
|
|
let contextOptions = {
|
|
preserveDrawingBuffer: true
|
|
};
|
|
gl = $canvas.getContext(contextName, contextOptions);
|
|
} catch (e) {
|
|
gl = null;
|
|
}
|
|
|
|
if (!gl || typeof gl.getParameter !== "function") {
|
|
gl = null;
|
|
}
|
|
|
|
++nameIndex;
|
|
}
|
|
|
|
return gl;
|
|
}
|
|
function dataURLToFile() {
|
|
let dataURL = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
|
|
const arr = dataURL.split(",");
|
|
const bstr = atob(arr[1]);
|
|
const type = arr[0].replace("data:", "").replace(";base64", "");
|
|
let n = bstr.length,
|
|
u8arr = new Uint8Array(n);
|
|
|
|
while (n--) {
|
|
u8arr[n] = bstr.charCodeAt(n);
|
|
}
|
|
|
|
return new File([u8arr], 'file', {
|
|
type
|
|
});
|
|
}
|
|
function now() {
|
|
return new Date().getTime();
|
|
}
|
|
(() => {
|
|
try {
|
|
if (typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function") {
|
|
const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00));
|
|
if (module instanceof WebAssembly.Module) return new WebAssembly.Instance(module) instanceof WebAssembly.Instance;
|
|
}
|
|
} catch (e) {}
|
|
|
|
return false;
|
|
})();
|
|
function clamp(num, a, b) {
|
|
return Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b));
|
|
}
|
|
function setStyle(element, key, value) {
|
|
if (!element) {
|
|
return;
|
|
}
|
|
|
|
if (typeof key === 'object') {
|
|
Object.keys(key).forEach(item => {
|
|
setStyle(element, item, key[item]);
|
|
});
|
|
}
|
|
|
|
element.style[key] = value;
|
|
return element;
|
|
}
|
|
function getStyle(element, key) {
|
|
let numberType = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
|
|
|
|
if (!element) {
|
|
return 0;
|
|
}
|
|
|
|
const value = getComputedStyle(element, null).getPropertyValue(key);
|
|
return numberType ? parseFloat(value) : value;
|
|
}
|
|
function getNowTime() {
|
|
if (performance && typeof performance.now === 'function') {
|
|
return performance.now();
|
|
}
|
|
|
|
return Date.now();
|
|
}
|
|
function calculationRate(callback) {
|
|
let totalSize = 0;
|
|
let lastTime = getNowTime();
|
|
return size => {
|
|
totalSize += size;
|
|
const thisTime = getNowTime();
|
|
const diffTime = thisTime - lastTime;
|
|
|
|
if (diffTime >= 1000) {
|
|
callback(totalSize / diffTime * 1000);
|
|
lastTime = thisTime;
|
|
totalSize = 0;
|
|
}
|
|
};
|
|
}
|
|
function isMobile() {
|
|
return /iphone|ipod|android.*mobile|windows.*phone|blackberry.*mobile/i.test(window.navigator.userAgent.toLowerCase());
|
|
}
|
|
|
|
function supportWCS() {
|
|
return "VideoEncoder" in window;
|
|
}
|
|
function formatVideoDecoderConfigure(avcC) {
|
|
let codecArray = avcC.subarray(1, 4);
|
|
let codecString = "avc1.";
|
|
|
|
for (let j = 0; j < 3; j++) {
|
|
let h = codecArray[j].toString(16);
|
|
|
|
if (h.length < 2) {
|
|
h = "0" + h;
|
|
}
|
|
|
|
codecString += h;
|
|
}
|
|
|
|
return {
|
|
codec: codecString,
|
|
description: avcC
|
|
};
|
|
}
|
|
function isFullScreen() {
|
|
return screenfull.isFullscreen;
|
|
}
|
|
function bpsSize(value) {
|
|
if (null == value || value === '' || parseInt(value) === 0 || isNaN(parseInt(value))) {
|
|
return "0KB/s";
|
|
}
|
|
|
|
let size = parseFloat(value);
|
|
size = size.toFixed(2);
|
|
return size + 'KB/s';
|
|
}
|
|
function fpsStatus(fps) {
|
|
let result = 0;
|
|
|
|
if (fps >= 24) {
|
|
result = 2;
|
|
} else if (fps >= 15) {
|
|
result = 1;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
function createEmptyImageBitmap(width, height) {
|
|
const $canvasElement = document.createElement("canvas");
|
|
$canvasElement.width = width;
|
|
$canvasElement.height = height;
|
|
return window.createImageBitmap($canvasElement, 0, 0, width, height);
|
|
}
|
|
function supportMSE() {
|
|
return window.MediaSource && window.MediaSource.isTypeSupported(MP4_CODECS.avc);
|
|
}
|
|
function supportMediaStreamTrack() {
|
|
return window.MediaStreamTrackGenerator && typeof window.MediaStreamTrackGenerator === 'function';
|
|
}
|
|
function isEmpty(value) {
|
|
return value === null || value === undefined;
|
|
}
|
|
function isBoolean(value) {
|
|
return value === true || value === false;
|
|
}
|
|
function isNotEmpty(value) {
|
|
return !isEmpty(value);
|
|
}
|
|
function initPlayTimes() {
|
|
return {
|
|
playInitStart: '',
|
|
//1
|
|
playStart: '',
|
|
// 2
|
|
streamStart: '',
|
|
//3
|
|
streamResponse: '',
|
|
// 4
|
|
demuxStart: '',
|
|
// 5
|
|
decodeStart: '',
|
|
// 6
|
|
videoStart: '',
|
|
// 7
|
|
playTimestamp: '',
|
|
// playStart- playInitStart
|
|
streamTimestamp: '',
|
|
// streamStart - playStart
|
|
streamResponseTimestamp: '',
|
|
// streamResponse - streamStart
|
|
demuxTimestamp: '',
|
|
// demuxStart - streamResponse
|
|
decodeTimestamp: '',
|
|
// decodeStart - demuxStart
|
|
videoTimestamp: '',
|
|
// videoStart - decodeStart
|
|
allTimestamp: '' // videoStart - playInitStart
|
|
|
|
};
|
|
} // create watermark
|
|
function formatTimeTips(time) {
|
|
var result; //
|
|
|
|
if (time > -1) {
|
|
var hour = Math.floor(time / 3600);
|
|
var min = Math.floor(time / 60) % 60;
|
|
var sec = time % 60;
|
|
sec = Math.round(sec);
|
|
|
|
if (hour < 10) {
|
|
result = '0' + hour + ":";
|
|
} else {
|
|
result = hour + ":";
|
|
}
|
|
|
|
if (min < 10) {
|
|
result += "0";
|
|
}
|
|
|
|
result += min + ":";
|
|
|
|
if (sec < 10) {
|
|
result += "0";
|
|
}
|
|
|
|
result += sec.toFixed(0);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
function getTarget(e) {
|
|
const event = e || window.event;
|
|
const target = event.target || event.srcElement;
|
|
return target;
|
|
}
|
|
function isWebglRenderSupport(width) {
|
|
return width / 2 % 4 === 0;
|
|
}
|
|
function getBrowser() {
|
|
const UserAgent = navigator.userAgent.toLowerCase();
|
|
const browserInfo = {};
|
|
const browserArray = {
|
|
IE: window.ActiveXObject || "ActiveXObject" in window,
|
|
// IE
|
|
Chrome: UserAgent.indexOf('chrome') > -1 && UserAgent.indexOf('safari') > -1,
|
|
// Chrome浏览器
|
|
Firefox: UserAgent.indexOf('firefox') > -1,
|
|
// 火狐浏览器
|
|
Opera: UserAgent.indexOf('opera') > -1,
|
|
// Opera浏览器
|
|
Safari: UserAgent.indexOf('safari') > -1 && UserAgent.indexOf('chrome') == -1,
|
|
// safari浏览器
|
|
Edge: UserAgent.indexOf('edge') > -1,
|
|
// Edge浏览器
|
|
QQBrowser: /qqbrowser/.test(UserAgent),
|
|
// qq浏览器
|
|
WeixinBrowser: /MicroMessenger/i.test(UserAgent) // 微信浏览器
|
|
|
|
}; // console.log(browserArray)
|
|
|
|
for (let i in browserArray) {
|
|
if (browserArray[i]) {
|
|
let versions = '';
|
|
|
|
if (i === 'IE') {
|
|
versions = UserAgent.match(/(msie\s|trident.*rv:)([\w.]+)/)[2];
|
|
} else if (i === 'Chrome') {
|
|
for (let mt in navigator.mimeTypes) {
|
|
//检测是否是360浏览器(测试只有pc端的360才起作用)
|
|
if (navigator.mimeTypes[mt]['type'] === 'application/360softmgrplugin') {
|
|
i = '360';
|
|
}
|
|
}
|
|
|
|
versions = UserAgent.match(/chrome\/([\d.]+)/)[1];
|
|
} else if (i === 'Firefox') {
|
|
versions = UserAgent.match(/firefox\/([\d.]+)/)[1];
|
|
} else if (i === 'Opera') {
|
|
versions = UserAgent.match(/opera\/([\d.]+)/)[1];
|
|
} else if (i === 'Safari') {
|
|
versions = UserAgent.match(/version\/([\d.]+)/)[1];
|
|
} else if (i === 'Edge') {
|
|
versions = UserAgent.match(/edge\/([\d.]+)/)[1];
|
|
} else if (i === 'QQBrowser') {
|
|
versions = UserAgent.match(/qqbrowser\/([\d.]+)/)[1];
|
|
}
|
|
|
|
browserInfo.type = i;
|
|
browserInfo.version = parseInt(versions);
|
|
}
|
|
}
|
|
|
|
return browserInfo;
|
|
}
|
|
function closeVideoFrame(videoFrame) {
|
|
if (videoFrame.close) {
|
|
videoFrame.close();
|
|
} else if (videoFrame.destroy) {
|
|
videoFrame.destroy();
|
|
}
|
|
}
|
|
|
|
var events$1 = (player => {
|
|
try {
|
|
const screenfullChange = e => {
|
|
if (getTarget(e) === player.$container) {
|
|
player.emit(JESSIBUCA_EVENTS.fullscreen, player.fullscreen); // 如果不是fullscreen,则触发下 resize 方法
|
|
|
|
if (!player.fullscreen) {
|
|
player.resize();
|
|
} else {
|
|
if (player._opt.useMSE) {
|
|
player.resize();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
screenfull.on('change', screenfullChange);
|
|
player.events.destroys.push(() => {
|
|
screenfull.off('change', screenfullChange);
|
|
});
|
|
} catch (error) {//
|
|
} //
|
|
|
|
|
|
player.on(EVENTS.decoderWorkerInit, () => {
|
|
player.debug.log('player', 'has loaded');
|
|
player.loaded = true;
|
|
}); //
|
|
|
|
player.on(EVENTS.play, () => {
|
|
player.loading = false;
|
|
}); //
|
|
|
|
player.on(EVENTS.fullscreen, value => {
|
|
if (value) {
|
|
try {
|
|
screenfull.request(player.$container).then(() => {}).catch(e => {
|
|
if (isMobile() && player._opt.useWebFullScreen) {
|
|
player.webFullscreen = true;
|
|
}
|
|
});
|
|
} catch (e) {
|
|
if (isMobile() && player._opt.useWebFullScreen) {
|
|
player.webFullscreen = true;
|
|
}
|
|
}
|
|
} else {
|
|
try {
|
|
screenfull.exit().then(() => {
|
|
if (player.webFullscreen) {
|
|
player.webFullscreen = false;
|
|
}
|
|
}).catch(() => {
|
|
player.webFullscreen = false;
|
|
});
|
|
} catch (e) {
|
|
player.webFullscreen = false;
|
|
}
|
|
}
|
|
});
|
|
|
|
if (isMobile()) {
|
|
player.on(EVENTS.webFullscreen, value => {
|
|
if (value) {
|
|
player.$container.classList.add('jessibuca-fullscreen-web');
|
|
} else {
|
|
player.$container.classList.remove('jessibuca-fullscreen-web');
|
|
} //
|
|
|
|
|
|
player.emit(JESSIBUCA_EVENTS.fullscreen, player.fullscreen);
|
|
});
|
|
} //
|
|
|
|
|
|
player.on(EVENTS.resize, () => {
|
|
player.video && player.video.resize();
|
|
});
|
|
|
|
if (player._opt.debug) {
|
|
const ignoreList = [EVENTS.timeUpdate];
|
|
Object.keys(EVENTS).forEach(key => {
|
|
player.on(EVENTS[key], value => {
|
|
if (ignoreList.includes(key)) {
|
|
return;
|
|
}
|
|
|
|
player.debug.log('player events', EVENTS[key], value);
|
|
});
|
|
});
|
|
Object.keys(EVENTS_ERROR).forEach(key => {
|
|
player.on(EVENTS_ERROR[key], value => {
|
|
player.debug.log('player event error', EVENTS_ERROR[key], value);
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
class Emitter {
|
|
on(name, fn, ctx) {
|
|
const e = this.e || (this.e = {});
|
|
(e[name] || (e[name] = [])).push({
|
|
fn,
|
|
ctx
|
|
});
|
|
return this;
|
|
}
|
|
|
|
once(name, fn, ctx) {
|
|
const self = this;
|
|
|
|
function listener() {
|
|
self.off(name, listener);
|
|
|
|
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
args[_key] = arguments[_key];
|
|
}
|
|
|
|
fn.apply(ctx, args);
|
|
}
|
|
|
|
listener._ = fn;
|
|
return this.on(name, listener, ctx);
|
|
}
|
|
|
|
emit(name) {
|
|
const evtArr = ((this.e || (this.e = {}))[name] || []).slice();
|
|
|
|
for (var _len2 = arguments.length, data = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
|
|
data[_key2 - 1] = arguments[_key2];
|
|
}
|
|
|
|
for (let i = 0; i < evtArr.length; i += 1) {
|
|
evtArr[i].fn.apply(evtArr[i].ctx, data);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
off(name, callback) {
|
|
const e = this.e || (this.e = {});
|
|
|
|
if (!name) {
|
|
Object.keys(e).forEach(key => {
|
|
delete e[key];
|
|
});
|
|
delete this.e;
|
|
return;
|
|
}
|
|
|
|
const evts = e[name];
|
|
const liveEvents = [];
|
|
|
|
if (evts && callback) {
|
|
for (let i = 0, len = evts.length; i < len; i += 1) {
|
|
if (evts[i].fn !== callback && evts[i].fn._ !== callback) liveEvents.push(evts[i]);
|
|
}
|
|
}
|
|
|
|
if (liveEvents.length) {
|
|
e[name] = liveEvents;
|
|
} else {
|
|
delete e[name];
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
}
|
|
|
|
var createWebGL = ((gl, openWebglAlignment) => {
|
|
var vertexShaderScript = ['attribute vec4 vertexPos;', 'attribute vec4 texturePos;', 'varying vec2 textureCoord;', 'void main()', '{', 'gl_Position = vertexPos;', 'textureCoord = texturePos.xy;', '}'].join('\n');
|
|
var fragmentShaderScript = ['precision highp float;', 'varying highp vec2 textureCoord;', 'uniform sampler2D ySampler;', 'uniform sampler2D uSampler;', 'uniform sampler2D vSampler;', 'const mat4 YUV2RGB = mat4', '(', '1.1643828125, 0, 1.59602734375, -.87078515625,', '1.1643828125, -.39176171875, -.81296875, .52959375,', '1.1643828125, 2.017234375, 0, -1.081390625,', '0, 0, 0, 1', ');', 'void main(void) {', 'highp float y = texture2D(ySampler, textureCoord).r;', 'highp float u = texture2D(uSampler, textureCoord).r;', 'highp float v = texture2D(vSampler, textureCoord).r;', 'gl_FragColor = vec4(y, u, v, 1) * YUV2RGB;', '}'].join('\n');
|
|
|
|
if (openWebglAlignment) {
|
|
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
}
|
|
|
|
var vertexShader = gl.createShader(gl.VERTEX_SHADER);
|
|
gl.shaderSource(vertexShader, vertexShaderScript);
|
|
gl.compileShader(vertexShader);
|
|
|
|
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
|
|
console.log('Vertex shader failed to compile: ' + gl.getShaderInfoLog(vertexShader));
|
|
}
|
|
|
|
var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
|
|
gl.shaderSource(fragmentShader, fragmentShaderScript);
|
|
gl.compileShader(fragmentShader);
|
|
|
|
if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
|
|
console.log('Fragment shader failed to compile: ' + gl.getShaderInfoLog(fragmentShader));
|
|
}
|
|
|
|
var program = gl.createProgram();
|
|
gl.attachShader(program, vertexShader);
|
|
gl.attachShader(program, fragmentShader);
|
|
gl.linkProgram(program);
|
|
|
|
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
console.log('Program failed to compile: ' + gl.getProgramInfoLog(program));
|
|
}
|
|
|
|
gl.useProgram(program); // initBuffers
|
|
|
|
var vertexPosBuffer = gl.createBuffer();
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, vertexPosBuffer);
|
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 1, -1, 1, 1, -1, -1, -1]), gl.STATIC_DRAW);
|
|
var vertexPosRef = gl.getAttribLocation(program, 'vertexPos');
|
|
gl.enableVertexAttribArray(vertexPosRef);
|
|
gl.vertexAttribPointer(vertexPosRef, 2, gl.FLOAT, false, 0, 0);
|
|
var texturePosBuffer = gl.createBuffer();
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, texturePosBuffer);
|
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 0, 0, 0, 1, 1, 0, 1]), gl.STATIC_DRAW);
|
|
var texturePosRef = gl.getAttribLocation(program, 'texturePos');
|
|
gl.enableVertexAttribArray(texturePosRef);
|
|
gl.vertexAttribPointer(texturePosRef, 2, gl.FLOAT, false, 0, 0);
|
|
|
|
function _initTexture(name, index) {
|
|
var textureRef = gl.createTexture();
|
|
gl.bindTexture(gl.TEXTURE_2D, textureRef);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
gl.uniform1i(gl.getUniformLocation(program, name), index);
|
|
return textureRef;
|
|
}
|
|
|
|
var yTextureRef = _initTexture('ySampler', 0);
|
|
|
|
var uTextureRef = _initTexture('uSampler', 1);
|
|
|
|
var vTextureRef = _initTexture('vSampler', 2);
|
|
|
|
return {
|
|
render: function (w, h, y, u, v) {
|
|
gl.viewport(0, 0, w, h);
|
|
gl.activeTexture(gl.TEXTURE0);
|
|
gl.bindTexture(gl.TEXTURE_2D, yTextureRef);
|
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, w, h, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, y);
|
|
gl.activeTexture(gl.TEXTURE1);
|
|
gl.bindTexture(gl.TEXTURE_2D, uTextureRef);
|
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, w / 2, h / 2, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, u);
|
|
gl.activeTexture(gl.TEXTURE2);
|
|
gl.bindTexture(gl.TEXTURE_2D, vTextureRef);
|
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, w / 2, h / 2, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, v);
|
|
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
},
|
|
destroy: function () {
|
|
try {
|
|
gl.deleteProgram(program);
|
|
gl.deleteBuffer(vertexPosBuffer);
|
|
gl.deleteBuffer(texturePosBuffer);
|
|
gl.deleteTexture(yTextureRef);
|
|
gl.deleteTexture(uTextureRef);
|
|
gl.deleteTexture(vTextureRef);
|
|
} catch (e) {// console.error(e);
|
|
}
|
|
}
|
|
};
|
|
});
|
|
|
|
class CommonLoader$1 extends Emitter {
|
|
constructor() {
|
|
super();
|
|
this.init = false;
|
|
}
|
|
|
|
resetInit() {
|
|
this.init = false;
|
|
this.videoInfo = {
|
|
width: '',
|
|
height: '',
|
|
encType: '',
|
|
encTypeCode: ''
|
|
};
|
|
}
|
|
|
|
destroy() {
|
|
this.resetInit();
|
|
this.player.$container.removeChild(this.$videoElement);
|
|
this.off();
|
|
} //
|
|
|
|
|
|
updateVideoInfo(data) {
|
|
if (data.encTypeCode) {
|
|
this.videoInfo.encType = VIDEO_ENC_TYPE[data.encTypeCode];
|
|
}
|
|
|
|
if (data.width) {
|
|
this.videoInfo.width = data.width;
|
|
}
|
|
|
|
if (data.height) {
|
|
this.videoInfo.height = data.height;
|
|
} // video 基本信息
|
|
|
|
|
|
if (this.videoInfo.encType && this.videoInfo.height && this.videoInfo.width && !this.init) {
|
|
this.player.emit(EVENTS.videoInfo, this.videoInfo);
|
|
this.init = true;
|
|
}
|
|
}
|
|
|
|
play() {}
|
|
|
|
pause() {}
|
|
|
|
clearView() {}
|
|
|
|
}
|
|
|
|
/*
|
|
* FileSaver.js
|
|
* A saveAs() FileSaver implementation.
|
|
*
|
|
* By Eli Grey, http://eligrey.com
|
|
*
|
|
* License : https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md (MIT)
|
|
* source : http://purl.eligrey.com/github/FileSaver.js
|
|
*/
|
|
// The one and only way of getting global scope in all environments
|
|
// https://stackoverflow.com/q/3277182/1008999
|
|
var _global = typeof window === 'object' && window.window === window ? window : typeof self === 'object' && self.self === self ? self : typeof global === 'object' && global.global === global ? global : undefined;
|
|
|
|
function bom(blob, opts) {
|
|
if (typeof opts === 'undefined') opts = {
|
|
autoBom: false
|
|
};else if (typeof opts !== 'object') {
|
|
console.warn('Deprecated: Expected third argument to be a object');
|
|
opts = {
|
|
autoBom: !opts
|
|
};
|
|
} // prepend BOM for UTF-8 XML and text/* types (including HTML)
|
|
// note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF
|
|
|
|
if (opts.autoBom && /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
|
|
return new Blob([String.fromCharCode(0xFEFF), blob], {
|
|
type: blob.type
|
|
});
|
|
}
|
|
|
|
return blob;
|
|
}
|
|
|
|
function download(url, name, opts) {
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.open('GET', url);
|
|
xhr.responseType = 'blob';
|
|
|
|
xhr.onload = function () {
|
|
saveAs(xhr.response, name, opts);
|
|
};
|
|
|
|
xhr.onerror = function () {
|
|
console.error('could not download file');
|
|
};
|
|
|
|
xhr.send();
|
|
}
|
|
|
|
function corsEnabled(url) {
|
|
var xhr = new XMLHttpRequest(); // use sync to avoid popup blocker
|
|
|
|
xhr.open('HEAD', url, false);
|
|
|
|
try {
|
|
xhr.send();
|
|
} catch (e) {}
|
|
|
|
return xhr.status >= 200 && xhr.status <= 299;
|
|
} // `a.click()` doesn't work for all browsers (#465)
|
|
|
|
|
|
function click(node) {
|
|
try {
|
|
node.dispatchEvent(new MouseEvent('click'));
|
|
} catch (e) {
|
|
var evt = document.createEvent('MouseEvents');
|
|
evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null);
|
|
node.dispatchEvent(evt);
|
|
}
|
|
} // Detect WebView inside a native macOS app by ruling out all browsers
|
|
// We just need to check for 'Safari' because all other browsers (besides Firefox) include that too
|
|
// https://www.whatismybrowser.com/guides/the-latest-user-agent/macos
|
|
|
|
|
|
var isMacOSWebView = _global.navigator && /Macintosh/.test(navigator.userAgent) && /AppleWebKit/.test(navigator.userAgent) && !/Safari/.test(navigator.userAgent);
|
|
var saveAs = // probably in some web worker
|
|
typeof window !== 'object' || window !== _global ? function saveAs() {
|
|
/* noop */
|
|
} // Use download attribute first if possible (#193 Lumia mobile) unless this is a macOS WebView
|
|
: 'download' in HTMLAnchorElement.prototype && !isMacOSWebView ? function saveAs(blob, name, opts) {
|
|
var URL = _global.URL || _global.webkitURL; // Namespace is used to prevent conflict w/ Chrome Poper Blocker extension (Issue #561)
|
|
|
|
var a = document.createElementNS('http://www.w3.org/1999/xhtml', 'a');
|
|
name = name || blob.name || 'download';
|
|
a.download = name;
|
|
a.rel = 'noopener'; // tabnabbing
|
|
// TODO: detect chrome extensions & packaged apps
|
|
// a.target = '_blank'
|
|
|
|
if (typeof blob === 'string') {
|
|
// Support regular links
|
|
a.href = blob;
|
|
|
|
if (a.origin !== location.origin) {
|
|
corsEnabled(a.href) ? download(blob, name, opts) : click(a, a.target = '_blank');
|
|
} else {
|
|
click(a);
|
|
}
|
|
} else {
|
|
// Support blobs
|
|
a.href = URL.createObjectURL(blob);
|
|
setTimeout(function () {
|
|
URL.revokeObjectURL(a.href);
|
|
}, 4E4); // 40s
|
|
|
|
setTimeout(function () {
|
|
click(a);
|
|
}, 0);
|
|
}
|
|
} // Use msSaveOrOpenBlob as a second approach
|
|
: 'msSaveOrOpenBlob' in navigator ? function saveAs(blob, name, opts) {
|
|
name = name || blob.name || 'download';
|
|
|
|
if (typeof blob === 'string') {
|
|
if (corsEnabled(blob)) {
|
|
download(blob, name, opts);
|
|
} else {
|
|
var a = document.createElement('a');
|
|
a.href = blob;
|
|
a.target = '_blank';
|
|
setTimeout(function () {
|
|
click(a);
|
|
});
|
|
}
|
|
} else {
|
|
navigator.msSaveOrOpenBlob(bom(blob, opts), name);
|
|
}
|
|
} // Fallback to using FileReader and a popup
|
|
: function saveAs(blob, name, opts, popup) {
|
|
// Open a popup immediately do go around popup blocker
|
|
// Mostly only available on user interaction and the fileReader is async so...
|
|
popup = popup || open('', '_blank');
|
|
|
|
if (popup) {
|
|
popup.document.title = popup.document.body.innerText = 'downloading...';
|
|
}
|
|
|
|
if (typeof blob === 'string') return download(blob, name, opts);
|
|
var force = blob.type === 'application/octet-stream';
|
|
|
|
var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari;
|
|
|
|
var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent);
|
|
|
|
if ((isChromeIOS || force && isSafari || isMacOSWebView) && typeof FileReader !== 'undefined') {
|
|
// Safari doesn't allow downloading of blob URLs
|
|
var reader = new FileReader();
|
|
|
|
reader.onloadend = function () {
|
|
var url = reader.result;
|
|
url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;');
|
|
if (popup) popup.location.href = url;else location = url;
|
|
popup = null; // reverse-tabnabbing #460
|
|
};
|
|
|
|
reader.readAsDataURL(blob);
|
|
} else {
|
|
var URL = _global.URL || _global.webkitURL;
|
|
var url = URL.createObjectURL(blob);
|
|
if (popup) popup.location = url;else location.href = url;
|
|
popup = null; // reverse-tabnabbing #460
|
|
|
|
setTimeout(function () {
|
|
URL.revokeObjectURL(url);
|
|
}, 4E4); // 40s
|
|
}
|
|
};
|
|
|
|
class CanvasVideoLoader extends CommonLoader$1 {
|
|
constructor(player) {
|
|
super();
|
|
this.player = player;
|
|
const $canvasElement = document.createElement("canvas");
|
|
$canvasElement.style.position = "absolute";
|
|
$canvasElement.style.top = 0;
|
|
$canvasElement.style.left = 0;
|
|
this.$videoElement = $canvasElement;
|
|
player.$container.appendChild(this.$videoElement);
|
|
this.context2D = null;
|
|
this.contextGl = null;
|
|
this.contextGlRender = null;
|
|
this.contextGlDestroy = null;
|
|
this.bitmaprenderer = null;
|
|
this.renderType = null;
|
|
this.videoInfo = {
|
|
width: '',
|
|
height: '',
|
|
encType: ''
|
|
}; //
|
|
|
|
this._initCanvasRender();
|
|
|
|
this.player.debug.log('CanvasVideo', 'init');
|
|
}
|
|
|
|
destroy() {
|
|
super.destroy();
|
|
|
|
if (this.contextGl) {
|
|
this.contextGl = null;
|
|
}
|
|
|
|
if (this.context2D) {
|
|
this.context2D = null;
|
|
}
|
|
|
|
if (this.contextGlRender) {
|
|
this.contextGlDestroy && this.contextGlDestroy();
|
|
this.contextGlDestroy = null;
|
|
this.contextGlRender = null;
|
|
}
|
|
|
|
if (this.bitmaprenderer) {
|
|
this.bitmaprenderer = null;
|
|
}
|
|
|
|
this.renderType = null;
|
|
this.player.debug.log(`CanvasVideoLoader`, 'destroy');
|
|
}
|
|
|
|
_initContextGl() {
|
|
this.contextGl = createContextGL(this.$videoElement);
|
|
const webgl = createWebGL(this.contextGl, this.player._opt.openWebglAlignment);
|
|
this.contextGlRender = webgl.render;
|
|
this.contextGlDestroy = webgl.destroy;
|
|
}
|
|
|
|
_initContext2D() {
|
|
this.context2D = this.$videoElement.getContext('2d');
|
|
} // 渲染类型
|
|
|
|
|
|
_initCanvasRender() {
|
|
if (this.player._opt.useWCS && !this._supportOffscreen()) {
|
|
this.renderType = CANVAS_RENDER_TYPE.webcodecs;
|
|
|
|
this._initContext2D();
|
|
} else if (this._supportOffscreen()) {
|
|
this.renderType = CANVAS_RENDER_TYPE.offscreen;
|
|
|
|
this._bindOffscreen();
|
|
} else {
|
|
this.renderType = CANVAS_RENDER_TYPE.webgl;
|
|
|
|
this._initContextGl();
|
|
}
|
|
}
|
|
|
|
_supportOffscreen() {
|
|
return supportOffscreen(this.$videoElement) && this.player._opt.useOffscreen;
|
|
} //
|
|
|
|
|
|
_bindOffscreen() {
|
|
this.bitmaprenderer = this.$videoElement.getContext('bitmaprenderer');
|
|
}
|
|
|
|
initCanvasViewSize() {
|
|
this.$videoElement.width = this.videoInfo.width;
|
|
this.$videoElement.height = this.videoInfo.height;
|
|
this.resize();
|
|
} //
|
|
|
|
|
|
render(msg) {
|
|
this.player.videoTimestamp = msg.ts;
|
|
|
|
switch (this.renderType) {
|
|
case CANVAS_RENDER_TYPE.offscreen:
|
|
this.bitmaprenderer.transferFromImageBitmap(msg.buffer);
|
|
break;
|
|
|
|
case CANVAS_RENDER_TYPE.webgl:
|
|
this.contextGlRender(this.$videoElement.width, this.$videoElement.height, msg.output[0], msg.output[1], msg.output[2]);
|
|
break;
|
|
|
|
case CANVAS_RENDER_TYPE.webcodecs:
|
|
// can use createImageBitmap in wexin
|
|
this.context2D.drawImage(msg.videoFrame, 0, 0, this.$videoElement.width, this.$videoElement.height);
|
|
closeVideoFrame(msg.videoFrame);
|
|
break;
|
|
}
|
|
}
|
|
|
|
screenshot(filename, format, quality, type) {
|
|
filename = filename || now();
|
|
type = type || SCREENSHOT_TYPE.download;
|
|
const formatType = {
|
|
png: 'image/png',
|
|
jpeg: 'image/jpeg',
|
|
webp: 'image/webp'
|
|
};
|
|
let encoderOptions = 0.92;
|
|
|
|
if (!formatType[format] && SCREENSHOT_TYPE[format]) {
|
|
type = format;
|
|
format = 'png';
|
|
quality = undefined;
|
|
}
|
|
|
|
if (typeof quality === "string") {
|
|
type = quality;
|
|
quality = undefined;
|
|
}
|
|
|
|
if (typeof quality !== 'undefined') {
|
|
encoderOptions = Number(quality);
|
|
}
|
|
|
|
const dataURL = this.$videoElement.toDataURL(formatType[format] || formatType.png, encoderOptions);
|
|
|
|
if (type === SCREENSHOT_TYPE.base64) {
|
|
return dataURL;
|
|
} else {
|
|
const file = dataURLToFile(dataURL);
|
|
|
|
if (type === SCREENSHOT_TYPE.blob) {
|
|
return file;
|
|
} else if (type === SCREENSHOT_TYPE.download) {
|
|
// downloadImg(file, filename);
|
|
saveAs(file, filename);
|
|
}
|
|
}
|
|
} //
|
|
|
|
|
|
clearView() {
|
|
switch (this.renderType) {
|
|
case CANVAS_RENDER_TYPE.offscreen:
|
|
createEmptyImageBitmap(this.$videoElement.width, this.$videoElement.height).then(imageBitMap => {
|
|
this.bitmaprenderer.transferFromImageBitmap(imageBitMap);
|
|
});
|
|
break;
|
|
|
|
case CANVAS_RENDER_TYPE.webgl:
|
|
this.contextGl.clear(this.contextGl.COLOR_BUFFER_BIT);
|
|
break;
|
|
|
|
case CANVAS_RENDER_TYPE.webcodecs:
|
|
this.context2D.clearRect(0, 0, this.$videoElement.width, this.$videoElement.height);
|
|
break;
|
|
}
|
|
}
|
|
|
|
resize() {
|
|
this.player.debug.log('canvasVideo', 'resize');
|
|
const option = this.player._opt;
|
|
let width = this.player.width;
|
|
let height = this.player.height;
|
|
|
|
if (option.hasControl && !option.controlAutoHide) {
|
|
if (isMobile() && this.player.fullscreen && option.useWebFullScreen) {
|
|
width -= CONTROL_HEIGHT;
|
|
} else {
|
|
height -= CONTROL_HEIGHT;
|
|
}
|
|
}
|
|
|
|
let resizeWidth = this.$videoElement.width;
|
|
let resizeHeight = this.$videoElement.height;
|
|
const rotate = option.rotate;
|
|
let left = (width - resizeWidth) / 2;
|
|
let top = (height - resizeHeight) / 2;
|
|
|
|
if (rotate === 270 || rotate === 90) {
|
|
resizeWidth = this.$videoElement.height;
|
|
resizeHeight = this.$videoElement.width;
|
|
}
|
|
|
|
const wScale = width / resizeWidth;
|
|
const hScale = height / resizeHeight;
|
|
let scale = wScale > hScale ? hScale : wScale; //
|
|
|
|
if (!option.isResize) {
|
|
if (wScale !== hScale) {
|
|
scale = wScale + ',' + hScale;
|
|
}
|
|
} //
|
|
|
|
|
|
if (option.isFullResize) {
|
|
scale = wScale > hScale ? wScale : hScale;
|
|
}
|
|
|
|
let transform = "scale(" + scale + ")";
|
|
|
|
if (rotate) {
|
|
transform += ' rotate(' + rotate + 'deg)';
|
|
}
|
|
|
|
this.$videoElement.style.transform = transform;
|
|
this.$videoElement.style.left = left + "px";
|
|
this.$videoElement.style.top = top + "px";
|
|
}
|
|
|
|
}
|
|
|
|
class VideoLoader extends CommonLoader$1 {
|
|
constructor(player) {
|
|
super();
|
|
this.player = player;
|
|
const $videoElement = document.createElement('video');
|
|
const $canvasElement = document.createElement('canvas');
|
|
$videoElement.muted = true;
|
|
$videoElement.style.position = "absolute";
|
|
$videoElement.style.top = 0;
|
|
$videoElement.style.left = 0;
|
|
this._delayPlay = false;
|
|
player.$container.appendChild($videoElement);
|
|
this.videoInfo = {
|
|
width: '',
|
|
height: '',
|
|
encType: ''
|
|
};
|
|
const _opt = this.player._opt;
|
|
|
|
if (_opt.useWCS && _opt.wcsUseVideoRender) {
|
|
this.trackGenerator = new MediaStreamTrackGenerator({
|
|
kind: 'video'
|
|
});
|
|
$videoElement.srcObject = new MediaStream([this.trackGenerator]);
|
|
this.vwriter = this.trackGenerator.writable.getWriter();
|
|
}
|
|
|
|
this.$videoElement = $videoElement;
|
|
this.$canvasElement = $canvasElement;
|
|
this.canvasContext = $canvasElement.getContext('2d');
|
|
this.fixChromeVideoFlashBug();
|
|
this.resize();
|
|
const {
|
|
proxy
|
|
} = this.player.events;
|
|
proxy(this.$videoElement, 'canplay', () => {
|
|
this.player.debug.log('Video', `canplay and _delayPlay is ${this._delayPlay}`);
|
|
|
|
if (this._delayPlay) {
|
|
this._play();
|
|
}
|
|
});
|
|
proxy(this.$videoElement, 'waiting', () => {
|
|
this.player.emit(EVENTS.videoWaiting);
|
|
});
|
|
proxy(this.$videoElement, 'timeupdate', event => {
|
|
// this.player.emit(EVENTS.videoTimeUpdate, event.timeStamp);
|
|
const timeStamp = parseInt(event.timeStamp, 10);
|
|
this.player.emit(EVENTS.timeUpdate, timeStamp); // check is pause;
|
|
|
|
if (!this.isPlaying() && this.init) {
|
|
this.player.debug.log('Video', `timeupdate and this.isPlaying is false and retry play`);
|
|
this.$videoElement.play();
|
|
}
|
|
});
|
|
this.player.debug.log('Video', 'init');
|
|
}
|
|
|
|
destroy() {
|
|
super.destroy();
|
|
this.$canvasElement = null;
|
|
this.canvasContext = null;
|
|
|
|
if (this.$videoElement) {
|
|
this.$videoElement.pause();
|
|
this.$videoElement.currentTime = 0;
|
|
this.$videoElement.src = '';
|
|
this.$videoElement.removeAttribute('src');
|
|
this.$videoElement = null;
|
|
}
|
|
|
|
if (this.trackGenerator) {
|
|
this.trackGenerator.stop();
|
|
this.trackGenerator = null;
|
|
}
|
|
|
|
if (this.vwriter) {
|
|
this.vwriter.close();
|
|
this.vwriter = null;
|
|
}
|
|
|
|
this.player.debug.log('Video', 'destroy');
|
|
}
|
|
|
|
fixChromeVideoFlashBug() {
|
|
const browser = getBrowser();
|
|
const type = browser.type.toLowerCase();
|
|
|
|
if (type === 'chrome' || type === 'edge') {
|
|
const $container = this.player.$container;
|
|
$container.style.backdropFilter = 'blur(0px)';
|
|
$container.style.translateZ = '0';
|
|
}
|
|
}
|
|
|
|
play() {
|
|
if (this.$videoElement) {
|
|
const readyState = this._getVideoReadyState();
|
|
|
|
this.player.debug.log('Video', `play and readyState: ${readyState}`);
|
|
|
|
if (readyState === 0) {
|
|
this.player.debug.warn('Video', 'readyState is 0 and set _delayPlay to true');
|
|
this._delayPlay = true;
|
|
return;
|
|
}
|
|
|
|
this._play();
|
|
}
|
|
}
|
|
|
|
_getVideoReadyState() {
|
|
let result = 0;
|
|
|
|
if (this.$videoElement) {
|
|
result = this.$videoElement.readyState;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
_play() {
|
|
this.$videoElement && this.$videoElement.play().then(() => {
|
|
this._delayPlay = false;
|
|
this.player.debug.log('Video', '_play success');
|
|
setTimeout(() => {
|
|
if (!this.isPlaying()) {
|
|
this.player.debug.warn('Video', `play failed and retry play`);
|
|
|
|
this._play();
|
|
}
|
|
}, 100);
|
|
}).catch(e => {
|
|
this.player.debug.error('Video', '_play error', e);
|
|
});
|
|
}
|
|
|
|
pause(isNow) {
|
|
// 预防
|
|
// https://developer.chrome.com/blog/play-request-was-interrupted/
|
|
// http://alonesuperman.com/?p=23
|
|
if (isNow) {
|
|
this.$videoElement && this.$videoElement.pause();
|
|
} else {
|
|
setTimeout(() => {
|
|
this.$videoElement && this.$videoElement.pause();
|
|
}, 100);
|
|
}
|
|
}
|
|
|
|
clearView() {}
|
|
|
|
screenshot(filename, format, quality, type) {
|
|
filename = filename || now();
|
|
type = type || SCREENSHOT_TYPE.download;
|
|
const formatType = {
|
|
png: 'image/png',
|
|
jpeg: 'image/jpeg',
|
|
webp: 'image/webp'
|
|
};
|
|
let encoderOptions = 0.92;
|
|
|
|
if (!formatType[format] && SCREENSHOT_TYPE[format]) {
|
|
type = format;
|
|
format = 'png';
|
|
quality = undefined;
|
|
}
|
|
|
|
if (typeof quality === "string") {
|
|
type = quality;
|
|
quality = undefined;
|
|
}
|
|
|
|
if (typeof quality !== 'undefined') {
|
|
encoderOptions = Number(quality);
|
|
}
|
|
|
|
const $video = this.$videoElement;
|
|
let canvas = this.$canvasElement;
|
|
canvas.width = $video.videoWidth;
|
|
canvas.height = $video.videoHeight;
|
|
this.canvasContext.drawImage($video, 0, 0, canvas.width, canvas.height);
|
|
const dataURL = canvas.toDataURL(formatType[format] || formatType.png, encoderOptions); // release memory
|
|
|
|
this.canvasContext.clearRect(0, 0, canvas.width, canvas.height);
|
|
canvas.width = 0;
|
|
canvas.height = 0;
|
|
|
|
if (type === SCREENSHOT_TYPE.base64) {
|
|
return dataURL;
|
|
} else {
|
|
const file = dataURLToFile(dataURL);
|
|
|
|
if (type === SCREENSHOT_TYPE.blob) {
|
|
return file;
|
|
} else if (type === SCREENSHOT_TYPE.download) {
|
|
// downloadImg(file, filename);
|
|
saveAs(file, filename);
|
|
}
|
|
}
|
|
}
|
|
|
|
initCanvasViewSize() {
|
|
this.resize();
|
|
} //
|
|
|
|
|
|
render(msg) {
|
|
if (this.vwriter) {
|
|
this.vwriter.write(msg.videoFrame);
|
|
}
|
|
}
|
|
|
|
resize() {
|
|
let width = this.player.width;
|
|
let height = this.player.height;
|
|
const option = this.player._opt;
|
|
const rotate = option.rotate;
|
|
|
|
if (option.hasControl && !option.controlAutoHide) {
|
|
if (isMobile() && this.player.fullscreen && option.useWebFullScreen) {
|
|
width -= CONTROL_HEIGHT;
|
|
} else {
|
|
height -= CONTROL_HEIGHT;
|
|
}
|
|
}
|
|
|
|
this.$videoElement.width = width;
|
|
this.$videoElement.height = height;
|
|
|
|
if (rotate === 270 || rotate === 90) {
|
|
this.$videoElement.width = height;
|
|
this.$videoElement.height = width;
|
|
}
|
|
|
|
let resizeWidth = this.$videoElement.width;
|
|
let resizeHeight = this.$videoElement.height;
|
|
let left = (width - resizeWidth) / 2;
|
|
let top = (height - resizeHeight) / 2;
|
|
let objectFill = 'contain'; // 默认是true
|
|
// 视频画面做等比缩放后,高或宽对齐canvas区域,画面不被拉伸,但有黑边
|
|
// 视频画面完全填充canvas区域,画面会被拉伸
|
|
|
|
if (!option.isResize) {
|
|
objectFill = 'fill';
|
|
} // 视频画面做等比缩放后,完全填充canvas区域,画面不被拉伸,没有黑边,但画面显示不全
|
|
|
|
|
|
if (option.isFullResize) {
|
|
objectFill = 'none';
|
|
}
|
|
|
|
this.$videoElement.style.objectFit = objectFill;
|
|
this.$videoElement.style.transform = 'rotate(' + rotate + 'deg)';
|
|
this.$videoElement.style.left = left + "px";
|
|
this.$videoElement.style.top = top + "px";
|
|
}
|
|
|
|
isPlaying() {
|
|
return this.$videoElement && !this.$videoElement.paused;
|
|
}
|
|
|
|
}
|
|
|
|
class Video {
|
|
constructor(player) {
|
|
const Loader = Video.getLoaderFactory(player._opt);
|
|
return new Loader(player);
|
|
}
|
|
|
|
static getLoaderFactory(opt) {
|
|
if (opt.useMSE || opt.useWCS && !opt.useOffscreen && opt.wcsUseVideoRender) {
|
|
return VideoLoader;
|
|
} else {
|
|
return CanvasVideoLoader;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
class AudioContextLoader extends Emitter {
|
|
constructor(player) {
|
|
super();
|
|
this.bufferList = [];
|
|
this.player = player;
|
|
this.scriptNode = null;
|
|
this.hasInitScriptNode = false;
|
|
this.audioContextChannel = null;
|
|
this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); //
|
|
|
|
this.gainNode = this.audioContext.createGain(); // Get an AudioBufferSourceNode.
|
|
// This is the AudioNode to use when we want to play an AudioBuffer
|
|
|
|
const source = this.audioContext.createBufferSource(); // set the buffer in the AudioBufferSourceNode
|
|
|
|
source.buffer = this.audioContext.createBuffer(1, 1, 22050); // connect the AudioBufferSourceNode to the
|
|
// destination so we can hear the sound
|
|
|
|
source.connect(this.audioContext.destination); // noteOn as start
|
|
// start the source playing
|
|
|
|
if (source.noteOn) {
|
|
source.noteOn(0);
|
|
} else {
|
|
source.start(0);
|
|
}
|
|
|
|
this.audioBufferSourceNode = source; //
|
|
|
|
this.mediaStreamAudioDestinationNode = this.audioContext.createMediaStreamDestination(); //
|
|
|
|
this.audioEnabled(true); // default setting 0
|
|
|
|
this.gainNode.gain.value = 0;
|
|
this.playing = false; //
|
|
|
|
this.audioSyncVideoOption = {
|
|
diff: null
|
|
};
|
|
this.audioInfo = {
|
|
encType: '',
|
|
channels: '',
|
|
sampleRate: ''
|
|
};
|
|
this.init = false;
|
|
this.hasAudio = false; // update
|
|
|
|
this.on(EVENTS.videoSyncAudio, options => {
|
|
// this.player.debug.log('AudioContext', `videoSyncAudio , audioTimestamp: ${options.audioTimestamp},videoTimestamp: ${options.videoTimestamp},diff:${options.diff}`)
|
|
this.audioSyncVideoOption = options;
|
|
});
|
|
this.player.debug.log('AudioContext', 'init');
|
|
}
|
|
|
|
resetInit() {
|
|
this.init = false;
|
|
this.audioInfo = {
|
|
encType: '',
|
|
channels: '',
|
|
sampleRate: ''
|
|
};
|
|
}
|
|
|
|
destroy() {
|
|
this.closeAudio();
|
|
this.resetInit();
|
|
this.audioContext.close();
|
|
this.audioContext = null;
|
|
this.gainNode = null;
|
|
this.hasAudio = false;
|
|
this.playing = false;
|
|
|
|
if (this.scriptNode) {
|
|
this.scriptNode.onaudioprocess = noop;
|
|
this.scriptNode = null;
|
|
}
|
|
|
|
this.audioBufferSourceNode = null;
|
|
this.mediaStreamAudioDestinationNode = null;
|
|
this.hasInitScriptNode = false;
|
|
this.audioSyncVideoOption = {
|
|
diff: null
|
|
};
|
|
this.off();
|
|
this.player.debug.log('AudioContext', 'destroy');
|
|
}
|
|
|
|
updateAudioInfo(data) {
|
|
if (data.encTypeCode) {
|
|
this.audioInfo.encType = AUDIO_ENC_TYPE[data.encTypeCode];
|
|
}
|
|
|
|
if (data.channels) {
|
|
this.audioInfo.channels = data.channels;
|
|
}
|
|
|
|
if (data.sampleRate) {
|
|
this.audioInfo.sampleRate = data.sampleRate;
|
|
} // audio 基本信息
|
|
|
|
|
|
if (this.audioInfo.sampleRate && this.audioInfo.channels && this.audioInfo.encType && !this.init) {
|
|
this.player.emit(EVENTS.audioInfo, this.audioInfo);
|
|
this.init = true;
|
|
}
|
|
} //
|
|
|
|
|
|
get isPlaying() {
|
|
return this.playing;
|
|
}
|
|
|
|
get isMute() {
|
|
return this.gainNode.gain.value === 0;
|
|
}
|
|
|
|
get volume() {
|
|
return this.gainNode.gain.value;
|
|
}
|
|
|
|
get bufferSize() {
|
|
return this.bufferList.length;
|
|
}
|
|
|
|
initScriptNode() {
|
|
this.playing = true;
|
|
|
|
if (this.hasInitScriptNode) {
|
|
return;
|
|
}
|
|
|
|
const channels = this.audioInfo.channels;
|
|
const scriptNode = this.audioContext.createScriptProcessor(1024, 0, channels); // tips: if audio isStateSuspended onaudioprocess method not working
|
|
|
|
scriptNode.onaudioprocess = audioProcessingEvent => {
|
|
const outputBuffer = audioProcessingEvent.outputBuffer;
|
|
|
|
if (this.bufferList.length && this.playing) {
|
|
// just for wasm
|
|
if (!this.player._opt.useWCS && !this.player._opt.useMSE && this.player._opt.wasmDecodeAudioSyncVideo) {
|
|
// audio > video
|
|
// wait
|
|
if (this.audioSyncVideoOption.diff > AUDIO_SYNC_VIDEO_DIFF) {
|
|
this.player.debug.warn('AudioContext', `audioSyncVideoOption more than diff :${this.audioSyncVideoOption.diff}, waiting`); // wait
|
|
|
|
return;
|
|
} // audio < video
|
|
// throw away then chase video
|
|
else if (this.audioSyncVideoOption.diff < -AUDIO_SYNC_VIDEO_DIFF) {
|
|
this.player.debug.warn('AudioContext', `audioSyncVideoOption less than diff :${this.audioSyncVideoOption.diff}, dropping`); //
|
|
|
|
let bufferItem = this.bufferList.shift(); //
|
|
|
|
while (bufferItem.ts - this.player.videoTimestamp < -AUDIO_SYNC_VIDEO_DIFF && this.bufferList.length > 0) {
|
|
// this.player.debug.warn('AudioContext', `audioSyncVideoOption less than inner ts is:${bufferItem.ts}, videoTimestamp is ${this.player.videoTimestamp},diff:${bufferItem.ts - this.player.videoTimestamp}`)
|
|
bufferItem = this.bufferList.shift();
|
|
}
|
|
|
|
if (this.bufferList.length === 0) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.bufferList.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const bufferItem = this.bufferList.shift(); // update audio time stamp
|
|
|
|
if (bufferItem && bufferItem.ts) {
|
|
this.player.audioTimestamp = bufferItem.ts;
|
|
}
|
|
|
|
for (let channel = 0; channel < channels; channel++) {
|
|
const b = bufferItem.buffer[channel];
|
|
const nowBuffering = outputBuffer.getChannelData(channel);
|
|
|
|
for (let i = 0; i < 1024; i++) {
|
|
nowBuffering[i] = b[i] || 0;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
scriptNode.connect(this.gainNode);
|
|
this.scriptNode = scriptNode;
|
|
this.gainNode.connect(this.audioContext.destination);
|
|
this.gainNode.connect(this.mediaStreamAudioDestinationNode);
|
|
this.hasInitScriptNode = true;
|
|
}
|
|
|
|
mute(flag) {
|
|
if (flag) {
|
|
if (!this.isMute) {
|
|
this.player.emit(EVENTS.mute, flag);
|
|
}
|
|
|
|
this.setVolume(0);
|
|
this.clear();
|
|
} else {
|
|
if (this.isMute) {
|
|
this.player.emit(EVENTS.mute, flag);
|
|
}
|
|
|
|
this.setVolume(0.5);
|
|
}
|
|
}
|
|
|
|
setVolume(volume) {
|
|
volume = parseFloat(volume).toFixed(2);
|
|
|
|
if (isNaN(volume)) {
|
|
return;
|
|
}
|
|
|
|
this.audioEnabled(true);
|
|
volume = clamp(volume, 0, 1);
|
|
this.gainNode.gain.value = volume;
|
|
this.gainNode.gain.setValueAtTime(volume, this.audioContext.currentTime);
|
|
this.player.emit(EVENTS.volumechange, this.player.volume);
|
|
}
|
|
|
|
closeAudio() {
|
|
if (this.hasInitScriptNode) {
|
|
this.scriptNode && this.scriptNode.disconnect(this.gainNode);
|
|
this.gainNode && this.gainNode.disconnect(this.audioContext.destination);
|
|
this.gainNode && this.gainNode.disconnect(this.mediaStreamAudioDestinationNode);
|
|
}
|
|
|
|
this.clear();
|
|
} // 是否播放。。。
|
|
|
|
|
|
audioEnabled(flag) {
|
|
if (flag) {
|
|
if (this.audioContext.state === 'suspended') {
|
|
// resume
|
|
this.audioContext.resume();
|
|
}
|
|
} else {
|
|
if (this.audioContext.state === 'running') {
|
|
// suspend
|
|
this.audioContext.suspend();
|
|
}
|
|
}
|
|
}
|
|
|
|
isStateRunning() {
|
|
return this.audioContext.state === 'running';
|
|
}
|
|
|
|
isStateSuspended() {
|
|
return this.audioContext.state === 'suspended';
|
|
}
|
|
|
|
clear() {
|
|
this.bufferList = [];
|
|
}
|
|
|
|
play(buffer, ts) {
|
|
// if is mute
|
|
if (this.isMute) {
|
|
return;
|
|
}
|
|
|
|
this.hasAudio = true;
|
|
this.bufferList.push({
|
|
buffer,
|
|
ts
|
|
});
|
|
|
|
if (this.bufferList.length > 20) {
|
|
this.player.debug.warn('AudioContext', `bufferList is large: ${this.bufferList.length}`); // out of memory
|
|
|
|
if (this.bufferList.length > 50) {
|
|
this.bufferList.shift();
|
|
}
|
|
} // this.player.debug.log('AudioContext', `bufferList is ${this.bufferList.length}`)
|
|
|
|
}
|
|
|
|
pause() {
|
|
this.audioSyncVideoOption = {
|
|
diff: null
|
|
};
|
|
this.playing = false;
|
|
this.clear();
|
|
}
|
|
|
|
resume() {
|
|
this.playing = true;
|
|
}
|
|
|
|
}
|
|
|
|
class Audio {
|
|
constructor(player) {
|
|
const Loader = Audio.getLoaderFactory();
|
|
return new Loader(player);
|
|
}
|
|
|
|
static getLoaderFactory() {
|
|
return AudioContextLoader;
|
|
}
|
|
|
|
}
|
|
|
|
class FetchLoader extends Emitter {
|
|
constructor(player) {
|
|
super();
|
|
this.player = player;
|
|
this.playing = false;
|
|
this.abortController = new AbortController(); //
|
|
|
|
this.streamRate = calculationRate(rate => {
|
|
player.emit(EVENTS.kBps, (rate / 1024).toFixed(2));
|
|
});
|
|
player.debug.log('FetchStream', 'init');
|
|
}
|
|
|
|
destroy() {
|
|
this.abort();
|
|
this.off();
|
|
this.streamRate = null;
|
|
this.player.debug.log('FetchStream', 'destroy');
|
|
}
|
|
/**
|
|
*
|
|
* @param url
|
|
* @param options
|
|
*/
|
|
|
|
|
|
fetchStream(url) {
|
|
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
const {
|
|
demux
|
|
} = this.player;
|
|
this.player._times.streamStart = now();
|
|
const fetchOptions = Object.assign({
|
|
signal: this.abortController.signal
|
|
}, {
|
|
headers: options.headers || {}
|
|
});
|
|
fetch(url, fetchOptions).then(res => {
|
|
const reader = res.body.getReader();
|
|
this.emit(EVENTS.streamSuccess);
|
|
|
|
const fetchNext = () => {
|
|
reader.read().then(_ref => {
|
|
let {
|
|
done,
|
|
value
|
|
} = _ref;
|
|
|
|
if (done) {
|
|
demux.close();
|
|
} else {
|
|
this.streamRate && this.streamRate(value.byteLength);
|
|
demux.dispatch(value);
|
|
fetchNext();
|
|
}
|
|
}).catch(e => {
|
|
demux.close();
|
|
const errorString = e.toString(); // aborted a request 。
|
|
|
|
if (errorString.indexOf(FETCH_ERROR.abortError1) !== -1) {
|
|
return;
|
|
}
|
|
|
|
if (errorString.indexOf(FETCH_ERROR.abortError2) !== -1) {
|
|
return;
|
|
}
|
|
|
|
if (e.name === FETCH_ERROR.abort) {
|
|
return;
|
|
}
|
|
|
|
this.abort();
|
|
this.emit(EVENTS_ERROR.fetchError, e);
|
|
this.player.emit(EVENTS.error, EVENTS_ERROR.fetchError);
|
|
});
|
|
};
|
|
|
|
fetchNext();
|
|
}).catch(e => {
|
|
if (e.name === 'AbortError') {
|
|
return;
|
|
}
|
|
|
|
demux.close();
|
|
this.abort();
|
|
this.emit(EVENTS_ERROR.fetchError, e);
|
|
this.player.emit(EVENTS.error, EVENTS_ERROR.fetchError);
|
|
});
|
|
}
|
|
|
|
abort() {
|
|
if (this.abortController) {
|
|
this.abortController.abort();
|
|
this.abortController = null;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
class WebsocketLoader extends Emitter {
|
|
constructor(player) {
|
|
super();
|
|
this.player = player;
|
|
this.socket = null;
|
|
this.socketStatus = WEBSOCKET_STATUS.notConnect;
|
|
this.wsUrl = null; //
|
|
|
|
this.streamRate = calculationRate(rate => {
|
|
player.emit(EVENTS.kBps, (rate / 1024).toFixed(2));
|
|
});
|
|
player.debug.log('WebsocketLoader', 'init');
|
|
}
|
|
|
|
destroy() {
|
|
if (this.socket) {
|
|
this.socket.close();
|
|
this.socket = null;
|
|
}
|
|
|
|
this.socketStatus = WEBSOCKET_STATUS.notConnect;
|
|
this.streamRate = null;
|
|
this.wsUrl = null;
|
|
this.off();
|
|
this.player.debug.log('websocketLoader', 'destroy');
|
|
}
|
|
|
|
_createWebSocket() {
|
|
const player = this.player;
|
|
const {
|
|
debug,
|
|
events: {
|
|
proxy
|
|
},
|
|
demux
|
|
} = player;
|
|
this.socket = new WebSocket(this.wsUrl);
|
|
this.socket.binaryType = 'arraybuffer';
|
|
proxy(this.socket, 'open', () => {
|
|
this.emit(EVENTS.streamSuccess);
|
|
debug.log('websocketLoader', 'socket open');
|
|
this.socketStatus = WEBSOCKET_STATUS.open;
|
|
});
|
|
proxy(this.socket, 'message', event => {
|
|
this.streamRate && this.streamRate(event.data.byteLength);
|
|
|
|
this._handleMessage(event.data);
|
|
});
|
|
proxy(this.socket, 'close', () => {
|
|
debug.log('websocketLoader', 'socket close');
|
|
this.emit(EVENTS.streamEnd);
|
|
this.socketStatus = WEBSOCKET_STATUS.close;
|
|
});
|
|
proxy(this.socket, 'error', error => {
|
|
debug.log('websocketLoader', 'socket error');
|
|
this.emit(EVENTS_ERROR.websocketError, error);
|
|
this.player.emit(EVENTS.error, EVENTS_ERROR.websocketError);
|
|
this.socketStatus = WEBSOCKET_STATUS.error;
|
|
demux.close();
|
|
debug.log('websocketLoader', `socket error:`, error);
|
|
});
|
|
} //
|
|
|
|
|
|
_handleMessage(message) {
|
|
const {
|
|
demux
|
|
} = this.player;
|
|
|
|
if (!demux) {
|
|
this.player.debug.warn('websocketLoader', 'websocket handle message demux is null');
|
|
return;
|
|
}
|
|
|
|
demux.dispatch(message);
|
|
}
|
|
/**
|
|
*
|
|
* @param url
|
|
* @param options
|
|
*/
|
|
|
|
|
|
fetchStream(url, options) {
|
|
this.player._times.streamStart = now();
|
|
this.wsUrl = url;
|
|
|
|
this._createWebSocket();
|
|
}
|
|
|
|
}
|
|
|
|
class Stream {
|
|
constructor(player) {
|
|
const Loader = Stream.getLoaderFactory(player._opt.protocol);
|
|
return new Loader(player);
|
|
}
|
|
|
|
static getLoaderFactory(protocol) {
|
|
if (protocol === PLAYER_PLAY_PROTOCOL.fetch) {
|
|
return FetchLoader;
|
|
} else if (protocol === PLAYER_PLAY_PROTOCOL.websocket) {
|
|
return WebsocketLoader;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
var RecordRTC_1 = createCommonjsModule(function (module) {
|
|
|
|
// Last time updated: 2021-03-09 3:20:22 AM UTC
|
|
|
|
// ________________
|
|
// RecordRTC v5.6.2
|
|
|
|
// Open-Sourced: https://github.com/muaz-khan/RecordRTC
|
|
|
|
// --------------------------------------------------
|
|
// Muaz Khan - www.MuazKhan.com
|
|
// MIT License - www.WebRTC-Experiment.com/licence
|
|
// --------------------------------------------------
|
|
|
|
// ____________
|
|
// RecordRTC.js
|
|
|
|
/**
|
|
* {@link https://github.com/muaz-khan/RecordRTC|RecordRTC} is a WebRTC JavaScript library for audio/video as well as screen activity recording. It supports Chrome, Firefox, Opera, Android, and Microsoft Edge. Platforms: Linux, Mac and Windows.
|
|
* @summary Record audio, video or screen inside the browser.
|
|
* @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
|
|
* @author {@link https://MuazKhan.com|Muaz Khan}
|
|
* @typedef RecordRTC
|
|
* @class
|
|
* @example
|
|
* var recorder = RecordRTC(mediaStream or [arrayOfMediaStream], {
|
|
* type: 'video', // audio or video or gif or canvas
|
|
* recorderType: MediaStreamRecorder || CanvasRecorder || StereoAudioRecorder || Etc
|
|
* });
|
|
* recorder.startRecording();
|
|
* @see For further information:
|
|
* @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
|
|
* @param {MediaStream} mediaStream - Single media-stream object, array of media-streams, html-canvas-element, etc.
|
|
* @param {object} config - {type:"video", recorderType: MediaStreamRecorder, disableLogs: true, numberOfAudioChannels: 1, bufferSize: 0, sampleRate: 0, desiredSampRate: 16000, video: HTMLVideoElement, etc.}
|
|
*/
|
|
|
|
function RecordRTC(mediaStream, config) {
|
|
if (!mediaStream) {
|
|
throw 'First parameter is required.';
|
|
}
|
|
|
|
config = config || {
|
|
type: 'video'
|
|
};
|
|
|
|
config = new RecordRTCConfiguration(mediaStream, config);
|
|
|
|
// a reference to user's recordRTC object
|
|
var self = this;
|
|
|
|
function startRecording(config2) {
|
|
if (!config.disableLogs) {
|
|
console.log('RecordRTC version: ', self.version);
|
|
}
|
|
|
|
if (!!config2) {
|
|
// allow users to set options using startRecording method
|
|
// config2 is similar to main "config" object (second parameter over RecordRTC constructor)
|
|
config = new RecordRTCConfiguration(mediaStream, config2);
|
|
}
|
|
|
|
if (!config.disableLogs) {
|
|
console.log('started recording ' + config.type + ' stream.');
|
|
}
|
|
|
|
if (mediaRecorder) {
|
|
mediaRecorder.clearRecordedData();
|
|
mediaRecorder.record();
|
|
|
|
setState('recording');
|
|
|
|
if (self.recordingDuration) {
|
|
handleRecordingDuration();
|
|
}
|
|
return self;
|
|
}
|
|
|
|
initRecorder(function() {
|
|
if (self.recordingDuration) {
|
|
handleRecordingDuration();
|
|
}
|
|
});
|
|
|
|
return self;
|
|
}
|
|
|
|
function initRecorder(initCallback) {
|
|
if (initCallback) {
|
|
config.initCallback = function() {
|
|
initCallback();
|
|
initCallback = config.initCallback = null; // recorder.initRecorder should be call-backed once.
|
|
};
|
|
}
|
|
|
|
var Recorder = new GetRecorderType(mediaStream, config);
|
|
|
|
mediaRecorder = new Recorder(mediaStream, config);
|
|
mediaRecorder.record();
|
|
|
|
setState('recording');
|
|
|
|
if (!config.disableLogs) {
|
|
console.log('Initialized recorderType:', mediaRecorder.constructor.name, 'for output-type:', config.type);
|
|
}
|
|
}
|
|
|
|
function stopRecording(callback) {
|
|
callback = callback || function() {};
|
|
|
|
if (!mediaRecorder) {
|
|
warningLog();
|
|
return;
|
|
}
|
|
|
|
if (self.state === 'paused') {
|
|
self.resumeRecording();
|
|
|
|
setTimeout(function() {
|
|
stopRecording(callback);
|
|
}, 1);
|
|
return;
|
|
}
|
|
|
|
if (self.state !== 'recording' && !config.disableLogs) {
|
|
console.warn('Recording state should be: "recording", however current state is: ', self.state);
|
|
}
|
|
|
|
if (!config.disableLogs) {
|
|
console.log('Stopped recording ' + config.type + ' stream.');
|
|
}
|
|
|
|
if (config.type !== 'gif') {
|
|
mediaRecorder.stop(_callback);
|
|
} else {
|
|
mediaRecorder.stop();
|
|
_callback();
|
|
}
|
|
|
|
setState('stopped');
|
|
|
|
function _callback(__blob) {
|
|
if (!mediaRecorder) {
|
|
if (typeof callback.call === 'function') {
|
|
callback.call(self, '');
|
|
} else {
|
|
callback('');
|
|
}
|
|
return;
|
|
}
|
|
|
|
Object.keys(mediaRecorder).forEach(function(key) {
|
|
if (typeof mediaRecorder[key] === 'function') {
|
|
return;
|
|
}
|
|
|
|
self[key] = mediaRecorder[key];
|
|
});
|
|
|
|
var blob = mediaRecorder.blob;
|
|
|
|
if (!blob) {
|
|
if (__blob) {
|
|
mediaRecorder.blob = blob = __blob;
|
|
} else {
|
|
throw 'Recording failed.';
|
|
}
|
|
}
|
|
|
|
if (blob && !config.disableLogs) {
|
|
console.log(blob.type, '->', bytesToSize(blob.size));
|
|
}
|
|
|
|
if (callback) {
|
|
var url;
|
|
|
|
try {
|
|
url = URL.createObjectURL(blob);
|
|
} catch (e) {}
|
|
|
|
if (typeof callback.call === 'function') {
|
|
callback.call(self, url);
|
|
} else {
|
|
callback(url);
|
|
}
|
|
}
|
|
|
|
if (!config.autoWriteToDisk) {
|
|
return;
|
|
}
|
|
|
|
getDataURL(function(dataURL) {
|
|
var parameter = {};
|
|
parameter[config.type + 'Blob'] = dataURL;
|
|
DiskStorage.Store(parameter);
|
|
});
|
|
}
|
|
}
|
|
|
|
function pauseRecording() {
|
|
if (!mediaRecorder) {
|
|
warningLog();
|
|
return;
|
|
}
|
|
|
|
if (self.state !== 'recording') {
|
|
if (!config.disableLogs) {
|
|
console.warn('Unable to pause the recording. Recording state: ', self.state);
|
|
}
|
|
return;
|
|
}
|
|
|
|
setState('paused');
|
|
|
|
mediaRecorder.pause();
|
|
|
|
if (!config.disableLogs) {
|
|
console.log('Paused recording.');
|
|
}
|
|
}
|
|
|
|
function resumeRecording() {
|
|
if (!mediaRecorder) {
|
|
warningLog();
|
|
return;
|
|
}
|
|
|
|
if (self.state !== 'paused') {
|
|
if (!config.disableLogs) {
|
|
console.warn('Unable to resume the recording. Recording state: ', self.state);
|
|
}
|
|
return;
|
|
}
|
|
|
|
setState('recording');
|
|
|
|
// not all libs have this method yet
|
|
mediaRecorder.resume();
|
|
|
|
if (!config.disableLogs) {
|
|
console.log('Resumed recording.');
|
|
}
|
|
}
|
|
|
|
function readFile(_blob) {
|
|
postMessage(new FileReaderSync().readAsDataURL(_blob));
|
|
}
|
|
|
|
function getDataURL(callback, _mediaRecorder) {
|
|
if (!callback) {
|
|
throw 'Pass a callback function over getDataURL.';
|
|
}
|
|
|
|
var blob = _mediaRecorder ? _mediaRecorder.blob : (mediaRecorder || {}).blob;
|
|
|
|
if (!blob) {
|
|
if (!config.disableLogs) {
|
|
console.warn('Blob encoder did not finish its job yet.');
|
|
}
|
|
|
|
setTimeout(function() {
|
|
getDataURL(callback, _mediaRecorder);
|
|
}, 1000);
|
|
return;
|
|
}
|
|
|
|
if (typeof Worker !== 'undefined' && !navigator.mozGetUserMedia) {
|
|
var webWorker = processInWebWorker(readFile);
|
|
|
|
webWorker.onmessage = function(event) {
|
|
callback(event.data);
|
|
};
|
|
|
|
webWorker.postMessage(blob);
|
|
} else {
|
|
var reader = new FileReader();
|
|
reader.readAsDataURL(blob);
|
|
reader.onload = function(event) {
|
|
callback(event.target.result);
|
|
};
|
|
}
|
|
|
|
function processInWebWorker(_function) {
|
|
try {
|
|
var blob = URL.createObjectURL(new Blob([_function.toString(),
|
|
'this.onmessage = function (eee) {' + _function.name + '(eee.data);}'
|
|
], {
|
|
type: 'application/javascript'
|
|
}));
|
|
|
|
var worker = new Worker(blob);
|
|
URL.revokeObjectURL(blob);
|
|
return worker;
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
|
|
function handleRecordingDuration(counter) {
|
|
counter = counter || 0;
|
|
|
|
if (self.state === 'paused') {
|
|
setTimeout(function() {
|
|
handleRecordingDuration(counter);
|
|
}, 1000);
|
|
return;
|
|
}
|
|
|
|
if (self.state === 'stopped') {
|
|
return;
|
|
}
|
|
|
|
if (counter >= self.recordingDuration) {
|
|
stopRecording(self.onRecordingStopped);
|
|
return;
|
|
}
|
|
|
|
counter += 1000; // 1-second
|
|
|
|
setTimeout(function() {
|
|
handleRecordingDuration(counter);
|
|
}, 1000);
|
|
}
|
|
|
|
function setState(state) {
|
|
if (!self) {
|
|
return;
|
|
}
|
|
|
|
self.state = state;
|
|
|
|
if (typeof self.onStateChanged.call === 'function') {
|
|
self.onStateChanged.call(self, state);
|
|
} else {
|
|
self.onStateChanged(state);
|
|
}
|
|
}
|
|
|
|
var WARNING = 'It seems that recorder is destroyed or "startRecording" is not invoked for ' + config.type + ' recorder.';
|
|
|
|
function warningLog() {
|
|
if (config.disableLogs === true) {
|
|
return;
|
|
}
|
|
|
|
console.warn(WARNING);
|
|
}
|
|
|
|
var mediaRecorder;
|
|
|
|
var returnObject = {
|
|
/**
|
|
* This method starts the recording.
|
|
* @method
|
|
* @memberof RecordRTC
|
|
* @instance
|
|
* @example
|
|
* var recorder = RecordRTC(mediaStream, {
|
|
* type: 'video'
|
|
* });
|
|
* recorder.startRecording();
|
|
*/
|
|
startRecording: startRecording,
|
|
|
|
/**
|
|
* This method stops the recording. It is strongly recommended to get "blob" or "URI" inside the callback to make sure all recorders finished their job.
|
|
* @param {function} callback - Callback to get the recorded blob.
|
|
* @method
|
|
* @memberof RecordRTC
|
|
* @instance
|
|
* @example
|
|
* recorder.stopRecording(function() {
|
|
* // use either "this" or "recorder" object; both are identical
|
|
* video.src = this.toURL();
|
|
* var blob = this.getBlob();
|
|
* });
|
|
*/
|
|
stopRecording: stopRecording,
|
|
|
|
/**
|
|
* This method pauses the recording. You can resume recording using "resumeRecording" method.
|
|
* @method
|
|
* @memberof RecordRTC
|
|
* @instance
|
|
* @todo Firefox is unable to pause the recording. Fix it.
|
|
* @example
|
|
* recorder.pauseRecording(); // pause the recording
|
|
* recorder.resumeRecording(); // resume again
|
|
*/
|
|
pauseRecording: pauseRecording,
|
|
|
|
/**
|
|
* This method resumes the recording.
|
|
* @method
|
|
* @memberof RecordRTC
|
|
* @instance
|
|
* @example
|
|
* recorder.pauseRecording(); // first of all, pause the recording
|
|
* recorder.resumeRecording(); // now resume it
|
|
*/
|
|
resumeRecording: resumeRecording,
|
|
|
|
/**
|
|
* This method initializes the recording.
|
|
* @method
|
|
* @memberof RecordRTC
|
|
* @instance
|
|
* @todo This method should be deprecated.
|
|
* @example
|
|
* recorder.initRecorder();
|
|
*/
|
|
initRecorder: initRecorder,
|
|
|
|
/**
|
|
* Ask RecordRTC to auto-stop the recording after 5 minutes.
|
|
* @method
|
|
* @memberof RecordRTC
|
|
* @instance
|
|
* @example
|
|
* var fiveMinutes = 5 * 1000 * 60;
|
|
* recorder.setRecordingDuration(fiveMinutes, function() {
|
|
* var blob = this.getBlob();
|
|
* video.src = this.toURL();
|
|
* });
|
|
*
|
|
* // or otherwise
|
|
* recorder.setRecordingDuration(fiveMinutes).onRecordingStopped(function() {
|
|
* var blob = this.getBlob();
|
|
* video.src = this.toURL();
|
|
* });
|
|
*/
|
|
setRecordingDuration: function(recordingDuration, callback) {
|
|
if (typeof recordingDuration === 'undefined') {
|
|
throw 'recordingDuration is required.';
|
|
}
|
|
|
|
if (typeof recordingDuration !== 'number') {
|
|
throw 'recordingDuration must be a number.';
|
|
}
|
|
|
|
self.recordingDuration = recordingDuration;
|
|
self.onRecordingStopped = callback || function() {};
|
|
|
|
return {
|
|
onRecordingStopped: function(callback) {
|
|
self.onRecordingStopped = callback;
|
|
}
|
|
};
|
|
},
|
|
|
|
/**
|
|
* This method can be used to clear/reset all the recorded data.
|
|
* @method
|
|
* @memberof RecordRTC
|
|
* @instance
|
|
* @todo Figure out the difference between "reset" and "clearRecordedData" methods.
|
|
* @example
|
|
* recorder.clearRecordedData();
|
|
*/
|
|
clearRecordedData: function() {
|
|
if (!mediaRecorder) {
|
|
warningLog();
|
|
return;
|
|
}
|
|
|
|
mediaRecorder.clearRecordedData();
|
|
|
|
if (!config.disableLogs) {
|
|
console.log('Cleared old recorded data.');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get the recorded blob. Use this method inside the "stopRecording" callback.
|
|
* @method
|
|
* @memberof RecordRTC
|
|
* @instance
|
|
* @example
|
|
* recorder.stopRecording(function() {
|
|
* var blob = this.getBlob();
|
|
*
|
|
* var file = new File([blob], 'filename.webm', {
|
|
* type: 'video/webm'
|
|
* });
|
|
*
|
|
* var formData = new FormData();
|
|
* formData.append('file', file); // upload "File" object rather than a "Blob"
|
|
* uploadToServer(formData);
|
|
* });
|
|
* @returns {Blob} Returns recorded data as "Blob" object.
|
|
*/
|
|
getBlob: function() {
|
|
if (!mediaRecorder) {
|
|
warningLog();
|
|
return;
|
|
}
|
|
|
|
return mediaRecorder.blob;
|
|
},
|
|
|
|
/**
|
|
* Get data-URI instead of Blob.
|
|
* @param {function} callback - Callback to get the Data-URI.
|
|
* @method
|
|
* @memberof RecordRTC
|
|
* @instance
|
|
* @example
|
|
* recorder.stopRecording(function() {
|
|
* recorder.getDataURL(function(dataURI) {
|
|
* video.src = dataURI;
|
|
* });
|
|
* });
|
|
*/
|
|
getDataURL: getDataURL,
|
|
|
|
/**
|
|
* Get virtual/temporary URL. Usage of this URL is limited to current tab.
|
|
* @method
|
|
* @memberof RecordRTC
|
|
* @instance
|
|
* @example
|
|
* recorder.stopRecording(function() {
|
|
* video.src = this.toURL();
|
|
* });
|
|
* @returns {String} Returns a virtual/temporary URL for the recorded "Blob".
|
|
*/
|
|
toURL: function() {
|
|
if (!mediaRecorder) {
|
|
warningLog();
|
|
return;
|
|
}
|
|
|
|
return URL.createObjectURL(mediaRecorder.blob);
|
|
},
|
|
|
|
/**
|
|
* Get internal recording object (i.e. internal module) e.g. MutliStreamRecorder, MediaStreamRecorder, StereoAudioRecorder or WhammyRecorder etc.
|
|
* @method
|
|
* @memberof RecordRTC
|
|
* @instance
|
|
* @example
|
|
* var internalRecorder = recorder.getInternalRecorder();
|
|
* if(internalRecorder instanceof MultiStreamRecorder) {
|
|
* internalRecorder.addStreams([newAudioStream]);
|
|
* internalRecorder.resetVideoStreams([screenStream]);
|
|
* }
|
|
* @returns {Object} Returns internal recording object.
|
|
*/
|
|
getInternalRecorder: function() {
|
|
return mediaRecorder;
|
|
},
|
|
|
|
/**
|
|
* Invoke save-as dialog to save the recorded blob into your disk.
|
|
* @param {string} fileName - Set your own file name.
|
|
* @method
|
|
* @memberof RecordRTC
|
|
* @instance
|
|
* @example
|
|
* recorder.stopRecording(function() {
|
|
* this.save('file-name');
|
|
*
|
|
* // or manually:
|
|
* invokeSaveAsDialog(this.getBlob(), 'filename.webm');
|
|
* });
|
|
*/
|
|
save: function(fileName) {
|
|
if (!mediaRecorder) {
|
|
warningLog();
|
|
return;
|
|
}
|
|
|
|
invokeSaveAsDialog(mediaRecorder.blob, fileName);
|
|
},
|
|
|
|
/**
|
|
* This method gets a blob from indexed-DB storage.
|
|
* @param {function} callback - Callback to get the recorded blob.
|
|
* @method
|
|
* @memberof RecordRTC
|
|
* @instance
|
|
* @example
|
|
* recorder.getFromDisk(function(dataURL) {
|
|
* video.src = dataURL;
|
|
* });
|
|
*/
|
|
getFromDisk: function(callback) {
|
|
if (!mediaRecorder) {
|
|
warningLog();
|
|
return;
|
|
}
|
|
|
|
RecordRTC.getFromDisk(config.type, callback);
|
|
},
|
|
|
|
/**
|
|
* This method appends an array of webp images to the recorded video-blob. It takes an "array" object.
|
|
* @type {Array.<Array>}
|
|
* @param {Array} arrayOfWebPImages - Array of webp images.
|
|
* @method
|
|
* @memberof RecordRTC
|
|
* @instance
|
|
* @todo This method should be deprecated.
|
|
* @example
|
|
* var arrayOfWebPImages = [];
|
|
* arrayOfWebPImages.push({
|
|
* duration: index,
|
|
* image: 'data:image/webp;base64,...'
|
|
* });
|
|
* recorder.setAdvertisementArray(arrayOfWebPImages);
|
|
*/
|
|
setAdvertisementArray: function(arrayOfWebPImages) {
|
|
config.advertisement = [];
|
|
|
|
var length = arrayOfWebPImages.length;
|
|
for (var i = 0; i < length; i++) {
|
|
config.advertisement.push({
|
|
duration: i,
|
|
image: arrayOfWebPImages[i]
|
|
});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* It is equivalent to <code class="str">"recorder.getBlob()"</code> method. Usage of "getBlob" is recommended, though.
|
|
* @property {Blob} blob - Recorded Blob can be accessed using this property.
|
|
* @memberof RecordRTC
|
|
* @instance
|
|
* @readonly
|
|
* @example
|
|
* recorder.stopRecording(function() {
|
|
* var blob = this.blob;
|
|
*
|
|
* // below one is recommended
|
|
* var blob = this.getBlob();
|
|
* });
|
|
*/
|
|
blob: null,
|
|
|
|
/**
|
|
* This works only with {recorderType:StereoAudioRecorder}. Use this property on "stopRecording" to verify the encoder's sample-rates.
|
|
* @property {number} bufferSize - Buffer-size used to encode the WAV container
|
|
* @memberof RecordRTC
|
|
* @instance
|
|
* @readonly
|
|
* @example
|
|
* recorder.stopRecording(function() {
|
|
* alert('Recorder used this buffer-size: ' + this.bufferSize);
|
|
* });
|
|
*/
|
|
bufferSize: 0,
|
|
|
|
/**
|
|
* This works only with {recorderType:StereoAudioRecorder}. Use this property on "stopRecording" to verify the encoder's sample-rates.
|
|
* @property {number} sampleRate - Sample-rates used to encode the WAV container
|
|
* @memberof RecordRTC
|
|
* @instance
|
|
* @readonly
|
|
* @example
|
|
* recorder.stopRecording(function() {
|
|
* alert('Recorder used these sample-rates: ' + this.sampleRate);
|
|
* });
|
|
*/
|
|
sampleRate: 0,
|
|
|
|
/**
|
|
* {recorderType:StereoAudioRecorder} returns ArrayBuffer object.
|
|
* @property {ArrayBuffer} buffer - Audio ArrayBuffer, supported only in Chrome.
|
|
* @memberof RecordRTC
|
|
* @instance
|
|
* @readonly
|
|
* @example
|
|
* recorder.stopRecording(function() {
|
|
* var arrayBuffer = this.buffer;
|
|
* alert(arrayBuffer.byteLength);
|
|
* });
|
|
*/
|
|
buffer: null,
|
|
|
|
/**
|
|
* This method resets the recorder. So that you can reuse single recorder instance many times.
|
|
* @method
|
|
* @memberof RecordRTC
|
|
* @instance
|
|
* @example
|
|
* recorder.reset();
|
|
* recorder.startRecording();
|
|
*/
|
|
reset: function() {
|
|
if (self.state === 'recording' && !config.disableLogs) {
|
|
console.warn('Stop an active recorder.');
|
|
}
|
|
|
|
if (mediaRecorder && typeof mediaRecorder.clearRecordedData === 'function') {
|
|
mediaRecorder.clearRecordedData();
|
|
}
|
|
mediaRecorder = null;
|
|
setState('inactive');
|
|
self.blob = null;
|
|
},
|
|
|
|
/**
|
|
* This method is called whenever recorder's state changes. Use this as an "event".
|
|
* @property {String} state - A recorder's state can be: recording, paused, stopped or inactive.
|
|
* @method
|
|
* @memberof RecordRTC
|
|
* @instance
|
|
* @example
|
|
* recorder.onStateChanged = function(state) {
|
|
* console.log('Recorder state: ', state);
|
|
* };
|
|
*/
|
|
onStateChanged: function(state) {
|
|
if (!config.disableLogs) {
|
|
console.log('Recorder state changed:', state);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* A recorder can have inactive, recording, paused or stopped states.
|
|
* @property {String} state - A recorder's state can be: recording, paused, stopped or inactive.
|
|
* @memberof RecordRTC
|
|
* @static
|
|
* @readonly
|
|
* @example
|
|
* // this looper function will keep you updated about the recorder's states.
|
|
* (function looper() {
|
|
* document.querySelector('h1').innerHTML = 'Recorder\'s state is: ' + recorder.state;
|
|
* if(recorder.state === 'stopped') return; // ignore+stop
|
|
* setTimeout(looper, 1000); // update after every 3-seconds
|
|
* })();
|
|
* recorder.startRecording();
|
|
*/
|
|
state: 'inactive',
|
|
|
|
/**
|
|
* Get recorder's readonly state.
|
|
* @method
|
|
* @memberof RecordRTC
|
|
* @example
|
|
* var state = recorder.getState();
|
|
* @returns {String} Returns recording state.
|
|
*/
|
|
getState: function() {
|
|
return self.state;
|
|
},
|
|
|
|
/**
|
|
* Destroy RecordRTC instance. Clear all recorders and objects.
|
|
* @method
|
|
* @memberof RecordRTC
|
|
* @example
|
|
* recorder.destroy();
|
|
*/
|
|
destroy: function() {
|
|
var disableLogsCache = config.disableLogs;
|
|
|
|
config = {
|
|
disableLogs: true
|
|
};
|
|
self.reset();
|
|
setState('destroyed');
|
|
returnObject = self = null;
|
|
|
|
if (Storage.AudioContextConstructor) {
|
|
Storage.AudioContextConstructor.close();
|
|
Storage.AudioContextConstructor = null;
|
|
}
|
|
|
|
config.disableLogs = disableLogsCache;
|
|
|
|
if (!config.disableLogs) {
|
|
console.log('RecordRTC is destroyed.');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* RecordRTC version number
|
|
* @property {String} version - Release version number.
|
|
* @memberof RecordRTC
|
|
* @static
|
|
* @readonly
|
|
* @example
|
|
* alert(recorder.version);
|
|
*/
|
|
version: '5.6.2'
|
|
};
|
|
|
|
if (!this) {
|
|
self = returnObject;
|
|
return returnObject;
|
|
}
|
|
|
|
// if someone wants to use RecordRTC with the "new" keyword.
|
|
for (var prop in returnObject) {
|
|
this[prop] = returnObject[prop];
|
|
}
|
|
|
|
self = this;
|
|
|
|
return returnObject;
|
|
}
|
|
|
|
RecordRTC.version = '5.6.2';
|
|
|
|
{
|
|
module.exports = RecordRTC;
|
|
}
|
|
|
|
RecordRTC.getFromDisk = function(type, callback) {
|
|
if (!callback) {
|
|
throw 'callback is mandatory.';
|
|
}
|
|
|
|
console.log('Getting recorded ' + (type === 'all' ? 'blobs' : type + ' blob ') + ' from disk!');
|
|
DiskStorage.Fetch(function(dataURL, _type) {
|
|
if (type !== 'all' && _type === type + 'Blob' && callback) {
|
|
callback(dataURL);
|
|
}
|
|
|
|
if (type === 'all' && callback) {
|
|
callback(dataURL, _type.replace('Blob', ''));
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* This method can be used to store recorded blobs into IndexedDB storage.
|
|
* @param {object} options - {audio: Blob, video: Blob, gif: Blob}
|
|
* @method
|
|
* @memberof RecordRTC
|
|
* @example
|
|
* RecordRTC.writeToDisk({
|
|
* audio: audioBlob,
|
|
* video: videoBlob,
|
|
* gif : gifBlob
|
|
* });
|
|
*/
|
|
RecordRTC.writeToDisk = function(options) {
|
|
console.log('Writing recorded blob(s) to disk!');
|
|
options = options || {};
|
|
if (options.audio && options.video && options.gif) {
|
|
options.audio.getDataURL(function(audioDataURL) {
|
|
options.video.getDataURL(function(videoDataURL) {
|
|
options.gif.getDataURL(function(gifDataURL) {
|
|
DiskStorage.Store({
|
|
audioBlob: audioDataURL,
|
|
videoBlob: videoDataURL,
|
|
gifBlob: gifDataURL
|
|
});
|
|
});
|
|
});
|
|
});
|
|
} else if (options.audio && options.video) {
|
|
options.audio.getDataURL(function(audioDataURL) {
|
|
options.video.getDataURL(function(videoDataURL) {
|
|
DiskStorage.Store({
|
|
audioBlob: audioDataURL,
|
|
videoBlob: videoDataURL
|
|
});
|
|
});
|
|
});
|
|
} else if (options.audio && options.gif) {
|
|
options.audio.getDataURL(function(audioDataURL) {
|
|
options.gif.getDataURL(function(gifDataURL) {
|
|
DiskStorage.Store({
|
|
audioBlob: audioDataURL,
|
|
gifBlob: gifDataURL
|
|
});
|
|
});
|
|
});
|
|
} else if (options.video && options.gif) {
|
|
options.video.getDataURL(function(videoDataURL) {
|
|
options.gif.getDataURL(function(gifDataURL) {
|
|
DiskStorage.Store({
|
|
videoBlob: videoDataURL,
|
|
gifBlob: gifDataURL
|
|
});
|
|
});
|
|
});
|
|
} else if (options.audio) {
|
|
options.audio.getDataURL(function(audioDataURL) {
|
|
DiskStorage.Store({
|
|
audioBlob: audioDataURL
|
|
});
|
|
});
|
|
} else if (options.video) {
|
|
options.video.getDataURL(function(videoDataURL) {
|
|
DiskStorage.Store({
|
|
videoBlob: videoDataURL
|
|
});
|
|
});
|
|
} else if (options.gif) {
|
|
options.gif.getDataURL(function(gifDataURL) {
|
|
DiskStorage.Store({
|
|
gifBlob: gifDataURL
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
// __________________________
|
|
// RecordRTC-Configuration.js
|
|
|
|
/**
|
|
* {@link RecordRTCConfiguration} is an inner/private helper for {@link RecordRTC}.
|
|
* @summary It configures the 2nd parameter passed over {@link RecordRTC} and returns a valid "config" object.
|
|
* @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
|
|
* @author {@link https://MuazKhan.com|Muaz Khan}
|
|
* @typedef RecordRTCConfiguration
|
|
* @class
|
|
* @example
|
|
* var options = RecordRTCConfiguration(mediaStream, options);
|
|
* @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
|
|
* @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API.
|
|
* @param {object} config - {type:"video", disableLogs: true, numberOfAudioChannels: 1, bufferSize: 0, sampleRate: 0, video: HTMLVideoElement, getNativeBlob:true, etc.}
|
|
*/
|
|
|
|
function RecordRTCConfiguration(mediaStream, config) {
|
|
if (!config.recorderType && !config.type) {
|
|
if (!!config.audio && !!config.video) {
|
|
config.type = 'video';
|
|
} else if (!!config.audio && !config.video) {
|
|
config.type = 'audio';
|
|
}
|
|
}
|
|
|
|
if (config.recorderType && !config.type) {
|
|
if (config.recorderType === WhammyRecorder || config.recorderType === CanvasRecorder || (typeof WebAssemblyRecorder !== 'undefined' && config.recorderType === WebAssemblyRecorder)) {
|
|
config.type = 'video';
|
|
} else if (config.recorderType === GifRecorder) {
|
|
config.type = 'gif';
|
|
} else if (config.recorderType === StereoAudioRecorder) {
|
|
config.type = 'audio';
|
|
} else if (config.recorderType === MediaStreamRecorder) {
|
|
if (getTracks(mediaStream, 'audio').length && getTracks(mediaStream, 'video').length) {
|
|
config.type = 'video';
|
|
} else if (!getTracks(mediaStream, 'audio').length && getTracks(mediaStream, 'video').length) {
|
|
config.type = 'video';
|
|
} else if (getTracks(mediaStream, 'audio').length && !getTracks(mediaStream, 'video').length) {
|
|
config.type = 'audio';
|
|
} else ;
|
|
}
|
|
}
|
|
|
|
if (typeof MediaStreamRecorder !== 'undefined' && typeof MediaRecorder !== 'undefined' && 'requestData' in MediaRecorder.prototype) {
|
|
if (!config.mimeType) {
|
|
config.mimeType = 'video/webm';
|
|
}
|
|
|
|
if (!config.type) {
|
|
config.type = config.mimeType.split('/')[0];
|
|
}
|
|
|
|
if (!config.bitsPerSecond) ;
|
|
}
|
|
|
|
// consider default type=audio
|
|
if (!config.type) {
|
|
if (config.mimeType) {
|
|
config.type = config.mimeType.split('/')[0];
|
|
}
|
|
if (!config.type) {
|
|
config.type = 'audio';
|
|
}
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
// __________________
|
|
// GetRecorderType.js
|
|
|
|
/**
|
|
* {@link GetRecorderType} is an inner/private helper for {@link RecordRTC}.
|
|
* @summary It returns best recorder-type available for your browser.
|
|
* @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
|
|
* @author {@link https://MuazKhan.com|Muaz Khan}
|
|
* @typedef GetRecorderType
|
|
* @class
|
|
* @example
|
|
* var RecorderType = GetRecorderType(options);
|
|
* var recorder = new RecorderType(options);
|
|
* @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
|
|
* @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API.
|
|
* @param {object} config - {type:"video", disableLogs: true, numberOfAudioChannels: 1, bufferSize: 0, sampleRate: 0, video: HTMLVideoElement, etc.}
|
|
*/
|
|
|
|
function GetRecorderType(mediaStream, config) {
|
|
var recorder;
|
|
|
|
// StereoAudioRecorder can work with all three: Edge, Firefox and Chrome
|
|
// todo: detect if it is Edge, then auto use: StereoAudioRecorder
|
|
if (isChrome || isEdge || isOpera) {
|
|
// Media Stream Recording API has not been implemented in chrome yet;
|
|
// That's why using WebAudio API to record stereo audio in WAV format
|
|
recorder = StereoAudioRecorder;
|
|
}
|
|
|
|
if (typeof MediaRecorder !== 'undefined' && 'requestData' in MediaRecorder.prototype && !isChrome) {
|
|
recorder = MediaStreamRecorder;
|
|
}
|
|
|
|
// video recorder (in WebM format)
|
|
if (config.type === 'video' && (isChrome || isOpera)) {
|
|
recorder = WhammyRecorder;
|
|
|
|
if (typeof WebAssemblyRecorder !== 'undefined' && typeof ReadableStream !== 'undefined') {
|
|
recorder = WebAssemblyRecorder;
|
|
}
|
|
}
|
|
|
|
// video recorder (in Gif format)
|
|
if (config.type === 'gif') {
|
|
recorder = GifRecorder;
|
|
}
|
|
|
|
// html2canvas recording!
|
|
if (config.type === 'canvas') {
|
|
recorder = CanvasRecorder;
|
|
}
|
|
|
|
if (isMediaRecorderCompatible() && recorder !== CanvasRecorder && recorder !== GifRecorder && typeof MediaRecorder !== 'undefined' && 'requestData' in MediaRecorder.prototype) {
|
|
if (getTracks(mediaStream, 'video').length || getTracks(mediaStream, 'audio').length) {
|
|
// audio-only recording
|
|
if (config.type === 'audio') {
|
|
if (typeof MediaRecorder.isTypeSupported === 'function' && MediaRecorder.isTypeSupported('audio/webm')) {
|
|
recorder = MediaStreamRecorder;
|
|
}
|
|
// else recorder = StereoAudioRecorder;
|
|
} else {
|
|
// video or screen tracks
|
|
if (typeof MediaRecorder.isTypeSupported === 'function' && MediaRecorder.isTypeSupported('video/webm')) {
|
|
recorder = MediaStreamRecorder;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mediaStream instanceof Array && mediaStream.length) {
|
|
recorder = MultiStreamRecorder;
|
|
}
|
|
|
|
if (config.recorderType) {
|
|
recorder = config.recorderType;
|
|
}
|
|
|
|
if (!config.disableLogs && !!recorder && !!recorder.name) {
|
|
console.log('Using recorderType:', recorder.name || recorder.constructor.name);
|
|
}
|
|
|
|
if (!recorder && isSafari) {
|
|
recorder = MediaStreamRecorder;
|
|
}
|
|
|
|
return recorder;
|
|
}
|
|
|
|
// _____________
|
|
// MRecordRTC.js
|
|
|
|
/**
|
|
* MRecordRTC runs on top of {@link RecordRTC} to bring multiple recordings in a single place, by providing simple API.
|
|
* @summary MRecordRTC stands for "Multiple-RecordRTC".
|
|
* @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
|
|
* @author {@link https://MuazKhan.com|Muaz Khan}
|
|
* @typedef MRecordRTC
|
|
* @class
|
|
* @example
|
|
* var recorder = new MRecordRTC();
|
|
* recorder.addStream(MediaStream);
|
|
* recorder.mediaType = {
|
|
* audio: true, // or StereoAudioRecorder or MediaStreamRecorder
|
|
* video: true, // or WhammyRecorder or MediaStreamRecorder or WebAssemblyRecorder or CanvasRecorder
|
|
* gif: true // or GifRecorder
|
|
* };
|
|
* // mimeType is optional and should be set only in advance cases.
|
|
* recorder.mimeType = {
|
|
* audio: 'audio/wav',
|
|
* video: 'video/webm',
|
|
* gif: 'image/gif'
|
|
* };
|
|
* recorder.startRecording();
|
|
* @see For further information:
|
|
* @see {@link https://github.com/muaz-khan/RecordRTC/tree/master/MRecordRTC|MRecordRTC Source Code}
|
|
* @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API.
|
|
* @requires {@link RecordRTC}
|
|
*/
|
|
|
|
function MRecordRTC(mediaStream) {
|
|
|
|
/**
|
|
* This method attaches MediaStream object to {@link MRecordRTC}.
|
|
* @param {MediaStream} mediaStream - A MediaStream object, either fetched using getUserMedia API, or generated using captureStreamUntilEnded or WebAudio API.
|
|
* @method
|
|
* @memberof MRecordRTC
|
|
* @example
|
|
* recorder.addStream(MediaStream);
|
|
*/
|
|
this.addStream = function(_mediaStream) {
|
|
if (_mediaStream) {
|
|
mediaStream = _mediaStream;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This property can be used to set the recording type e.g. audio, or video, or gif, or canvas.
|
|
* @property {object} mediaType - {audio: true, video: true, gif: true}
|
|
* @memberof MRecordRTC
|
|
* @example
|
|
* var recorder = new MRecordRTC();
|
|
* recorder.mediaType = {
|
|
* audio: true, // TRUE or StereoAudioRecorder or MediaStreamRecorder
|
|
* video: true, // TRUE or WhammyRecorder or MediaStreamRecorder or WebAssemblyRecorder or CanvasRecorder
|
|
* gif : true // TRUE or GifRecorder
|
|
* };
|
|
*/
|
|
this.mediaType = {
|
|
audio: true,
|
|
video: true
|
|
};
|
|
|
|
/**
|
|
* This method starts recording.
|
|
* @method
|
|
* @memberof MRecordRTC
|
|
* @example
|
|
* recorder.startRecording();
|
|
*/
|
|
this.startRecording = function() {
|
|
var mediaType = this.mediaType;
|
|
var recorderType;
|
|
var mimeType = this.mimeType || {
|
|
audio: null,
|
|
video: null,
|
|
gif: null
|
|
};
|
|
|
|
if (typeof mediaType.audio !== 'function' && isMediaRecorderCompatible() && !getTracks(mediaStream, 'audio').length) {
|
|
mediaType.audio = false;
|
|
}
|
|
|
|
if (typeof mediaType.video !== 'function' && isMediaRecorderCompatible() && !getTracks(mediaStream, 'video').length) {
|
|
mediaType.video = false;
|
|
}
|
|
|
|
if (typeof mediaType.gif !== 'function' && isMediaRecorderCompatible() && !getTracks(mediaStream, 'video').length) {
|
|
mediaType.gif = false;
|
|
}
|
|
|
|
if (!mediaType.audio && !mediaType.video && !mediaType.gif) {
|
|
throw 'MediaStream must have either audio or video tracks.';
|
|
}
|
|
|
|
if (!!mediaType.audio) {
|
|
recorderType = null;
|
|
if (typeof mediaType.audio === 'function') {
|
|
recorderType = mediaType.audio;
|
|
}
|
|
|
|
this.audioRecorder = new RecordRTC(mediaStream, {
|
|
type: 'audio',
|
|
bufferSize: this.bufferSize,
|
|
sampleRate: this.sampleRate,
|
|
numberOfAudioChannels: this.numberOfAudioChannels || 2,
|
|
disableLogs: this.disableLogs,
|
|
recorderType: recorderType,
|
|
mimeType: mimeType.audio,
|
|
timeSlice: this.timeSlice,
|
|
onTimeStamp: this.onTimeStamp
|
|
});
|
|
|
|
if (!mediaType.video) {
|
|
this.audioRecorder.startRecording();
|
|
}
|
|
}
|
|
|
|
if (!!mediaType.video) {
|
|
recorderType = null;
|
|
if (typeof mediaType.video === 'function') {
|
|
recorderType = mediaType.video;
|
|
}
|
|
|
|
var newStream = mediaStream;
|
|
|
|
if (isMediaRecorderCompatible() && !!mediaType.audio && typeof mediaType.audio === 'function') {
|
|
var videoTrack = getTracks(mediaStream, 'video')[0];
|
|
|
|
if (isFirefox) {
|
|
newStream = new MediaStream();
|
|
newStream.addTrack(videoTrack);
|
|
|
|
if (recorderType && recorderType === WhammyRecorder) {
|
|
// Firefox does NOT supports webp-encoding yet
|
|
// But Firefox do supports WebAssemblyRecorder
|
|
recorderType = MediaStreamRecorder;
|
|
}
|
|
} else {
|
|
newStream = new MediaStream();
|
|
newStream.addTrack(videoTrack);
|
|
}
|
|
}
|
|
|
|
this.videoRecorder = new RecordRTC(newStream, {
|
|
type: 'video',
|
|
video: this.video,
|
|
canvas: this.canvas,
|
|
frameInterval: this.frameInterval || 10,
|
|
disableLogs: this.disableLogs,
|
|
recorderType: recorderType,
|
|
mimeType: mimeType.video,
|
|
timeSlice: this.timeSlice,
|
|
onTimeStamp: this.onTimeStamp,
|
|
workerPath: this.workerPath,
|
|
webAssemblyPath: this.webAssemblyPath,
|
|
frameRate: this.frameRate, // used by WebAssemblyRecorder; values: usually 30; accepts any.
|
|
bitrate: this.bitrate // used by WebAssemblyRecorder; values: 0 to 1000+
|
|
});
|
|
|
|
if (!mediaType.audio) {
|
|
this.videoRecorder.startRecording();
|
|
}
|
|
}
|
|
|
|
if (!!mediaType.audio && !!mediaType.video) {
|
|
var self = this;
|
|
|
|
var isSingleRecorder = isMediaRecorderCompatible() === true;
|
|
|
|
if (mediaType.audio instanceof StereoAudioRecorder && !!mediaType.video) {
|
|
isSingleRecorder = false;
|
|
} else if (mediaType.audio !== true && mediaType.video !== true && mediaType.audio !== mediaType.video) {
|
|
isSingleRecorder = false;
|
|
}
|
|
|
|
if (isSingleRecorder === true) {
|
|
self.audioRecorder = null;
|
|
self.videoRecorder.startRecording();
|
|
} else {
|
|
self.videoRecorder.initRecorder(function() {
|
|
self.audioRecorder.initRecorder(function() {
|
|
// Both recorders are ready to record things accurately
|
|
self.videoRecorder.startRecording();
|
|
self.audioRecorder.startRecording();
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
if (!!mediaType.gif) {
|
|
recorderType = null;
|
|
if (typeof mediaType.gif === 'function') {
|
|
recorderType = mediaType.gif;
|
|
}
|
|
this.gifRecorder = new RecordRTC(mediaStream, {
|
|
type: 'gif',
|
|
frameRate: this.frameRate || 200,
|
|
quality: this.quality || 10,
|
|
disableLogs: this.disableLogs,
|
|
recorderType: recorderType,
|
|
mimeType: mimeType.gif
|
|
});
|
|
this.gifRecorder.startRecording();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This method stops recording.
|
|
* @param {function} callback - Callback function is invoked when all encoders finished their jobs.
|
|
* @method
|
|
* @memberof MRecordRTC
|
|
* @example
|
|
* recorder.stopRecording(function(recording){
|
|
* var audioBlob = recording.audio;
|
|
* var videoBlob = recording.video;
|
|
* var gifBlob = recording.gif;
|
|
* });
|
|
*/
|
|
this.stopRecording = function(callback) {
|
|
callback = callback || function() {};
|
|
|
|
if (this.audioRecorder) {
|
|
this.audioRecorder.stopRecording(function(blobURL) {
|
|
callback(blobURL, 'audio');
|
|
});
|
|
}
|
|
|
|
if (this.videoRecorder) {
|
|
this.videoRecorder.stopRecording(function(blobURL) {
|
|
callback(blobURL, 'video');
|
|
});
|
|
}
|
|
|
|
if (this.gifRecorder) {
|
|
this.gifRecorder.stopRecording(function(blobURL) {
|
|
callback(blobURL, 'gif');
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This method pauses recording.
|
|
* @method
|
|
* @memberof MRecordRTC
|
|
* @example
|
|
* recorder.pauseRecording();
|
|
*/
|
|
this.pauseRecording = function() {
|
|
if (this.audioRecorder) {
|
|
this.audioRecorder.pauseRecording();
|
|
}
|
|
|
|
if (this.videoRecorder) {
|
|
this.videoRecorder.pauseRecording();
|
|
}
|
|
|
|
if (this.gifRecorder) {
|
|
this.gifRecorder.pauseRecording();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This method resumes recording.
|
|
* @method
|
|
* @memberof MRecordRTC
|
|
* @example
|
|
* recorder.resumeRecording();
|
|
*/
|
|
this.resumeRecording = function() {
|
|
if (this.audioRecorder) {
|
|
this.audioRecorder.resumeRecording();
|
|
}
|
|
|
|
if (this.videoRecorder) {
|
|
this.videoRecorder.resumeRecording();
|
|
}
|
|
|
|
if (this.gifRecorder) {
|
|
this.gifRecorder.resumeRecording();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This method can be used to manually get all recorded blobs.
|
|
* @param {function} callback - All recorded blobs are passed back to the "callback" function.
|
|
* @method
|
|
* @memberof MRecordRTC
|
|
* @example
|
|
* recorder.getBlob(function(recording){
|
|
* var audioBlob = recording.audio;
|
|
* var videoBlob = recording.video;
|
|
* var gifBlob = recording.gif;
|
|
* });
|
|
* // or
|
|
* var audioBlob = recorder.getBlob().audio;
|
|
* var videoBlob = recorder.getBlob().video;
|
|
*/
|
|
this.getBlob = function(callback) {
|
|
var output = {};
|
|
|
|
if (this.audioRecorder) {
|
|
output.audio = this.audioRecorder.getBlob();
|
|
}
|
|
|
|
if (this.videoRecorder) {
|
|
output.video = this.videoRecorder.getBlob();
|
|
}
|
|
|
|
if (this.gifRecorder) {
|
|
output.gif = this.gifRecorder.getBlob();
|
|
}
|
|
|
|
if (callback) {
|
|
callback(output);
|
|
}
|
|
|
|
return output;
|
|
};
|
|
|
|
/**
|
|
* Destroy all recorder instances.
|
|
* @method
|
|
* @memberof MRecordRTC
|
|
* @example
|
|
* recorder.destroy();
|
|
*/
|
|
this.destroy = function() {
|
|
if (this.audioRecorder) {
|
|
this.audioRecorder.destroy();
|
|
this.audioRecorder = null;
|
|
}
|
|
|
|
if (this.videoRecorder) {
|
|
this.videoRecorder.destroy();
|
|
this.videoRecorder = null;
|
|
}
|
|
|
|
if (this.gifRecorder) {
|
|
this.gifRecorder.destroy();
|
|
this.gifRecorder = null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This method can be used to manually get all recorded blobs' DataURLs.
|
|
* @param {function} callback - All recorded blobs' DataURLs are passed back to the "callback" function.
|
|
* @method
|
|
* @memberof MRecordRTC
|
|
* @example
|
|
* recorder.getDataURL(function(recording){
|
|
* var audioDataURL = recording.audio;
|
|
* var videoDataURL = recording.video;
|
|
* var gifDataURL = recording.gif;
|
|
* });
|
|
*/
|
|
this.getDataURL = function(callback) {
|
|
this.getBlob(function(blob) {
|
|
if (blob.audio && blob.video) {
|
|
getDataURL(blob.audio, function(_audioDataURL) {
|
|
getDataURL(blob.video, function(_videoDataURL) {
|
|
callback({
|
|
audio: _audioDataURL,
|
|
video: _videoDataURL
|
|
});
|
|
});
|
|
});
|
|
} else if (blob.audio) {
|
|
getDataURL(blob.audio, function(_audioDataURL) {
|
|
callback({
|
|
audio: _audioDataURL
|
|
});
|
|
});
|
|
} else if (blob.video) {
|
|
getDataURL(blob.video, function(_videoDataURL) {
|
|
callback({
|
|
video: _videoDataURL
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
function getDataURL(blob, callback00) {
|
|
if (typeof Worker !== 'undefined') {
|
|
var webWorker = processInWebWorker(function readFile(_blob) {
|
|
postMessage(new FileReaderSync().readAsDataURL(_blob));
|
|
});
|
|
|
|
webWorker.onmessage = function(event) {
|
|
callback00(event.data);
|
|
};
|
|
|
|
webWorker.postMessage(blob);
|
|
} else {
|
|
var reader = new FileReader();
|
|
reader.readAsDataURL(blob);
|
|
reader.onload = function(event) {
|
|
callback00(event.target.result);
|
|
};
|
|
}
|
|
}
|
|
|
|
function processInWebWorker(_function) {
|
|
var blob = URL.createObjectURL(new Blob([_function.toString(),
|
|
'this.onmessage = function (eee) {' + _function.name + '(eee.data);}'
|
|
], {
|
|
type: 'application/javascript'
|
|
}));
|
|
|
|
var worker = new Worker(blob);
|
|
var url;
|
|
if (typeof URL !== 'undefined') {
|
|
url = URL;
|
|
} else if (typeof webkitURL !== 'undefined') {
|
|
url = webkitURL;
|
|
} else {
|
|
throw 'Neither URL nor webkitURL detected.';
|
|
}
|
|
url.revokeObjectURL(blob);
|
|
return worker;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This method can be used to ask {@link MRecordRTC} to write all recorded blobs into IndexedDB storage.
|
|
* @method
|
|
* @memberof MRecordRTC
|
|
* @example
|
|
* recorder.writeToDisk();
|
|
*/
|
|
this.writeToDisk = function() {
|
|
RecordRTC.writeToDisk({
|
|
audio: this.audioRecorder,
|
|
video: this.videoRecorder,
|
|
gif: this.gifRecorder
|
|
});
|
|
};
|
|
|
|
/**
|
|
* This method can be used to invoke a save-as dialog for all recorded blobs.
|
|
* @param {object} args - {audio: 'audio-name', video: 'video-name', gif: 'gif-name'}
|
|
* @method
|
|
* @memberof MRecordRTC
|
|
* @example
|
|
* recorder.save({
|
|
* audio: 'audio-file-name',
|
|
* video: 'video-file-name',
|
|
* gif : 'gif-file-name'
|
|
* });
|
|
*/
|
|
this.save = function(args) {
|
|
args = args || {
|
|
audio: true,
|
|
video: true,
|
|
gif: true
|
|
};
|
|
|
|
if (!!args.audio && this.audioRecorder) {
|
|
this.audioRecorder.save(typeof args.audio === 'string' ? args.audio : '');
|
|
}
|
|
|
|
if (!!args.video && this.videoRecorder) {
|
|
this.videoRecorder.save(typeof args.video === 'string' ? args.video : '');
|
|
}
|
|
if (!!args.gif && this.gifRecorder) {
|
|
this.gifRecorder.save(typeof args.gif === 'string' ? args.gif : '');
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* This method can be used to get all recorded blobs from IndexedDB storage.
|
|
* @param {string} type - 'all' or 'audio' or 'video' or 'gif'
|
|
* @param {function} callback - Callback function to get all stored blobs.
|
|
* @method
|
|
* @memberof MRecordRTC
|
|
* @example
|
|
* MRecordRTC.getFromDisk('all', function(dataURL, type){
|
|
* if(type === 'audio') { }
|
|
* if(type === 'video') { }
|
|
* if(type === 'gif') { }
|
|
* });
|
|
*/
|
|
MRecordRTC.getFromDisk = RecordRTC.getFromDisk;
|
|
|
|
/**
|
|
* This method can be used to store recorded blobs into IndexedDB storage.
|
|
* @param {object} options - {audio: Blob, video: Blob, gif: Blob}
|
|
* @method
|
|
* @memberof MRecordRTC
|
|
* @example
|
|
* MRecordRTC.writeToDisk({
|
|
* audio: audioBlob,
|
|
* video: videoBlob,
|
|
* gif : gifBlob
|
|
* });
|
|
*/
|
|
MRecordRTC.writeToDisk = RecordRTC.writeToDisk;
|
|
|
|
if (typeof RecordRTC !== 'undefined') {
|
|
RecordRTC.MRecordRTC = MRecordRTC;
|
|
}
|
|
|
|
var browserFakeUserAgent = 'Fake/5.0 (FakeOS) AppleWebKit/123 (KHTML, like Gecko) Fake/12.3.4567.89 Fake/123.45';
|
|
|
|
(function(that) {
|
|
if (!that) {
|
|
return;
|
|
}
|
|
|
|
if (typeof window !== 'undefined') {
|
|
return;
|
|
}
|
|
|
|
if (typeof commonjsGlobal === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
commonjsGlobal.navigator = {
|
|
userAgent: browserFakeUserAgent,
|
|
getUserMedia: function() {}
|
|
};
|
|
|
|
if (!commonjsGlobal.console) {
|
|
commonjsGlobal.console = {};
|
|
}
|
|
|
|
if (typeof commonjsGlobal.console.log === 'undefined' || typeof commonjsGlobal.console.error === 'undefined') {
|
|
commonjsGlobal.console.error = commonjsGlobal.console.log = commonjsGlobal.console.log || function() {
|
|
console.log(arguments);
|
|
};
|
|
}
|
|
|
|
if (typeof document === 'undefined') {
|
|
/*global document:true */
|
|
that.document = {
|
|
documentElement: {
|
|
appendChild: function() {
|
|
return '';
|
|
}
|
|
}
|
|
};
|
|
|
|
document.createElement = document.captureStream = document.mozCaptureStream = function() {
|
|
var obj = {
|
|
getContext: function() {
|
|
return obj;
|
|
},
|
|
play: function() {},
|
|
pause: function() {},
|
|
drawImage: function() {},
|
|
toDataURL: function() {
|
|
return '';
|
|
},
|
|
style: {}
|
|
};
|
|
return obj;
|
|
};
|
|
|
|
that.HTMLVideoElement = function() {};
|
|
}
|
|
|
|
if (typeof location === 'undefined') {
|
|
/*global location:true */
|
|
that.location = {
|
|
protocol: 'file:',
|
|
href: '',
|
|
hash: ''
|
|
};
|
|
}
|
|
|
|
if (typeof screen === 'undefined') {
|
|
/*global screen:true */
|
|
that.screen = {
|
|
width: 0,
|
|
height: 0
|
|
};
|
|
}
|
|
|
|
if (typeof URL === 'undefined') {
|
|
/*global screen:true */
|
|
that.URL = {
|
|
createObjectURL: function() {
|
|
return '';
|
|
},
|
|
revokeObjectURL: function() {
|
|
return '';
|
|
}
|
|
};
|
|
}
|
|
|
|
/*global window:true */
|
|
that.window = commonjsGlobal;
|
|
})(typeof commonjsGlobal !== 'undefined' ? commonjsGlobal : null);
|
|
|
|
// _____________________________
|
|
// Cross-Browser-Declarations.js
|
|
|
|
// animation-frame used in WebM recording
|
|
|
|
/*jshint -W079 */
|
|
var requestAnimationFrame = window.requestAnimationFrame;
|
|
if (typeof requestAnimationFrame === 'undefined') {
|
|
if (typeof webkitRequestAnimationFrame !== 'undefined') {
|
|
/*global requestAnimationFrame:true */
|
|
requestAnimationFrame = webkitRequestAnimationFrame;
|
|
} else if (typeof mozRequestAnimationFrame !== 'undefined') {
|
|
/*global requestAnimationFrame:true */
|
|
requestAnimationFrame = mozRequestAnimationFrame;
|
|
} else if (typeof msRequestAnimationFrame !== 'undefined') {
|
|
/*global requestAnimationFrame:true */
|
|
requestAnimationFrame = msRequestAnimationFrame;
|
|
} else if (typeof requestAnimationFrame === 'undefined') {
|
|
// via: https://gist.github.com/paulirish/1579671
|
|
var lastTime = 0;
|
|
|
|
/*global requestAnimationFrame:true */
|
|
requestAnimationFrame = function(callback, element) {
|
|
var currTime = new Date().getTime();
|
|
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
|
|
var id = setTimeout(function() {
|
|
callback(currTime + timeToCall);
|
|
}, timeToCall);
|
|
lastTime = currTime + timeToCall;
|
|
return id;
|
|
};
|
|
}
|
|
}
|
|
|
|
/*jshint -W079 */
|
|
var cancelAnimationFrame = window.cancelAnimationFrame;
|
|
if (typeof cancelAnimationFrame === 'undefined') {
|
|
if (typeof webkitCancelAnimationFrame !== 'undefined') {
|
|
/*global cancelAnimationFrame:true */
|
|
cancelAnimationFrame = webkitCancelAnimationFrame;
|
|
} else if (typeof mozCancelAnimationFrame !== 'undefined') {
|
|
/*global cancelAnimationFrame:true */
|
|
cancelAnimationFrame = mozCancelAnimationFrame;
|
|
} else if (typeof msCancelAnimationFrame !== 'undefined') {
|
|
/*global cancelAnimationFrame:true */
|
|
cancelAnimationFrame = msCancelAnimationFrame;
|
|
} else if (typeof cancelAnimationFrame === 'undefined') {
|
|
/*global cancelAnimationFrame:true */
|
|
cancelAnimationFrame = function(id) {
|
|
clearTimeout(id);
|
|
};
|
|
}
|
|
}
|
|
|
|
// WebAudio API representer
|
|
var AudioContext = window.AudioContext;
|
|
|
|
if (typeof AudioContext === 'undefined') {
|
|
if (typeof webkitAudioContext !== 'undefined') {
|
|
/*global AudioContext:true */
|
|
AudioContext = webkitAudioContext;
|
|
}
|
|
|
|
if (typeof mozAudioContext !== 'undefined') {
|
|
/*global AudioContext:true */
|
|
AudioContext = mozAudioContext;
|
|
}
|
|
}
|
|
|
|
/*jshint -W079 */
|
|
var URL = window.URL;
|
|
|
|
if (typeof URL === 'undefined' && typeof webkitURL !== 'undefined') {
|
|
/*global URL:true */
|
|
URL = webkitURL;
|
|
}
|
|
|
|
if (typeof navigator !== 'undefined' && typeof navigator.getUserMedia === 'undefined') { // maybe window.navigator?
|
|
if (typeof navigator.webkitGetUserMedia !== 'undefined') {
|
|
navigator.getUserMedia = navigator.webkitGetUserMedia;
|
|
}
|
|
|
|
if (typeof navigator.mozGetUserMedia !== 'undefined') {
|
|
navigator.getUserMedia = navigator.mozGetUserMedia;
|
|
}
|
|
}
|
|
|
|
var isEdge = navigator.userAgent.indexOf('Edge') !== -1 && (!!navigator.msSaveBlob || !!navigator.msSaveOrOpenBlob);
|
|
var isOpera = !!window.opera || navigator.userAgent.indexOf('OPR/') !== -1;
|
|
var isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1 && ('netscape' in window) && / rv:/.test(navigator.userAgent);
|
|
var isChrome = (!isOpera && !isEdge && !!navigator.webkitGetUserMedia) || isElectron() || navigator.userAgent.toLowerCase().indexOf('chrome/') !== -1;
|
|
|
|
var isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
|
|
|
if (isSafari && !isChrome && navigator.userAgent.indexOf('CriOS') !== -1) {
|
|
isSafari = false;
|
|
isChrome = true;
|
|
}
|
|
|
|
var MediaStream = window.MediaStream;
|
|
|
|
if (typeof MediaStream === 'undefined' && typeof webkitMediaStream !== 'undefined') {
|
|
MediaStream = webkitMediaStream;
|
|
}
|
|
|
|
/*global MediaStream:true */
|
|
if (typeof MediaStream !== 'undefined') {
|
|
// override "stop" method for all browsers
|
|
if (typeof MediaStream.prototype.stop === 'undefined') {
|
|
MediaStream.prototype.stop = function() {
|
|
this.getTracks().forEach(function(track) {
|
|
track.stop();
|
|
});
|
|
};
|
|
}
|
|
}
|
|
|
|
// below function via: http://goo.gl/B3ae8c
|
|
/**
|
|
* Return human-readable file size.
|
|
* @param {number} bytes - Pass bytes and get formatted string.
|
|
* @returns {string} - formatted string
|
|
* @example
|
|
* bytesToSize(1024*1024*5) === '5 GB'
|
|
* @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
|
|
*/
|
|
function bytesToSize(bytes) {
|
|
var k = 1000;
|
|
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
if (bytes === 0) {
|
|
return '0 Bytes';
|
|
}
|
|
var i = parseInt(Math.floor(Math.log(bytes) / Math.log(k)), 10);
|
|
return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i];
|
|
}
|
|
|
|
/**
|
|
* @param {Blob} file - File or Blob object. This parameter is required.
|
|
* @param {string} fileName - Optional file name e.g. "Recorded-Video.webm"
|
|
* @example
|
|
* invokeSaveAsDialog(blob or file, [optional] fileName);
|
|
* @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
|
|
*/
|
|
function invokeSaveAsDialog(file, fileName) {
|
|
if (!file) {
|
|
throw 'Blob object is required.';
|
|
}
|
|
|
|
if (!file.type) {
|
|
try {
|
|
file.type = 'video/webm';
|
|
} catch (e) {}
|
|
}
|
|
|
|
var fileExtension = (file.type || 'video/webm').split('/')[1];
|
|
if (fileExtension.indexOf(';') !== -1) {
|
|
// extended mimetype, e.g. 'video/webm;codecs=vp8,opus'
|
|
fileExtension = fileExtension.split(';')[0];
|
|
}
|
|
if (fileName && fileName.indexOf('.') !== -1) {
|
|
var splitted = fileName.split('.');
|
|
fileName = splitted[0];
|
|
fileExtension = splitted[1];
|
|
}
|
|
|
|
var fileFullName = (fileName || (Math.round(Math.random() * 9999999999) + 888888888)) + '.' + fileExtension;
|
|
|
|
if (typeof navigator.msSaveOrOpenBlob !== 'undefined') {
|
|
return navigator.msSaveOrOpenBlob(file, fileFullName);
|
|
} else if (typeof navigator.msSaveBlob !== 'undefined') {
|
|
return navigator.msSaveBlob(file, fileFullName);
|
|
}
|
|
|
|
var hyperlink = document.createElement('a');
|
|
hyperlink.href = URL.createObjectURL(file);
|
|
hyperlink.download = fileFullName;
|
|
|
|
hyperlink.style = 'display:none;opacity:0;color:transparent;';
|
|
(document.body || document.documentElement).appendChild(hyperlink);
|
|
|
|
if (typeof hyperlink.click === 'function') {
|
|
hyperlink.click();
|
|
} else {
|
|
hyperlink.target = '_blank';
|
|
hyperlink.dispatchEvent(new MouseEvent('click', {
|
|
view: window,
|
|
bubbles: true,
|
|
cancelable: true
|
|
}));
|
|
}
|
|
|
|
URL.revokeObjectURL(hyperlink.href);
|
|
}
|
|
|
|
/**
|
|
* from: https://github.com/cheton/is-electron/blob/master/index.js
|
|
**/
|
|
function isElectron() {
|
|
// Renderer process
|
|
if (typeof window !== 'undefined' && typeof window.process === 'object' && window.process.type === 'renderer') {
|
|
return true;
|
|
}
|
|
|
|
// Main process
|
|
if (typeof process !== 'undefined' && typeof process.versions === 'object' && !!process.versions.electron) {
|
|
return true;
|
|
}
|
|
|
|
// Detect the user agent when the `nodeIntegration` option is set to true
|
|
if (typeof navigator === 'object' && typeof navigator.userAgent === 'string' && navigator.userAgent.indexOf('Electron') >= 0) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function getTracks(stream, kind) {
|
|
if (!stream || !stream.getTracks) {
|
|
return [];
|
|
}
|
|
|
|
return stream.getTracks().filter(function(t) {
|
|
return t.kind === (kind || 'audio');
|
|
});
|
|
}
|
|
|
|
function setSrcObject(stream, element) {
|
|
if ('srcObject' in element) {
|
|
element.srcObject = stream;
|
|
} else if ('mozSrcObject' in element) {
|
|
element.mozSrcObject = stream;
|
|
} else {
|
|
element.srcObject = stream;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Blob} file - File or Blob object.
|
|
* @param {function} callback - Callback function.
|
|
* @example
|
|
* getSeekableBlob(blob or file, callback);
|
|
* @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
|
|
*/
|
|
function getSeekableBlob(inputBlob, callback) {
|
|
// EBML.js copyrights goes to: https://github.com/legokichi/ts-ebml
|
|
if (typeof EBML === 'undefined') {
|
|
throw new Error('Please link: https://www.webrtc-experiment.com/EBML.js');
|
|
}
|
|
|
|
var reader = new EBML.Reader();
|
|
var decoder = new EBML.Decoder();
|
|
var tools = EBML.tools;
|
|
|
|
var fileReader = new FileReader();
|
|
fileReader.onload = function(e) {
|
|
var ebmlElms = decoder.decode(this.result);
|
|
ebmlElms.forEach(function(element) {
|
|
reader.read(element);
|
|
});
|
|
reader.stop();
|
|
var refinedMetadataBuf = tools.makeMetadataSeekable(reader.metadatas, reader.duration, reader.cues);
|
|
var body = this.result.slice(reader.metadataSize);
|
|
var newBlob = new Blob([refinedMetadataBuf, body], {
|
|
type: 'video/webm'
|
|
});
|
|
|
|
callback(newBlob);
|
|
};
|
|
fileReader.readAsArrayBuffer(inputBlob);
|
|
}
|
|
|
|
if (typeof RecordRTC !== 'undefined') {
|
|
RecordRTC.invokeSaveAsDialog = invokeSaveAsDialog;
|
|
RecordRTC.getTracks = getTracks;
|
|
RecordRTC.getSeekableBlob = getSeekableBlob;
|
|
RecordRTC.bytesToSize = bytesToSize;
|
|
RecordRTC.isElectron = isElectron;
|
|
}
|
|
|
|
// __________ (used to handle stuff like http://goo.gl/xmE5eg) issue #129
|
|
// Storage.js
|
|
|
|
/**
|
|
* Storage is a standalone object used by {@link RecordRTC} to store reusable objects e.g. "new AudioContext".
|
|
* @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
|
|
* @author {@link https://MuazKhan.com|Muaz Khan}
|
|
* @example
|
|
* Storage.AudioContext === webkitAudioContext
|
|
* @property {webkitAudioContext} AudioContext - Keeps a reference to AudioContext object.
|
|
* @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
|
|
*/
|
|
|
|
var Storage = {};
|
|
|
|
if (typeof AudioContext !== 'undefined') {
|
|
Storage.AudioContext = AudioContext;
|
|
} else if (typeof webkitAudioContext !== 'undefined') {
|
|
Storage.AudioContext = webkitAudioContext;
|
|
}
|
|
|
|
if (typeof RecordRTC !== 'undefined') {
|
|
RecordRTC.Storage = Storage;
|
|
}
|
|
|
|
function isMediaRecorderCompatible() {
|
|
if (isFirefox || isSafari || isEdge) {
|
|
return true;
|
|
}
|
|
var nAgt = navigator.userAgent;
|
|
var fullVersion = '' + parseFloat(navigator.appVersion);
|
|
var majorVersion = parseInt(navigator.appVersion, 10);
|
|
var verOffset, ix;
|
|
|
|
if (isChrome || isOpera) {
|
|
verOffset = nAgt.indexOf('Chrome');
|
|
fullVersion = nAgt.substring(verOffset + 7);
|
|
}
|
|
|
|
// trim the fullVersion string at semicolon/space if present
|
|
if ((ix = fullVersion.indexOf(';')) !== -1) {
|
|
fullVersion = fullVersion.substring(0, ix);
|
|
}
|
|
|
|
if ((ix = fullVersion.indexOf(' ')) !== -1) {
|
|
fullVersion = fullVersion.substring(0, ix);
|
|
}
|
|
|
|
majorVersion = parseInt('' + fullVersion, 10);
|
|
|
|
if (isNaN(majorVersion)) {
|
|
fullVersion = '' + parseFloat(navigator.appVersion);
|
|
majorVersion = parseInt(navigator.appVersion, 10);
|
|
}
|
|
|
|
return majorVersion >= 49;
|
|
}
|
|
|
|
// ______________________
|
|
// MediaStreamRecorder.js
|
|
|
|
/**
|
|
* MediaStreamRecorder is an abstraction layer for {@link https://w3c.github.io/mediacapture-record/MediaRecorder.html|MediaRecorder API}. It is used by {@link RecordRTC} to record MediaStream(s) in both Chrome and Firefox.
|
|
* @summary Runs top over {@link https://w3c.github.io/mediacapture-record/MediaRecorder.html|MediaRecorder API}.
|
|
* @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
|
|
* @author {@link https://github.com/muaz-khan|Muaz Khan}
|
|
* @typedef MediaStreamRecorder
|
|
* @class
|
|
* @example
|
|
* var config = {
|
|
* mimeType: 'video/webm', // vp8, vp9, h264, mkv, opus/vorbis
|
|
* audioBitsPerSecond : 256 * 8 * 1024,
|
|
* videoBitsPerSecond : 256 * 8 * 1024,
|
|
* bitsPerSecond: 256 * 8 * 1024, // if this is provided, skip above two
|
|
* checkForInactiveTracks: true,
|
|
* timeSlice: 1000, // concatenate intervals based blobs
|
|
* ondataavailable: function() {} // get intervals based blobs
|
|
* }
|
|
* var recorder = new MediaStreamRecorder(mediaStream, config);
|
|
* recorder.record();
|
|
* recorder.stop(function(blob) {
|
|
* video.src = URL.createObjectURL(blob);
|
|
*
|
|
* // or
|
|
* var blob = recorder.blob;
|
|
* });
|
|
* @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
|
|
* @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API.
|
|
* @param {object} config - {disableLogs:true, initCallback: function, mimeType: "video/webm", timeSlice: 1000}
|
|
* @throws Will throw an error if first argument "MediaStream" is missing. Also throws error if "MediaRecorder API" are not supported by the browser.
|
|
*/
|
|
|
|
function MediaStreamRecorder(mediaStream, config) {
|
|
var self = this;
|
|
|
|
if (typeof mediaStream === 'undefined') {
|
|
throw 'First argument "MediaStream" is required.';
|
|
}
|
|
|
|
if (typeof MediaRecorder === 'undefined') {
|
|
throw 'Your browser does not support the Media Recorder API. Please try other modules e.g. WhammyRecorder or StereoAudioRecorder.';
|
|
}
|
|
|
|
config = config || {
|
|
// bitsPerSecond: 256 * 8 * 1024,
|
|
mimeType: 'video/webm'
|
|
};
|
|
|
|
if (config.type === 'audio') {
|
|
if (getTracks(mediaStream, 'video').length && getTracks(mediaStream, 'audio').length) {
|
|
var stream;
|
|
if (!!navigator.mozGetUserMedia) {
|
|
stream = new MediaStream();
|
|
stream.addTrack(getTracks(mediaStream, 'audio')[0]);
|
|
} else {
|
|
// webkitMediaStream
|
|
stream = new MediaStream(getTracks(mediaStream, 'audio'));
|
|
}
|
|
mediaStream = stream;
|
|
}
|
|
|
|
if (!config.mimeType || config.mimeType.toString().toLowerCase().indexOf('audio') === -1) {
|
|
config.mimeType = isChrome ? 'audio/webm' : 'audio/ogg';
|
|
}
|
|
|
|
if (config.mimeType && config.mimeType.toString().toLowerCase() !== 'audio/ogg' && !!navigator.mozGetUserMedia) {
|
|
// forcing better codecs on Firefox (via #166)
|
|
config.mimeType = 'audio/ogg';
|
|
}
|
|
}
|
|
|
|
var arrayOfBlobs = [];
|
|
|
|
/**
|
|
* This method returns array of blobs. Use only with "timeSlice". Its useful to preview recording anytime, without using the "stop" method.
|
|
* @method
|
|
* @memberof MediaStreamRecorder
|
|
* @example
|
|
* var arrayOfBlobs = recorder.getArrayOfBlobs();
|
|
* @returns {Array} Returns array of recorded blobs.
|
|
*/
|
|
this.getArrayOfBlobs = function() {
|
|
return arrayOfBlobs;
|
|
};
|
|
|
|
/**
|
|
* This method records MediaStream.
|
|
* @method
|
|
* @memberof MediaStreamRecorder
|
|
* @example
|
|
* recorder.record();
|
|
*/
|
|
this.record = function() {
|
|
// set defaults
|
|
self.blob = null;
|
|
self.clearRecordedData();
|
|
self.timestamps = [];
|
|
allStates = [];
|
|
arrayOfBlobs = [];
|
|
|
|
var recorderHints = config;
|
|
|
|
if (!config.disableLogs) {
|
|
console.log('Passing following config over MediaRecorder API.', recorderHints);
|
|
}
|
|
|
|
if (mediaRecorder) {
|
|
// mandatory to make sure Firefox doesn't fails to record streams 3-4 times without reloading the page.
|
|
mediaRecorder = null;
|
|
}
|
|
|
|
if (isChrome && !isMediaRecorderCompatible()) {
|
|
// to support video-only recording on stable
|
|
recorderHints = 'video/vp8';
|
|
}
|
|
|
|
if (typeof MediaRecorder.isTypeSupported === 'function' && recorderHints.mimeType) {
|
|
if (!MediaRecorder.isTypeSupported(recorderHints.mimeType)) {
|
|
if (!config.disableLogs) {
|
|
console.warn('MediaRecorder API seems unable to record mimeType:', recorderHints.mimeType);
|
|
}
|
|
|
|
recorderHints.mimeType = config.type === 'audio' ? 'audio/webm' : 'video/webm';
|
|
}
|
|
}
|
|
|
|
// using MediaRecorder API here
|
|
try {
|
|
mediaRecorder = new MediaRecorder(mediaStream, recorderHints);
|
|
|
|
// reset
|
|
config.mimeType = recorderHints.mimeType;
|
|
} catch (e) {
|
|
// chrome-based fallback
|
|
mediaRecorder = new MediaRecorder(mediaStream);
|
|
}
|
|
|
|
// old hack?
|
|
if (recorderHints.mimeType && !MediaRecorder.isTypeSupported && 'canRecordMimeType' in mediaRecorder && mediaRecorder.canRecordMimeType(recorderHints.mimeType) === false) {
|
|
if (!config.disableLogs) {
|
|
console.warn('MediaRecorder API seems unable to record mimeType:', recorderHints.mimeType);
|
|
}
|
|
}
|
|
|
|
// Dispatching OnDataAvailable Handler
|
|
mediaRecorder.ondataavailable = function(e) {
|
|
if (e.data) {
|
|
allStates.push('ondataavailable: ' + bytesToSize(e.data.size));
|
|
}
|
|
|
|
if (typeof config.timeSlice === 'number') {
|
|
if (e.data && e.data.size) {
|
|
arrayOfBlobs.push(e.data);
|
|
updateTimeStamp();
|
|
|
|
if (typeof config.ondataavailable === 'function') {
|
|
// intervals based blobs
|
|
var blob = config.getNativeBlob ? e.data : new Blob([e.data], {
|
|
type: getMimeType(recorderHints)
|
|
});
|
|
config.ondataavailable(blob);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!e.data || !e.data.size || e.data.size < 100 || self.blob) {
|
|
// make sure that stopRecording always getting fired
|
|
// even if there is invalid data
|
|
if (self.recordingCallback) {
|
|
self.recordingCallback(new Blob([], {
|
|
type: getMimeType(recorderHints)
|
|
}));
|
|
self.recordingCallback = null;
|
|
}
|
|
return;
|
|
}
|
|
|
|
self.blob = config.getNativeBlob ? e.data : new Blob([e.data], {
|
|
type: getMimeType(recorderHints)
|
|
});
|
|
|
|
if (self.recordingCallback) {
|
|
self.recordingCallback(self.blob);
|
|
self.recordingCallback = null;
|
|
}
|
|
};
|
|
|
|
mediaRecorder.onstart = function() {
|
|
allStates.push('started');
|
|
};
|
|
|
|
mediaRecorder.onpause = function() {
|
|
allStates.push('paused');
|
|
};
|
|
|
|
mediaRecorder.onresume = function() {
|
|
allStates.push('resumed');
|
|
};
|
|
|
|
mediaRecorder.onstop = function() {
|
|
allStates.push('stopped');
|
|
};
|
|
|
|
mediaRecorder.onerror = function(error) {
|
|
if (!error) {
|
|
return;
|
|
}
|
|
|
|
if (!error.name) {
|
|
error.name = 'UnknownError';
|
|
}
|
|
|
|
allStates.push('error: ' + error);
|
|
|
|
if (!config.disableLogs) {
|
|
// via: https://w3c.github.io/mediacapture-record/MediaRecorder.html#exception-summary
|
|
if (error.name.toString().toLowerCase().indexOf('invalidstate') !== -1) {
|
|
console.error('The MediaRecorder is not in a state in which the proposed operation is allowed to be executed.', error);
|
|
} else if (error.name.toString().toLowerCase().indexOf('notsupported') !== -1) {
|
|
console.error('MIME type (', recorderHints.mimeType, ') is not supported.', error);
|
|
} else if (error.name.toString().toLowerCase().indexOf('security') !== -1) {
|
|
console.error('MediaRecorder security error', error);
|
|
}
|
|
|
|
// older code below
|
|
else if (error.name === 'OutOfMemory') {
|
|
console.error('The UA has exhaused the available memory. User agents SHOULD provide as much additional information as possible in the message attribute.', error);
|
|
} else if (error.name === 'IllegalStreamModification') {
|
|
console.error('A modification to the stream has occurred that makes it impossible to continue recording. An example would be the addition of a Track while recording is occurring. User agents SHOULD provide as much additional information as possible in the message attribute.', error);
|
|
} else if (error.name === 'OtherRecordingError') {
|
|
console.error('Used for an fatal error other than those listed above. User agents SHOULD provide as much additional information as possible in the message attribute.', error);
|
|
} else if (error.name === 'GenericError') {
|
|
console.error('The UA cannot provide the codec or recording option that has been requested.', error);
|
|
} else {
|
|
console.error('MediaRecorder Error', error);
|
|
}
|
|
}
|
|
|
|
(function(looper) {
|
|
if (!self.manuallyStopped && mediaRecorder && mediaRecorder.state === 'inactive') {
|
|
delete config.timeslice;
|
|
|
|
// 10 minutes, enough?
|
|
mediaRecorder.start(10 * 60 * 1000);
|
|
return;
|
|
}
|
|
|
|
setTimeout(looper, 1000);
|
|
})();
|
|
|
|
if (mediaRecorder.state !== 'inactive' && mediaRecorder.state !== 'stopped') {
|
|
mediaRecorder.stop();
|
|
}
|
|
};
|
|
|
|
if (typeof config.timeSlice === 'number') {
|
|
updateTimeStamp();
|
|
mediaRecorder.start(config.timeSlice);
|
|
} else {
|
|
// default is 60 minutes; enough?
|
|
// use config => {timeSlice: 1000} otherwise
|
|
|
|
mediaRecorder.start(3.6e+6);
|
|
}
|
|
|
|
if (config.initCallback) {
|
|
config.initCallback(); // old code
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @property {Array} timestamps - Array of time stamps
|
|
* @memberof MediaStreamRecorder
|
|
* @example
|
|
* console.log(recorder.timestamps);
|
|
*/
|
|
this.timestamps = [];
|
|
|
|
function updateTimeStamp() {
|
|
self.timestamps.push(new Date().getTime());
|
|
|
|
if (typeof config.onTimeStamp === 'function') {
|
|
config.onTimeStamp(self.timestamps[self.timestamps.length - 1], self.timestamps);
|
|
}
|
|
}
|
|
|
|
function getMimeType(secondObject) {
|
|
if (mediaRecorder && mediaRecorder.mimeType) {
|
|
return mediaRecorder.mimeType;
|
|
}
|
|
|
|
return secondObject.mimeType || 'video/webm';
|
|
}
|
|
|
|
/**
|
|
* This method stops recording MediaStream.
|
|
* @param {function} callback - Callback function, that is used to pass recorded blob back to the callee.
|
|
* @method
|
|
* @memberof MediaStreamRecorder
|
|
* @example
|
|
* recorder.stop(function(blob) {
|
|
* video.src = URL.createObjectURL(blob);
|
|
* });
|
|
*/
|
|
this.stop = function(callback) {
|
|
callback = callback || function() {};
|
|
|
|
self.manuallyStopped = true; // used inside the mediaRecorder.onerror
|
|
|
|
if (!mediaRecorder) {
|
|
return;
|
|
}
|
|
|
|
this.recordingCallback = callback;
|
|
|
|
if (mediaRecorder.state === 'recording') {
|
|
mediaRecorder.stop();
|
|
}
|
|
|
|
if (typeof config.timeSlice === 'number') {
|
|
setTimeout(function() {
|
|
self.blob = new Blob(arrayOfBlobs, {
|
|
type: getMimeType(config)
|
|
});
|
|
|
|
self.recordingCallback(self.blob);
|
|
}, 100);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This method pauses the recording process.
|
|
* @method
|
|
* @memberof MediaStreamRecorder
|
|
* @example
|
|
* recorder.pause();
|
|
*/
|
|
this.pause = function() {
|
|
if (!mediaRecorder) {
|
|
return;
|
|
}
|
|
|
|
if (mediaRecorder.state === 'recording') {
|
|
mediaRecorder.pause();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This method resumes the recording process.
|
|
* @method
|
|
* @memberof MediaStreamRecorder
|
|
* @example
|
|
* recorder.resume();
|
|
*/
|
|
this.resume = function() {
|
|
if (!mediaRecorder) {
|
|
return;
|
|
}
|
|
|
|
if (mediaRecorder.state === 'paused') {
|
|
mediaRecorder.resume();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This method resets currently recorded data.
|
|
* @method
|
|
* @memberof MediaStreamRecorder
|
|
* @example
|
|
* recorder.clearRecordedData();
|
|
*/
|
|
this.clearRecordedData = function() {
|
|
if (mediaRecorder && mediaRecorder.state === 'recording') {
|
|
self.stop(clearRecordedDataCB);
|
|
}
|
|
|
|
clearRecordedDataCB();
|
|
};
|
|
|
|
function clearRecordedDataCB() {
|
|
arrayOfBlobs = [];
|
|
mediaRecorder = null;
|
|
self.timestamps = [];
|
|
}
|
|
|
|
// Reference to "MediaRecorder" object
|
|
var mediaRecorder;
|
|
|
|
/**
|
|
* Access to native MediaRecorder API
|
|
* @method
|
|
* @memberof MediaStreamRecorder
|
|
* @instance
|
|
* @example
|
|
* var internal = recorder.getInternalRecorder();
|
|
* internal.ondataavailable = function() {}; // override
|
|
* internal.stream, internal.onpause, internal.onstop, etc.
|
|
* @returns {Object} Returns internal recording object.
|
|
*/
|
|
this.getInternalRecorder = function() {
|
|
return mediaRecorder;
|
|
};
|
|
|
|
function isMediaStreamActive() {
|
|
if ('active' in mediaStream) {
|
|
if (!mediaStream.active) {
|
|
return false;
|
|
}
|
|
} else if ('ended' in mediaStream) { // old hack
|
|
if (mediaStream.ended) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @property {Blob} blob - Recorded data as "Blob" object.
|
|
* @memberof MediaStreamRecorder
|
|
* @example
|
|
* recorder.stop(function() {
|
|
* var blob = recorder.blob;
|
|
* });
|
|
*/
|
|
this.blob = null;
|
|
|
|
|
|
/**
|
|
* Get MediaRecorder readonly state.
|
|
* @method
|
|
* @memberof MediaStreamRecorder
|
|
* @example
|
|
* var state = recorder.getState();
|
|
* @returns {String} Returns recording state.
|
|
*/
|
|
this.getState = function() {
|
|
if (!mediaRecorder) {
|
|
return 'inactive';
|
|
}
|
|
|
|
return mediaRecorder.state || 'inactive';
|
|
};
|
|
|
|
// list of all recording states
|
|
var allStates = [];
|
|
|
|
/**
|
|
* Get MediaRecorder all recording states.
|
|
* @method
|
|
* @memberof MediaStreamRecorder
|
|
* @example
|
|
* var state = recorder.getAllStates();
|
|
* @returns {Array} Returns all recording states
|
|
*/
|
|
this.getAllStates = function() {
|
|
return allStates;
|
|
};
|
|
|
|
// if any Track within the MediaStream is muted or not enabled at any time,
|
|
// the browser will only record black frames
|
|
// or silence since that is the content produced by the Track
|
|
// so we need to stopRecording as soon as any single track ends.
|
|
if (typeof config.checkForInactiveTracks === 'undefined') {
|
|
config.checkForInactiveTracks = false; // disable to minimize CPU usage
|
|
}
|
|
|
|
var self = this;
|
|
|
|
// this method checks if media stream is stopped
|
|
// or if any track is ended.
|
|
(function looper() {
|
|
if (!mediaRecorder || config.checkForInactiveTracks === false) {
|
|
return;
|
|
}
|
|
|
|
if (isMediaStreamActive() === false) {
|
|
if (!config.disableLogs) {
|
|
console.log('MediaStream seems stopped.');
|
|
}
|
|
self.stop();
|
|
return;
|
|
}
|
|
|
|
setTimeout(looper, 1000); // check every second
|
|
})();
|
|
|
|
// for debugging
|
|
this.name = 'MediaStreamRecorder';
|
|
this.toString = function() {
|
|
return this.name;
|
|
};
|
|
}
|
|
|
|
if (typeof RecordRTC !== 'undefined') {
|
|
RecordRTC.MediaStreamRecorder = MediaStreamRecorder;
|
|
}
|
|
|
|
// source code from: http://typedarray.org/wp-content/projects/WebAudioRecorder/script.js
|
|
// https://github.com/mattdiamond/Recorderjs#license-mit
|
|
// ______________________
|
|
// StereoAudioRecorder.js
|
|
|
|
/**
|
|
* StereoAudioRecorder is a standalone class used by {@link RecordRTC} to bring "stereo" audio-recording in chrome.
|
|
* @summary JavaScript standalone object for stereo audio recording.
|
|
* @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
|
|
* @author {@link https://MuazKhan.com|Muaz Khan}
|
|
* @typedef StereoAudioRecorder
|
|
* @class
|
|
* @example
|
|
* var recorder = new StereoAudioRecorder(MediaStream, {
|
|
* sampleRate: 44100,
|
|
* bufferSize: 4096
|
|
* });
|
|
* recorder.record();
|
|
* recorder.stop(function(blob) {
|
|
* video.src = URL.createObjectURL(blob);
|
|
* });
|
|
* @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
|
|
* @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API.
|
|
* @param {object} config - {sampleRate: 44100, bufferSize: 4096, numberOfAudioChannels: 1, etc.}
|
|
*/
|
|
|
|
function StereoAudioRecorder(mediaStream, config) {
|
|
if (!getTracks(mediaStream, 'audio').length) {
|
|
throw 'Your stream has no audio tracks.';
|
|
}
|
|
|
|
config = config || {};
|
|
|
|
var self = this;
|
|
|
|
// variables
|
|
var leftchannel = [];
|
|
var rightchannel = [];
|
|
var recording = false;
|
|
var recordingLength = 0;
|
|
var jsAudioNode;
|
|
|
|
var numberOfAudioChannels = 2;
|
|
|
|
/**
|
|
* Set sample rates such as 8K or 16K. Reference: http://stackoverflow.com/a/28977136/552182
|
|
* @property {number} desiredSampRate - Desired Bits per sample * 1000
|
|
* @memberof StereoAudioRecorder
|
|
* @instance
|
|
* @example
|
|
* var recorder = StereoAudioRecorder(mediaStream, {
|
|
* desiredSampRate: 16 * 1000 // bits-per-sample * 1000
|
|
* });
|
|
*/
|
|
var desiredSampRate = config.desiredSampRate;
|
|
|
|
// backward compatibility
|
|
if (config.leftChannel === true) {
|
|
numberOfAudioChannels = 1;
|
|
}
|
|
|
|
if (config.numberOfAudioChannels === 1) {
|
|
numberOfAudioChannels = 1;
|
|
}
|
|
|
|
if (!numberOfAudioChannels || numberOfAudioChannels < 1) {
|
|
numberOfAudioChannels = 2;
|
|
}
|
|
|
|
if (!config.disableLogs) {
|
|
console.log('StereoAudioRecorder is set to record number of channels: ' + numberOfAudioChannels);
|
|
}
|
|
|
|
// if any Track within the MediaStream is muted or not enabled at any time,
|
|
// the browser will only record black frames
|
|
// or silence since that is the content produced by the Track
|
|
// so we need to stopRecording as soon as any single track ends.
|
|
if (typeof config.checkForInactiveTracks === 'undefined') {
|
|
config.checkForInactiveTracks = true;
|
|
}
|
|
|
|
function isMediaStreamActive() {
|
|
if (config.checkForInactiveTracks === false) {
|
|
// always return "true"
|
|
return true;
|
|
}
|
|
|
|
if ('active' in mediaStream) {
|
|
if (!mediaStream.active) {
|
|
return false;
|
|
}
|
|
} else if ('ended' in mediaStream) { // old hack
|
|
if (mediaStream.ended) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* This method records MediaStream.
|
|
* @method
|
|
* @memberof StereoAudioRecorder
|
|
* @example
|
|
* recorder.record();
|
|
*/
|
|
this.record = function() {
|
|
if (isMediaStreamActive() === false) {
|
|
throw 'Please make sure MediaStream is active.';
|
|
}
|
|
|
|
resetVariables();
|
|
|
|
isAudioProcessStarted = isPaused = false;
|
|
recording = true;
|
|
|
|
if (typeof config.timeSlice !== 'undefined') {
|
|
looper();
|
|
}
|
|
};
|
|
|
|
function mergeLeftRightBuffers(config, callback) {
|
|
function mergeAudioBuffers(config, cb) {
|
|
var numberOfAudioChannels = config.numberOfAudioChannels;
|
|
|
|
// todo: "slice(0)" --- is it causes loop? Should be removed?
|
|
var leftBuffers = config.leftBuffers.slice(0);
|
|
var rightBuffers = config.rightBuffers.slice(0);
|
|
var sampleRate = config.sampleRate;
|
|
var internalInterleavedLength = config.internalInterleavedLength;
|
|
var desiredSampRate = config.desiredSampRate;
|
|
|
|
if (numberOfAudioChannels === 2) {
|
|
leftBuffers = mergeBuffers(leftBuffers, internalInterleavedLength);
|
|
rightBuffers = mergeBuffers(rightBuffers, internalInterleavedLength);
|
|
|
|
if (desiredSampRate) {
|
|
leftBuffers = interpolateArray(leftBuffers, desiredSampRate, sampleRate);
|
|
rightBuffers = interpolateArray(rightBuffers, desiredSampRate, sampleRate);
|
|
}
|
|
}
|
|
|
|
if (numberOfAudioChannels === 1) {
|
|
leftBuffers = mergeBuffers(leftBuffers, internalInterleavedLength);
|
|
|
|
if (desiredSampRate) {
|
|
leftBuffers = interpolateArray(leftBuffers, desiredSampRate, sampleRate);
|
|
}
|
|
}
|
|
|
|
// set sample rate as desired sample rate
|
|
if (desiredSampRate) {
|
|
sampleRate = desiredSampRate;
|
|
}
|
|
|
|
// for changing the sampling rate, reference:
|
|
// http://stackoverflow.com/a/28977136/552182
|
|
function interpolateArray(data, newSampleRate, oldSampleRate) {
|
|
var fitCount = Math.round(data.length * (newSampleRate / oldSampleRate));
|
|
var newData = [];
|
|
var springFactor = Number((data.length - 1) / (fitCount - 1));
|
|
newData[0] = data[0];
|
|
for (var i = 1; i < fitCount - 1; i++) {
|
|
var tmp = i * springFactor;
|
|
var before = Number(Math.floor(tmp)).toFixed();
|
|
var after = Number(Math.ceil(tmp)).toFixed();
|
|
var atPoint = tmp - before;
|
|
newData[i] = linearInterpolate(data[before], data[after], atPoint);
|
|
}
|
|
newData[fitCount - 1] = data[data.length - 1];
|
|
return newData;
|
|
}
|
|
|
|
function linearInterpolate(before, after, atPoint) {
|
|
return before + (after - before) * atPoint;
|
|
}
|
|
|
|
function mergeBuffers(channelBuffer, rLength) {
|
|
var result = new Float64Array(rLength);
|
|
var offset = 0;
|
|
var lng = channelBuffer.length;
|
|
|
|
for (var i = 0; i < lng; i++) {
|
|
var buffer = channelBuffer[i];
|
|
result.set(buffer, offset);
|
|
offset += buffer.length;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function interleave(leftChannel, rightChannel) {
|
|
var length = leftChannel.length + rightChannel.length;
|
|
|
|
var result = new Float64Array(length);
|
|
|
|
var inputIndex = 0;
|
|
|
|
for (var index = 0; index < length;) {
|
|
result[index++] = leftChannel[inputIndex];
|
|
result[index++] = rightChannel[inputIndex];
|
|
inputIndex++;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function writeUTFBytes(view, offset, string) {
|
|
var lng = string.length;
|
|
for (var i = 0; i < lng; i++) {
|
|
view.setUint8(offset + i, string.charCodeAt(i));
|
|
}
|
|
}
|
|
|
|
// interleave both channels together
|
|
var interleaved;
|
|
|
|
if (numberOfAudioChannels === 2) {
|
|
interleaved = interleave(leftBuffers, rightBuffers);
|
|
}
|
|
|
|
if (numberOfAudioChannels === 1) {
|
|
interleaved = leftBuffers;
|
|
}
|
|
|
|
var interleavedLength = interleaved.length;
|
|
|
|
// create wav file
|
|
var resultingBufferLength = 44 + interleavedLength * 2;
|
|
|
|
var buffer = new ArrayBuffer(resultingBufferLength);
|
|
|
|
var view = new DataView(buffer);
|
|
|
|
// RIFF chunk descriptor/identifier
|
|
writeUTFBytes(view, 0, 'RIFF');
|
|
|
|
// RIFF chunk length
|
|
// changed "44" to "36" via #401
|
|
view.setUint32(4, 36 + interleavedLength * 2, true);
|
|
|
|
// RIFF type
|
|
writeUTFBytes(view, 8, 'WAVE');
|
|
|
|
// format chunk identifier
|
|
// FMT sub-chunk
|
|
writeUTFBytes(view, 12, 'fmt ');
|
|
|
|
// format chunk length
|
|
view.setUint32(16, 16, true);
|
|
|
|
// sample format (raw)
|
|
view.setUint16(20, 1, true);
|
|
|
|
// stereo (2 channels)
|
|
view.setUint16(22, numberOfAudioChannels, true);
|
|
|
|
// sample rate
|
|
view.setUint32(24, sampleRate, true);
|
|
|
|
// byte rate (sample rate * block align)
|
|
view.setUint32(28, sampleRate * numberOfAudioChannels * 2, true);
|
|
|
|
// block align (channel count * bytes per sample)
|
|
view.setUint16(32, numberOfAudioChannels * 2, true);
|
|
|
|
// bits per sample
|
|
view.setUint16(34, 16, true);
|
|
|
|
// data sub-chunk
|
|
// data chunk identifier
|
|
writeUTFBytes(view, 36, 'data');
|
|
|
|
// data chunk length
|
|
view.setUint32(40, interleavedLength * 2, true);
|
|
|
|
// write the PCM samples
|
|
var lng = interleavedLength;
|
|
var index = 44;
|
|
var volume = 1;
|
|
for (var i = 0; i < lng; i++) {
|
|
view.setInt16(index, interleaved[i] * (0x7FFF * volume), true);
|
|
index += 2;
|
|
}
|
|
|
|
if (cb) {
|
|
return cb({
|
|
buffer: buffer,
|
|
view: view
|
|
});
|
|
}
|
|
|
|
postMessage({
|
|
buffer: buffer,
|
|
view: view
|
|
});
|
|
}
|
|
|
|
if (config.noWorker) {
|
|
mergeAudioBuffers(config, function(data) {
|
|
callback(data.buffer, data.view);
|
|
});
|
|
return;
|
|
}
|
|
|
|
|
|
var webWorker = processInWebWorker(mergeAudioBuffers);
|
|
|
|
webWorker.onmessage = function(event) {
|
|
callback(event.data.buffer, event.data.view);
|
|
|
|
// release memory
|
|
URL.revokeObjectURL(webWorker.workerURL);
|
|
|
|
// kill webworker (or Chrome will kill your page after ~25 calls)
|
|
webWorker.terminate();
|
|
};
|
|
|
|
webWorker.postMessage(config);
|
|
}
|
|
|
|
function processInWebWorker(_function) {
|
|
var workerURL = URL.createObjectURL(new Blob([_function.toString(),
|
|
';this.onmessage = function (eee) {' + _function.name + '(eee.data);}'
|
|
], {
|
|
type: 'application/javascript'
|
|
}));
|
|
|
|
var worker = new Worker(workerURL);
|
|
worker.workerURL = workerURL;
|
|
return worker;
|
|
}
|
|
|
|
/**
|
|
* This method stops recording MediaStream.
|
|
* @param {function} callback - Callback function, that is used to pass recorded blob back to the callee.
|
|
* @method
|
|
* @memberof StereoAudioRecorder
|
|
* @example
|
|
* recorder.stop(function(blob) {
|
|
* video.src = URL.createObjectURL(blob);
|
|
* });
|
|
*/
|
|
this.stop = function(callback) {
|
|
callback = callback || function() {};
|
|
|
|
// stop recording
|
|
recording = false;
|
|
|
|
mergeLeftRightBuffers({
|
|
desiredSampRate: desiredSampRate,
|
|
sampleRate: sampleRate,
|
|
numberOfAudioChannels: numberOfAudioChannels,
|
|
internalInterleavedLength: recordingLength,
|
|
leftBuffers: leftchannel,
|
|
rightBuffers: numberOfAudioChannels === 1 ? [] : rightchannel,
|
|
noWorker: config.noWorker
|
|
}, function(buffer, view) {
|
|
/**
|
|
* @property {Blob} blob - The recorded blob object.
|
|
* @memberof StereoAudioRecorder
|
|
* @example
|
|
* recorder.stop(function(){
|
|
* var blob = recorder.blob;
|
|
* });
|
|
*/
|
|
self.blob = new Blob([view], {
|
|
type: 'audio/wav'
|
|
});
|
|
|
|
/**
|
|
* @property {ArrayBuffer} buffer - The recorded buffer object.
|
|
* @memberof StereoAudioRecorder
|
|
* @example
|
|
* recorder.stop(function(){
|
|
* var buffer = recorder.buffer;
|
|
* });
|
|
*/
|
|
self.buffer = new ArrayBuffer(view.buffer.byteLength);
|
|
|
|
/**
|
|
* @property {DataView} view - The recorded data-view object.
|
|
* @memberof StereoAudioRecorder
|
|
* @example
|
|
* recorder.stop(function(){
|
|
* var view = recorder.view;
|
|
* });
|
|
*/
|
|
self.view = view;
|
|
|
|
self.sampleRate = desiredSampRate || sampleRate;
|
|
self.bufferSize = bufferSize;
|
|
|
|
// recorded audio length
|
|
self.length = recordingLength;
|
|
|
|
isAudioProcessStarted = false;
|
|
|
|
if (callback) {
|
|
callback(self.blob);
|
|
}
|
|
});
|
|
};
|
|
|
|
if (typeof RecordRTC.Storage === 'undefined') {
|
|
RecordRTC.Storage = {
|
|
AudioContextConstructor: null,
|
|
AudioContext: window.AudioContext || window.webkitAudioContext
|
|
};
|
|
}
|
|
|
|
if (!RecordRTC.Storage.AudioContextConstructor || RecordRTC.Storage.AudioContextConstructor.state === 'closed') {
|
|
RecordRTC.Storage.AudioContextConstructor = new RecordRTC.Storage.AudioContext();
|
|
}
|
|
|
|
var context = RecordRTC.Storage.AudioContextConstructor;
|
|
|
|
// creates an audio node from the microphone incoming stream
|
|
var audioInput = context.createMediaStreamSource(mediaStream);
|
|
|
|
var legalBufferValues = [0, 256, 512, 1024, 2048, 4096, 8192, 16384];
|
|
|
|
/**
|
|
* From the spec: This value controls how frequently the audioprocess event is
|
|
* dispatched and how many sample-frames need to be processed each call.
|
|
* Lower values for buffer size will result in a lower (better) latency.
|
|
* Higher values will be necessary to avoid audio breakup and glitches
|
|
* The size of the buffer (in sample-frames) which needs to
|
|
* be processed each time onprocessaudio is called.
|
|
* Legal values are (256, 512, 1024, 2048, 4096, 8192, 16384).
|
|
* @property {number} bufferSize - Buffer-size for how frequently the audioprocess event is dispatched.
|
|
* @memberof StereoAudioRecorder
|
|
* @example
|
|
* recorder = new StereoAudioRecorder(mediaStream, {
|
|
* bufferSize: 4096
|
|
* });
|
|
*/
|
|
|
|
// "0" means, let chrome decide the most accurate buffer-size for current platform.
|
|
var bufferSize = typeof config.bufferSize === 'undefined' ? 4096 : config.bufferSize;
|
|
|
|
if (legalBufferValues.indexOf(bufferSize) === -1) {
|
|
if (!config.disableLogs) {
|
|
console.log('Legal values for buffer-size are ' + JSON.stringify(legalBufferValues, null, '\t'));
|
|
}
|
|
}
|
|
|
|
if (context.createJavaScriptNode) {
|
|
jsAudioNode = context.createJavaScriptNode(bufferSize, numberOfAudioChannels, numberOfAudioChannels);
|
|
} else if (context.createScriptProcessor) {
|
|
jsAudioNode = context.createScriptProcessor(bufferSize, numberOfAudioChannels, numberOfAudioChannels);
|
|
} else {
|
|
throw 'WebAudio API has no support on this browser.';
|
|
}
|
|
|
|
// connect the stream to the script processor
|
|
audioInput.connect(jsAudioNode);
|
|
|
|
if (!config.bufferSize) {
|
|
bufferSize = jsAudioNode.bufferSize; // device buffer-size
|
|
}
|
|
|
|
/**
|
|
* The sample rate (in sample-frames per second) at which the
|
|
* AudioContext handles audio. It is assumed that all AudioNodes
|
|
* in the context run at this rate. In making this assumption,
|
|
* sample-rate converters or "varispeed" processors are not supported
|
|
* in real-time processing.
|
|
* The sampleRate parameter describes the sample-rate of the
|
|
* linear PCM audio data in the buffer in sample-frames per second.
|
|
* An implementation must support sample-rates in at least
|
|
* the range 22050 to 96000.
|
|
* @property {number} sampleRate - Buffer-size for how frequently the audioprocess event is dispatched.
|
|
* @memberof StereoAudioRecorder
|
|
* @example
|
|
* recorder = new StereoAudioRecorder(mediaStream, {
|
|
* sampleRate: 44100
|
|
* });
|
|
*/
|
|
var sampleRate = typeof config.sampleRate !== 'undefined' ? config.sampleRate : context.sampleRate || 44100;
|
|
|
|
if (sampleRate < 22050 || sampleRate > 96000) {
|
|
// Ref: http://stackoverflow.com/a/26303918/552182
|
|
if (!config.disableLogs) {
|
|
console.log('sample-rate must be under range 22050 and 96000.');
|
|
}
|
|
}
|
|
|
|
if (!config.disableLogs) {
|
|
if (config.desiredSampRate) {
|
|
console.log('Desired sample-rate: ' + config.desiredSampRate);
|
|
}
|
|
}
|
|
|
|
var isPaused = false;
|
|
/**
|
|
* This method pauses the recording process.
|
|
* @method
|
|
* @memberof StereoAudioRecorder
|
|
* @example
|
|
* recorder.pause();
|
|
*/
|
|
this.pause = function() {
|
|
isPaused = true;
|
|
};
|
|
|
|
/**
|
|
* This method resumes the recording process.
|
|
* @method
|
|
* @memberof StereoAudioRecorder
|
|
* @example
|
|
* recorder.resume();
|
|
*/
|
|
this.resume = function() {
|
|
if (isMediaStreamActive() === false) {
|
|
throw 'Please make sure MediaStream is active.';
|
|
}
|
|
|
|
if (!recording) {
|
|
if (!config.disableLogs) {
|
|
console.log('Seems recording has been restarted.');
|
|
}
|
|
this.record();
|
|
return;
|
|
}
|
|
|
|
isPaused = false;
|
|
};
|
|
|
|
/**
|
|
* This method resets currently recorded data.
|
|
* @method
|
|
* @memberof StereoAudioRecorder
|
|
* @example
|
|
* recorder.clearRecordedData();
|
|
*/
|
|
this.clearRecordedData = function() {
|
|
config.checkForInactiveTracks = false;
|
|
|
|
if (recording) {
|
|
this.stop(clearRecordedDataCB);
|
|
}
|
|
|
|
clearRecordedDataCB();
|
|
};
|
|
|
|
function resetVariables() {
|
|
leftchannel = [];
|
|
rightchannel = [];
|
|
recordingLength = 0;
|
|
isAudioProcessStarted = false;
|
|
recording = false;
|
|
isPaused = false;
|
|
context = null;
|
|
|
|
self.leftchannel = leftchannel;
|
|
self.rightchannel = rightchannel;
|
|
self.numberOfAudioChannels = numberOfAudioChannels;
|
|
self.desiredSampRate = desiredSampRate;
|
|
self.sampleRate = sampleRate;
|
|
self.recordingLength = recordingLength;
|
|
|
|
intervalsBasedBuffers = {
|
|
left: [],
|
|
right: [],
|
|
recordingLength: 0
|
|
};
|
|
}
|
|
|
|
function clearRecordedDataCB() {
|
|
if (jsAudioNode) {
|
|
jsAudioNode.onaudioprocess = null;
|
|
jsAudioNode.disconnect();
|
|
jsAudioNode = null;
|
|
}
|
|
|
|
if (audioInput) {
|
|
audioInput.disconnect();
|
|
audioInput = null;
|
|
}
|
|
|
|
resetVariables();
|
|
}
|
|
|
|
// for debugging
|
|
this.name = 'StereoAudioRecorder';
|
|
this.toString = function() {
|
|
return this.name;
|
|
};
|
|
|
|
var isAudioProcessStarted = false;
|
|
|
|
function onAudioProcessDataAvailable(e) {
|
|
if (isPaused) {
|
|
return;
|
|
}
|
|
|
|
if (isMediaStreamActive() === false) {
|
|
if (!config.disableLogs) {
|
|
console.log('MediaStream seems stopped.');
|
|
}
|
|
jsAudioNode.disconnect();
|
|
recording = false;
|
|
}
|
|
|
|
if (!recording) {
|
|
if (audioInput) {
|
|
audioInput.disconnect();
|
|
audioInput = null;
|
|
}
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* This method is called on "onaudioprocess" event's first invocation.
|
|
* @method {function} onAudioProcessStarted
|
|
* @memberof StereoAudioRecorder
|
|
* @example
|
|
* recorder.onAudioProcessStarted: function() { };
|
|
*/
|
|
if (!isAudioProcessStarted) {
|
|
isAudioProcessStarted = true;
|
|
if (config.onAudioProcessStarted) {
|
|
config.onAudioProcessStarted();
|
|
}
|
|
|
|
if (config.initCallback) {
|
|
config.initCallback();
|
|
}
|
|
}
|
|
|
|
var left = e.inputBuffer.getChannelData(0);
|
|
|
|
// we clone the samples
|
|
var chLeft = new Float32Array(left);
|
|
leftchannel.push(chLeft);
|
|
|
|
if (numberOfAudioChannels === 2) {
|
|
var right = e.inputBuffer.getChannelData(1);
|
|
var chRight = new Float32Array(right);
|
|
rightchannel.push(chRight);
|
|
}
|
|
|
|
recordingLength += bufferSize;
|
|
|
|
// export raw PCM
|
|
self.recordingLength = recordingLength;
|
|
|
|
if (typeof config.timeSlice !== 'undefined') {
|
|
intervalsBasedBuffers.recordingLength += bufferSize;
|
|
intervalsBasedBuffers.left.push(chLeft);
|
|
|
|
if (numberOfAudioChannels === 2) {
|
|
intervalsBasedBuffers.right.push(chRight);
|
|
}
|
|
}
|
|
}
|
|
|
|
jsAudioNode.onaudioprocess = onAudioProcessDataAvailable;
|
|
|
|
// to prevent self audio to be connected with speakers
|
|
if (context.createMediaStreamDestination) {
|
|
jsAudioNode.connect(context.createMediaStreamDestination());
|
|
} else {
|
|
jsAudioNode.connect(context.destination);
|
|
}
|
|
|
|
// export raw PCM
|
|
this.leftchannel = leftchannel;
|
|
this.rightchannel = rightchannel;
|
|
this.numberOfAudioChannels = numberOfAudioChannels;
|
|
this.desiredSampRate = desiredSampRate;
|
|
this.sampleRate = sampleRate;
|
|
self.recordingLength = recordingLength;
|
|
|
|
// helper for intervals based blobs
|
|
var intervalsBasedBuffers = {
|
|
left: [],
|
|
right: [],
|
|
recordingLength: 0
|
|
};
|
|
|
|
// this looper is used to support intervals based blobs (via timeSlice+ondataavailable)
|
|
function looper() {
|
|
if (!recording || typeof config.ondataavailable !== 'function' || typeof config.timeSlice === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
if (intervalsBasedBuffers.left.length) {
|
|
mergeLeftRightBuffers({
|
|
desiredSampRate: desiredSampRate,
|
|
sampleRate: sampleRate,
|
|
numberOfAudioChannels: numberOfAudioChannels,
|
|
internalInterleavedLength: intervalsBasedBuffers.recordingLength,
|
|
leftBuffers: intervalsBasedBuffers.left,
|
|
rightBuffers: numberOfAudioChannels === 1 ? [] : intervalsBasedBuffers.right
|
|
}, function(buffer, view) {
|
|
var blob = new Blob([view], {
|
|
type: 'audio/wav'
|
|
});
|
|
config.ondataavailable(blob);
|
|
|
|
setTimeout(looper, config.timeSlice);
|
|
});
|
|
|
|
intervalsBasedBuffers = {
|
|
left: [],
|
|
right: [],
|
|
recordingLength: 0
|
|
};
|
|
} else {
|
|
setTimeout(looper, config.timeSlice);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (typeof RecordRTC !== 'undefined') {
|
|
RecordRTC.StereoAudioRecorder = StereoAudioRecorder;
|
|
}
|
|
|
|
// _________________
|
|
// CanvasRecorder.js
|
|
|
|
/**
|
|
* CanvasRecorder is a standalone class used by {@link RecordRTC} to bring HTML5-Canvas recording into video WebM. It uses HTML2Canvas library and runs top over {@link Whammy}.
|
|
* @summary HTML2Canvas recording into video WebM.
|
|
* @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
|
|
* @author {@link https://MuazKhan.com|Muaz Khan}
|
|
* @typedef CanvasRecorder
|
|
* @class
|
|
* @example
|
|
* var recorder = new CanvasRecorder(htmlElement, { disableLogs: true, useWhammyRecorder: true });
|
|
* recorder.record();
|
|
* recorder.stop(function(blob) {
|
|
* video.src = URL.createObjectURL(blob);
|
|
* });
|
|
* @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
|
|
* @param {HTMLElement} htmlElement - querySelector/getElementById/getElementsByTagName[0]/etc.
|
|
* @param {object} config - {disableLogs:true, initCallback: function}
|
|
*/
|
|
|
|
function CanvasRecorder(htmlElement, config) {
|
|
if (typeof html2canvas === 'undefined') {
|
|
throw 'Please link: https://www.webrtc-experiment.com/screenshot.js';
|
|
}
|
|
|
|
config = config || {};
|
|
if (!config.frameInterval) {
|
|
config.frameInterval = 10;
|
|
}
|
|
|
|
// via DetectRTC.js
|
|
var isCanvasSupportsStreamCapturing = false;
|
|
['captureStream', 'mozCaptureStream', 'webkitCaptureStream'].forEach(function(item) {
|
|
if (item in document.createElement('canvas')) {
|
|
isCanvasSupportsStreamCapturing = true;
|
|
}
|
|
});
|
|
|
|
var _isChrome = (!!window.webkitRTCPeerConnection || !!window.webkitGetUserMedia) && !!window.chrome;
|
|
|
|
var chromeVersion = 50;
|
|
var matchArray = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
|
|
if (_isChrome && matchArray && matchArray[2]) {
|
|
chromeVersion = parseInt(matchArray[2], 10);
|
|
}
|
|
|
|
if (_isChrome && chromeVersion < 52) {
|
|
isCanvasSupportsStreamCapturing = false;
|
|
}
|
|
|
|
if (config.useWhammyRecorder) {
|
|
isCanvasSupportsStreamCapturing = false;
|
|
}
|
|
|
|
var globalCanvas, mediaStreamRecorder;
|
|
|
|
if (isCanvasSupportsStreamCapturing) {
|
|
if (!config.disableLogs) {
|
|
console.log('Your browser supports both MediRecorder API and canvas.captureStream!');
|
|
}
|
|
|
|
if (htmlElement instanceof HTMLCanvasElement) {
|
|
globalCanvas = htmlElement;
|
|
} else if (htmlElement instanceof CanvasRenderingContext2D) {
|
|
globalCanvas = htmlElement.canvas;
|
|
} else {
|
|
throw 'Please pass either HTMLCanvasElement or CanvasRenderingContext2D.';
|
|
}
|
|
} else if (!!navigator.mozGetUserMedia) {
|
|
if (!config.disableLogs) {
|
|
console.error('Canvas recording is NOT supported in Firefox.');
|
|
}
|
|
}
|
|
|
|
var isRecording;
|
|
|
|
/**
|
|
* This method records Canvas.
|
|
* @method
|
|
* @memberof CanvasRecorder
|
|
* @example
|
|
* recorder.record();
|
|
*/
|
|
this.record = function() {
|
|
isRecording = true;
|
|
|
|
if (isCanvasSupportsStreamCapturing && !config.useWhammyRecorder) {
|
|
// CanvasCaptureMediaStream
|
|
var canvasMediaStream;
|
|
if ('captureStream' in globalCanvas) {
|
|
canvasMediaStream = globalCanvas.captureStream(25); // 25 FPS
|
|
} else if ('mozCaptureStream' in globalCanvas) {
|
|
canvasMediaStream = globalCanvas.mozCaptureStream(25);
|
|
} else if ('webkitCaptureStream' in globalCanvas) {
|
|
canvasMediaStream = globalCanvas.webkitCaptureStream(25);
|
|
}
|
|
|
|
try {
|
|
var mdStream = new MediaStream();
|
|
mdStream.addTrack(getTracks(canvasMediaStream, 'video')[0]);
|
|
canvasMediaStream = mdStream;
|
|
} catch (e) {}
|
|
|
|
if (!canvasMediaStream) {
|
|
throw 'captureStream API are NOT available.';
|
|
}
|
|
|
|
// Note: Jan 18, 2016 status is that,
|
|
// Firefox MediaRecorder API can't record CanvasCaptureMediaStream object.
|
|
mediaStreamRecorder = new MediaStreamRecorder(canvasMediaStream, {
|
|
mimeType: config.mimeType || 'video/webm'
|
|
});
|
|
mediaStreamRecorder.record();
|
|
} else {
|
|
whammy.frames = [];
|
|
lastTime = new Date().getTime();
|
|
drawCanvasFrame();
|
|
}
|
|
|
|
if (config.initCallback) {
|
|
config.initCallback();
|
|
}
|
|
};
|
|
|
|
this.getWebPImages = function(callback) {
|
|
if (htmlElement.nodeName.toLowerCase() !== 'canvas') {
|
|
callback();
|
|
return;
|
|
}
|
|
|
|
var framesLength = whammy.frames.length;
|
|
whammy.frames.forEach(function(frame, idx) {
|
|
var framesRemaining = framesLength - idx;
|
|
if (!config.disableLogs) {
|
|
console.log(framesRemaining + '/' + framesLength + ' frames remaining');
|
|
}
|
|
|
|
if (config.onEncodingCallback) {
|
|
config.onEncodingCallback(framesRemaining, framesLength);
|
|
}
|
|
|
|
var webp = frame.image.toDataURL('image/webp', 1);
|
|
whammy.frames[idx].image = webp;
|
|
});
|
|
|
|
if (!config.disableLogs) {
|
|
console.log('Generating WebM');
|
|
}
|
|
|
|
callback();
|
|
};
|
|
|
|
/**
|
|
* This method stops recording Canvas.
|
|
* @param {function} callback - Callback function, that is used to pass recorded blob back to the callee.
|
|
* @method
|
|
* @memberof CanvasRecorder
|
|
* @example
|
|
* recorder.stop(function(blob) {
|
|
* video.src = URL.createObjectURL(blob);
|
|
* });
|
|
*/
|
|
this.stop = function(callback) {
|
|
isRecording = false;
|
|
|
|
var that = this;
|
|
|
|
if (isCanvasSupportsStreamCapturing && mediaStreamRecorder) {
|
|
mediaStreamRecorder.stop(callback);
|
|
return;
|
|
}
|
|
|
|
this.getWebPImages(function() {
|
|
/**
|
|
* @property {Blob} blob - Recorded frames in video/webm blob.
|
|
* @memberof CanvasRecorder
|
|
* @example
|
|
* recorder.stop(function() {
|
|
* var blob = recorder.blob;
|
|
* });
|
|
*/
|
|
whammy.compile(function(blob) {
|
|
if (!config.disableLogs) {
|
|
console.log('Recording finished!');
|
|
}
|
|
|
|
that.blob = blob;
|
|
|
|
if (that.blob.forEach) {
|
|
that.blob = new Blob([], {
|
|
type: 'video/webm'
|
|
});
|
|
}
|
|
|
|
if (callback) {
|
|
callback(that.blob);
|
|
}
|
|
|
|
whammy.frames = [];
|
|
});
|
|
});
|
|
};
|
|
|
|
var isPausedRecording = false;
|
|
|
|
/**
|
|
* This method pauses the recording process.
|
|
* @method
|
|
* @memberof CanvasRecorder
|
|
* @example
|
|
* recorder.pause();
|
|
*/
|
|
this.pause = function() {
|
|
isPausedRecording = true;
|
|
|
|
if (mediaStreamRecorder instanceof MediaStreamRecorder) {
|
|
mediaStreamRecorder.pause();
|
|
return;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This method resumes the recording process.
|
|
* @method
|
|
* @memberof CanvasRecorder
|
|
* @example
|
|
* recorder.resume();
|
|
*/
|
|
this.resume = function() {
|
|
isPausedRecording = false;
|
|
|
|
if (mediaStreamRecorder instanceof MediaStreamRecorder) {
|
|
mediaStreamRecorder.resume();
|
|
return;
|
|
}
|
|
|
|
if (!isRecording) {
|
|
this.record();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This method resets currently recorded data.
|
|
* @method
|
|
* @memberof CanvasRecorder
|
|
* @example
|
|
* recorder.clearRecordedData();
|
|
*/
|
|
this.clearRecordedData = function() {
|
|
if (isRecording) {
|
|
this.stop(clearRecordedDataCB);
|
|
}
|
|
clearRecordedDataCB();
|
|
};
|
|
|
|
function clearRecordedDataCB() {
|
|
whammy.frames = [];
|
|
isRecording = false;
|
|
isPausedRecording = false;
|
|
}
|
|
|
|
// for debugging
|
|
this.name = 'CanvasRecorder';
|
|
this.toString = function() {
|
|
return this.name;
|
|
};
|
|
|
|
function cloneCanvas() {
|
|
//create a new canvas
|
|
var newCanvas = document.createElement('canvas');
|
|
var context = newCanvas.getContext('2d');
|
|
|
|
//set dimensions
|
|
newCanvas.width = htmlElement.width;
|
|
newCanvas.height = htmlElement.height;
|
|
|
|
//apply the old canvas to the new one
|
|
context.drawImage(htmlElement, 0, 0);
|
|
|
|
//return the new canvas
|
|
return newCanvas;
|
|
}
|
|
|
|
function drawCanvasFrame() {
|
|
if (isPausedRecording) {
|
|
lastTime = new Date().getTime();
|
|
return setTimeout(drawCanvasFrame, 500);
|
|
}
|
|
|
|
if (htmlElement.nodeName.toLowerCase() === 'canvas') {
|
|
var duration = new Date().getTime() - lastTime;
|
|
// via #206, by Jack i.e. @Seymourr
|
|
lastTime = new Date().getTime();
|
|
|
|
whammy.frames.push({
|
|
image: cloneCanvas(),
|
|
duration: duration
|
|
});
|
|
|
|
if (isRecording) {
|
|
setTimeout(drawCanvasFrame, config.frameInterval);
|
|
}
|
|
return;
|
|
}
|
|
|
|
html2canvas(htmlElement, {
|
|
grabMouse: typeof config.showMousePointer === 'undefined' || config.showMousePointer,
|
|
onrendered: function(canvas) {
|
|
var duration = new Date().getTime() - lastTime;
|
|
if (!duration) {
|
|
return setTimeout(drawCanvasFrame, config.frameInterval);
|
|
}
|
|
|
|
// via #206, by Jack i.e. @Seymourr
|
|
lastTime = new Date().getTime();
|
|
|
|
whammy.frames.push({
|
|
image: canvas.toDataURL('image/webp', 1),
|
|
duration: duration
|
|
});
|
|
|
|
if (isRecording) {
|
|
setTimeout(drawCanvasFrame, config.frameInterval);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
var lastTime = new Date().getTime();
|
|
|
|
var whammy = new Whammy.Video(100);
|
|
}
|
|
|
|
if (typeof RecordRTC !== 'undefined') {
|
|
RecordRTC.CanvasRecorder = CanvasRecorder;
|
|
}
|
|
|
|
// _________________
|
|
// WhammyRecorder.js
|
|
|
|
/**
|
|
* WhammyRecorder is a standalone class used by {@link RecordRTC} to bring video recording in Chrome. It runs top over {@link Whammy}.
|
|
* @summary Video recording feature in Chrome.
|
|
* @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
|
|
* @author {@link https://MuazKhan.com|Muaz Khan}
|
|
* @typedef WhammyRecorder
|
|
* @class
|
|
* @example
|
|
* var recorder = new WhammyRecorder(mediaStream);
|
|
* recorder.record();
|
|
* recorder.stop(function(blob) {
|
|
* video.src = URL.createObjectURL(blob);
|
|
* });
|
|
* @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
|
|
* @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API.
|
|
* @param {object} config - {disableLogs: true, initCallback: function, video: HTMLVideoElement, etc.}
|
|
*/
|
|
|
|
function WhammyRecorder(mediaStream, config) {
|
|
|
|
config = config || {};
|
|
|
|
if (!config.frameInterval) {
|
|
config.frameInterval = 10;
|
|
}
|
|
|
|
if (!config.disableLogs) {
|
|
console.log('Using frames-interval:', config.frameInterval);
|
|
}
|
|
|
|
/**
|
|
* This method records video.
|
|
* @method
|
|
* @memberof WhammyRecorder
|
|
* @example
|
|
* recorder.record();
|
|
*/
|
|
this.record = function() {
|
|
if (!config.width) {
|
|
config.width = 320;
|
|
}
|
|
|
|
if (!config.height) {
|
|
config.height = 240;
|
|
}
|
|
|
|
if (!config.video) {
|
|
config.video = {
|
|
width: config.width,
|
|
height: config.height
|
|
};
|
|
}
|
|
|
|
if (!config.canvas) {
|
|
config.canvas = {
|
|
width: config.width,
|
|
height: config.height
|
|
};
|
|
}
|
|
|
|
canvas.width = config.canvas.width || 320;
|
|
canvas.height = config.canvas.height || 240;
|
|
|
|
context = canvas.getContext('2d');
|
|
|
|
// setting defaults
|
|
if (config.video && config.video instanceof HTMLVideoElement) {
|
|
video = config.video.cloneNode();
|
|
|
|
if (config.initCallback) {
|
|
config.initCallback();
|
|
}
|
|
} else {
|
|
video = document.createElement('video');
|
|
|
|
setSrcObject(mediaStream, video);
|
|
|
|
video.onloadedmetadata = function() { // "onloadedmetadata" may NOT work in FF?
|
|
if (config.initCallback) {
|
|
config.initCallback();
|
|
}
|
|
};
|
|
|
|
video.width = config.video.width;
|
|
video.height = config.video.height;
|
|
}
|
|
|
|
video.muted = true;
|
|
video.play();
|
|
|
|
lastTime = new Date().getTime();
|
|
whammy = new Whammy.Video();
|
|
|
|
if (!config.disableLogs) {
|
|
console.log('canvas resolutions', canvas.width, '*', canvas.height);
|
|
console.log('video width/height', video.width || canvas.width, '*', video.height || canvas.height);
|
|
}
|
|
|
|
drawFrames(config.frameInterval);
|
|
};
|
|
|
|
/**
|
|
* Draw and push frames to Whammy
|
|
* @param {integer} frameInterval - set minimum interval (in milliseconds) between each time we push a frame to Whammy
|
|
*/
|
|
function drawFrames(frameInterval) {
|
|
frameInterval = typeof frameInterval !== 'undefined' ? frameInterval : 10;
|
|
|
|
var duration = new Date().getTime() - lastTime;
|
|
if (!duration) {
|
|
return setTimeout(drawFrames, frameInterval, frameInterval);
|
|
}
|
|
|
|
if (isPausedRecording) {
|
|
lastTime = new Date().getTime();
|
|
return setTimeout(drawFrames, 100);
|
|
}
|
|
|
|
// via #206, by Jack i.e. @Seymourr
|
|
lastTime = new Date().getTime();
|
|
|
|
if (video.paused) {
|
|
// via: https://github.com/muaz-khan/WebRTC-Experiment/pull/316
|
|
// Tweak for Android Chrome
|
|
video.play();
|
|
}
|
|
|
|
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
whammy.frames.push({
|
|
duration: duration,
|
|
image: canvas.toDataURL('image/webp')
|
|
});
|
|
|
|
if (!isStopDrawing) {
|
|
setTimeout(drawFrames, frameInterval, frameInterval);
|
|
}
|
|
}
|
|
|
|
function asyncLoop(o) {
|
|
var i = -1,
|
|
length = o.length;
|
|
|
|
(function loop() {
|
|
i++;
|
|
if (i === length) {
|
|
o.callback();
|
|
return;
|
|
}
|
|
|
|
// "setTimeout" added by Jim McLeod
|
|
setTimeout(function() {
|
|
o.functionToLoop(loop, i);
|
|
}, 1);
|
|
})();
|
|
}
|
|
|
|
|
|
/**
|
|
* remove black frames from the beginning to the specified frame
|
|
* @param {Array} _frames - array of frames to be checked
|
|
* @param {number} _framesToCheck - number of frame until check will be executed (-1 - will drop all frames until frame not matched will be found)
|
|
* @param {number} _pixTolerance - 0 - very strict (only black pixel color) ; 1 - all
|
|
* @param {number} _frameTolerance - 0 - very strict (only black frame color) ; 1 - all
|
|
* @returns {Array} - array of frames
|
|
*/
|
|
// pull#293 by @volodalexey
|
|
function dropBlackFrames(_frames, _framesToCheck, _pixTolerance, _frameTolerance, callback) {
|
|
var localCanvas = document.createElement('canvas');
|
|
localCanvas.width = canvas.width;
|
|
localCanvas.height = canvas.height;
|
|
var context2d = localCanvas.getContext('2d');
|
|
var resultFrames = [];
|
|
|
|
var checkUntilNotBlack = _framesToCheck === -1;
|
|
var endCheckFrame = (_framesToCheck && _framesToCheck > 0 && _framesToCheck <= _frames.length) ?
|
|
_framesToCheck : _frames.length;
|
|
var sampleColor = {
|
|
r: 0,
|
|
g: 0,
|
|
b: 0
|
|
};
|
|
var maxColorDifference = Math.sqrt(
|
|
Math.pow(255, 2) +
|
|
Math.pow(255, 2) +
|
|
Math.pow(255, 2)
|
|
);
|
|
var pixTolerance = _pixTolerance && _pixTolerance >= 0 && _pixTolerance <= 1 ? _pixTolerance : 0;
|
|
var frameTolerance = _frameTolerance && _frameTolerance >= 0 && _frameTolerance <= 1 ? _frameTolerance : 0;
|
|
var doNotCheckNext = false;
|
|
|
|
asyncLoop({
|
|
length: endCheckFrame,
|
|
functionToLoop: function(loop, f) {
|
|
var matchPixCount, endPixCheck, maxPixCount;
|
|
|
|
var finishImage = function() {
|
|
if (!doNotCheckNext && maxPixCount - matchPixCount <= maxPixCount * frameTolerance) ; else {
|
|
// console.log('frame is passed : ' + f);
|
|
if (checkUntilNotBlack) {
|
|
doNotCheckNext = true;
|
|
}
|
|
resultFrames.push(_frames[f]);
|
|
}
|
|
loop();
|
|
};
|
|
|
|
if (!doNotCheckNext) {
|
|
var image = new Image();
|
|
image.onload = function() {
|
|
context2d.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
var imageData = context2d.getImageData(0, 0, canvas.width, canvas.height);
|
|
matchPixCount = 0;
|
|
endPixCheck = imageData.data.length;
|
|
maxPixCount = imageData.data.length / 4;
|
|
|
|
for (var pix = 0; pix < endPixCheck; pix += 4) {
|
|
var currentColor = {
|
|
r: imageData.data[pix],
|
|
g: imageData.data[pix + 1],
|
|
b: imageData.data[pix + 2]
|
|
};
|
|
var colorDifference = Math.sqrt(
|
|
Math.pow(currentColor.r - sampleColor.r, 2) +
|
|
Math.pow(currentColor.g - sampleColor.g, 2) +
|
|
Math.pow(currentColor.b - sampleColor.b, 2)
|
|
);
|
|
// difference in color it is difference in color vectors (r1,g1,b1) <=> (r2,g2,b2)
|
|
if (colorDifference <= maxColorDifference * pixTolerance) {
|
|
matchPixCount++;
|
|
}
|
|
}
|
|
finishImage();
|
|
};
|
|
image.src = _frames[f].image;
|
|
} else {
|
|
finishImage();
|
|
}
|
|
},
|
|
callback: function() {
|
|
resultFrames = resultFrames.concat(_frames.slice(endCheckFrame));
|
|
|
|
if (resultFrames.length <= 0) {
|
|
// at least one last frame should be available for next manipulation
|
|
// if total duration of all frames will be < 1000 than ffmpeg doesn't work well...
|
|
resultFrames.push(_frames[_frames.length - 1]);
|
|
}
|
|
callback(resultFrames);
|
|
}
|
|
});
|
|
}
|
|
|
|
var isStopDrawing = false;
|
|
|
|
/**
|
|
* This method stops recording video.
|
|
* @param {function} callback - Callback function, that is used to pass recorded blob back to the callee.
|
|
* @method
|
|
* @memberof WhammyRecorder
|
|
* @example
|
|
* recorder.stop(function(blob) {
|
|
* video.src = URL.createObjectURL(blob);
|
|
* });
|
|
*/
|
|
this.stop = function(callback) {
|
|
callback = callback || function() {};
|
|
|
|
isStopDrawing = true;
|
|
|
|
var _this = this;
|
|
// analyse of all frames takes some time!
|
|
setTimeout(function() {
|
|
// e.g. dropBlackFrames(frames, 10, 1, 1) - will cut all 10 frames
|
|
// e.g. dropBlackFrames(frames, 10, 0.5, 0.5) - will analyse 10 frames
|
|
// e.g. dropBlackFrames(frames, 10) === dropBlackFrames(frames, 10, 0, 0) - will analyse 10 frames with strict black color
|
|
dropBlackFrames(whammy.frames, -1, null, null, function(frames) {
|
|
whammy.frames = frames;
|
|
|
|
// to display advertisement images!
|
|
if (config.advertisement && config.advertisement.length) {
|
|
whammy.frames = config.advertisement.concat(whammy.frames);
|
|
}
|
|
|
|
/**
|
|
* @property {Blob} blob - Recorded frames in video/webm blob.
|
|
* @memberof WhammyRecorder
|
|
* @example
|
|
* recorder.stop(function() {
|
|
* var blob = recorder.blob;
|
|
* });
|
|
*/
|
|
whammy.compile(function(blob) {
|
|
_this.blob = blob;
|
|
|
|
if (_this.blob.forEach) {
|
|
_this.blob = new Blob([], {
|
|
type: 'video/webm'
|
|
});
|
|
}
|
|
|
|
if (callback) {
|
|
callback(_this.blob);
|
|
}
|
|
});
|
|
});
|
|
}, 10);
|
|
};
|
|
|
|
var isPausedRecording = false;
|
|
|
|
/**
|
|
* This method pauses the recording process.
|
|
* @method
|
|
* @memberof WhammyRecorder
|
|
* @example
|
|
* recorder.pause();
|
|
*/
|
|
this.pause = function() {
|
|
isPausedRecording = true;
|
|
};
|
|
|
|
/**
|
|
* This method resumes the recording process.
|
|
* @method
|
|
* @memberof WhammyRecorder
|
|
* @example
|
|
* recorder.resume();
|
|
*/
|
|
this.resume = function() {
|
|
isPausedRecording = false;
|
|
|
|
if (isStopDrawing) {
|
|
this.record();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This method resets currently recorded data.
|
|
* @method
|
|
* @memberof WhammyRecorder
|
|
* @example
|
|
* recorder.clearRecordedData();
|
|
*/
|
|
this.clearRecordedData = function() {
|
|
if (!isStopDrawing) {
|
|
this.stop(clearRecordedDataCB);
|
|
}
|
|
clearRecordedDataCB();
|
|
};
|
|
|
|
function clearRecordedDataCB() {
|
|
whammy.frames = [];
|
|
isStopDrawing = true;
|
|
isPausedRecording = false;
|
|
}
|
|
|
|
// for debugging
|
|
this.name = 'WhammyRecorder';
|
|
this.toString = function() {
|
|
return this.name;
|
|
};
|
|
|
|
var canvas = document.createElement('canvas');
|
|
var context = canvas.getContext('2d');
|
|
|
|
var video;
|
|
var lastTime;
|
|
var whammy;
|
|
}
|
|
|
|
if (typeof RecordRTC !== 'undefined') {
|
|
RecordRTC.WhammyRecorder = WhammyRecorder;
|
|
}
|
|
|
|
// https://github.com/antimatter15/whammy/blob/master/LICENSE
|
|
// _________
|
|
// Whammy.js
|
|
|
|
// todo: Firefox now supports webp for webm containers!
|
|
// their MediaRecorder implementation works well!
|
|
// should we provide an option to record via Whammy.js or MediaRecorder API is a better solution?
|
|
|
|
/**
|
|
* Whammy is a standalone class used by {@link RecordRTC} to bring video recording in Chrome. It is written by {@link https://github.com/antimatter15|antimatter15}
|
|
* @summary A real time javascript webm encoder based on a canvas hack.
|
|
* @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
|
|
* @author {@link https://MuazKhan.com|Muaz Khan}
|
|
* @typedef Whammy
|
|
* @class
|
|
* @example
|
|
* var recorder = new Whammy().Video(15);
|
|
* recorder.add(context || canvas || dataURL);
|
|
* var output = recorder.compile();
|
|
* @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
|
|
*/
|
|
|
|
var Whammy = (function() {
|
|
// a more abstract-ish API
|
|
|
|
function WhammyVideo(duration) {
|
|
this.frames = [];
|
|
this.duration = duration || 1;
|
|
this.quality = 0.8;
|
|
}
|
|
|
|
/**
|
|
* Pass Canvas or Context or image/webp(string) to {@link Whammy} encoder.
|
|
* @method
|
|
* @memberof Whammy
|
|
* @example
|
|
* recorder = new Whammy().Video(0.8, 100);
|
|
* recorder.add(canvas || context || 'image/webp');
|
|
* @param {string} frame - Canvas || Context || image/webp
|
|
* @param {number} duration - Stick a duration (in milliseconds)
|
|
*/
|
|
WhammyVideo.prototype.add = function(frame, duration) {
|
|
if ('canvas' in frame) { //CanvasRenderingContext2D
|
|
frame = frame.canvas;
|
|
}
|
|
|
|
if ('toDataURL' in frame) {
|
|
frame = frame.toDataURL('image/webp', this.quality);
|
|
}
|
|
|
|
if (!(/^data:image\/webp;base64,/ig).test(frame)) {
|
|
throw 'Input must be formatted properly as a base64 encoded DataURI of type image/webp';
|
|
}
|
|
this.frames.push({
|
|
image: frame,
|
|
duration: duration || this.duration
|
|
});
|
|
};
|
|
|
|
function processInWebWorker(_function) {
|
|
var blob = URL.createObjectURL(new Blob([_function.toString(),
|
|
'this.onmessage = function (eee) {' + _function.name + '(eee.data);}'
|
|
], {
|
|
type: 'application/javascript'
|
|
}));
|
|
|
|
var worker = new Worker(blob);
|
|
URL.revokeObjectURL(blob);
|
|
return worker;
|
|
}
|
|
|
|
function whammyInWebWorker(frames) {
|
|
function ArrayToWebM(frames) {
|
|
var info = checkFrames(frames);
|
|
if (!info) {
|
|
return [];
|
|
}
|
|
|
|
var clusterMaxDuration = 30000;
|
|
|
|
var EBML = [{
|
|
'id': 0x1a45dfa3, // EBML
|
|
'data': [{
|
|
'data': 1,
|
|
'id': 0x4286 // EBMLVersion
|
|
}, {
|
|
'data': 1,
|
|
'id': 0x42f7 // EBMLReadVersion
|
|
}, {
|
|
'data': 4,
|
|
'id': 0x42f2 // EBMLMaxIDLength
|
|
}, {
|
|
'data': 8,
|
|
'id': 0x42f3 // EBMLMaxSizeLength
|
|
}, {
|
|
'data': 'webm',
|
|
'id': 0x4282 // DocType
|
|
}, {
|
|
'data': 2,
|
|
'id': 0x4287 // DocTypeVersion
|
|
}, {
|
|
'data': 2,
|
|
'id': 0x4285 // DocTypeReadVersion
|
|
}]
|
|
}, {
|
|
'id': 0x18538067, // Segment
|
|
'data': [{
|
|
'id': 0x1549a966, // Info
|
|
'data': [{
|
|
'data': 1e6, //do things in millisecs (num of nanosecs for duration scale)
|
|
'id': 0x2ad7b1 // TimecodeScale
|
|
}, {
|
|
'data': 'whammy',
|
|
'id': 0x4d80 // MuxingApp
|
|
}, {
|
|
'data': 'whammy',
|
|
'id': 0x5741 // WritingApp
|
|
}, {
|
|
'data': doubleToString(info.duration),
|
|
'id': 0x4489 // Duration
|
|
}]
|
|
}, {
|
|
'id': 0x1654ae6b, // Tracks
|
|
'data': [{
|
|
'id': 0xae, // TrackEntry
|
|
'data': [{
|
|
'data': 1,
|
|
'id': 0xd7 // TrackNumber
|
|
}, {
|
|
'data': 1,
|
|
'id': 0x73c5 // TrackUID
|
|
}, {
|
|
'data': 0,
|
|
'id': 0x9c // FlagLacing
|
|
}, {
|
|
'data': 'und',
|
|
'id': 0x22b59c // Language
|
|
}, {
|
|
'data': 'V_VP8',
|
|
'id': 0x86 // CodecID
|
|
}, {
|
|
'data': 'VP8',
|
|
'id': 0x258688 // CodecName
|
|
}, {
|
|
'data': 1,
|
|
'id': 0x83 // TrackType
|
|
}, {
|
|
'id': 0xe0, // Video
|
|
'data': [{
|
|
'data': info.width,
|
|
'id': 0xb0 // PixelWidth
|
|
}, {
|
|
'data': info.height,
|
|
'id': 0xba // PixelHeight
|
|
}]
|
|
}]
|
|
}]
|
|
}]
|
|
}];
|
|
|
|
//Generate clusters (max duration)
|
|
var frameNumber = 0;
|
|
var clusterTimecode = 0;
|
|
while (frameNumber < frames.length) {
|
|
|
|
var clusterFrames = [];
|
|
var clusterDuration = 0;
|
|
do {
|
|
clusterFrames.push(frames[frameNumber]);
|
|
clusterDuration += frames[frameNumber].duration;
|
|
frameNumber++;
|
|
} while (frameNumber < frames.length && clusterDuration < clusterMaxDuration);
|
|
|
|
var clusterCounter = 0;
|
|
var cluster = {
|
|
'id': 0x1f43b675, // Cluster
|
|
'data': getClusterData(clusterTimecode, clusterCounter, clusterFrames)
|
|
}; //Add cluster to segment
|
|
EBML[1].data.push(cluster);
|
|
clusterTimecode += clusterDuration;
|
|
}
|
|
|
|
return generateEBML(EBML);
|
|
}
|
|
|
|
function getClusterData(clusterTimecode, clusterCounter, clusterFrames) {
|
|
return [{
|
|
'data': clusterTimecode,
|
|
'id': 0xe7 // Timecode
|
|
}].concat(clusterFrames.map(function(webp) {
|
|
var block = makeSimpleBlock({
|
|
discardable: 0,
|
|
frame: webp.data.slice(4),
|
|
invisible: 0,
|
|
keyframe: 1,
|
|
lacing: 0,
|
|
trackNum: 1,
|
|
timecode: Math.round(clusterCounter)
|
|
});
|
|
clusterCounter += webp.duration;
|
|
return {
|
|
data: block,
|
|
id: 0xa3
|
|
};
|
|
}));
|
|
}
|
|
|
|
// sums the lengths of all the frames and gets the duration
|
|
|
|
function checkFrames(frames) {
|
|
if (!frames[0]) {
|
|
postMessage({
|
|
error: 'Something went wrong. Maybe WebP format is not supported in the current browser.'
|
|
});
|
|
return;
|
|
}
|
|
|
|
var width = frames[0].width,
|
|
height = frames[0].height,
|
|
duration = frames[0].duration;
|
|
|
|
for (var i = 1; i < frames.length; i++) {
|
|
duration += frames[i].duration;
|
|
}
|
|
return {
|
|
duration: duration,
|
|
width: width,
|
|
height: height
|
|
};
|
|
}
|
|
|
|
function numToBuffer(num) {
|
|
var parts = [];
|
|
while (num > 0) {
|
|
parts.push(num & 0xff);
|
|
num = num >> 8;
|
|
}
|
|
return new Uint8Array(parts.reverse());
|
|
}
|
|
|
|
function strToBuffer(str) {
|
|
return new Uint8Array(str.split('').map(function(e) {
|
|
return e.charCodeAt(0);
|
|
}));
|
|
}
|
|
|
|
function bitsToBuffer(bits) {
|
|
var data = [];
|
|
var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : '';
|
|
bits = pad + bits;
|
|
for (var i = 0; i < bits.length; i += 8) {
|
|
data.push(parseInt(bits.substr(i, 8), 2));
|
|
}
|
|
return new Uint8Array(data);
|
|
}
|
|
|
|
function generateEBML(json) {
|
|
var ebml = [];
|
|
for (var i = 0; i < json.length; i++) {
|
|
var data = json[i].data;
|
|
|
|
if (typeof data === 'object') {
|
|
data = generateEBML(data);
|
|
}
|
|
|
|
if (typeof data === 'number') {
|
|
data = bitsToBuffer(data.toString(2));
|
|
}
|
|
|
|
if (typeof data === 'string') {
|
|
data = strToBuffer(data);
|
|
}
|
|
|
|
var len = data.size || data.byteLength || data.length;
|
|
var zeroes = Math.ceil(Math.ceil(Math.log(len) / Math.log(2)) / 8);
|
|
var sizeToString = len.toString(2);
|
|
var padded = (new Array((zeroes * 7 + 7 + 1) - sizeToString.length)).join('0') + sizeToString;
|
|
var size = (new Array(zeroes)).join('0') + '1' + padded;
|
|
|
|
ebml.push(numToBuffer(json[i].id));
|
|
ebml.push(bitsToBuffer(size));
|
|
ebml.push(data);
|
|
}
|
|
|
|
return new Blob(ebml, {
|
|
type: 'video/webm'
|
|
});
|
|
}
|
|
|
|
function makeSimpleBlock(data) {
|
|
var flags = 0;
|
|
|
|
if (data.keyframe) {
|
|
flags |= 128;
|
|
}
|
|
|
|
if (data.invisible) {
|
|
flags |= 8;
|
|
}
|
|
|
|
if (data.lacing) {
|
|
flags |= (data.lacing << 1);
|
|
}
|
|
|
|
if (data.discardable) {
|
|
flags |= 1;
|
|
}
|
|
|
|
if (data.trackNum > 127) {
|
|
throw 'TrackNumber > 127 not supported';
|
|
}
|
|
|
|
var out = [data.trackNum | 0x80, data.timecode >> 8, data.timecode & 0xff, flags].map(function(e) {
|
|
return String.fromCharCode(e);
|
|
}).join('') + data.frame;
|
|
|
|
return out;
|
|
}
|
|
|
|
function parseWebP(riff) {
|
|
var VP8 = riff.RIFF[0].WEBP[0];
|
|
|
|
var frameStart = VP8.indexOf('\x9d\x01\x2a'); // A VP8 keyframe starts with the 0x9d012a header
|
|
for (var i = 0, c = []; i < 4; i++) {
|
|
c[i] = VP8.charCodeAt(frameStart + 3 + i);
|
|
}
|
|
|
|
var width, height, tmp;
|
|
|
|
//the code below is literally copied verbatim from the bitstream spec
|
|
tmp = (c[1] << 8) | c[0];
|
|
width = tmp & 0x3FFF;
|
|
tmp = (c[3] << 8) | c[2];
|
|
height = tmp & 0x3FFF;
|
|
return {
|
|
width: width,
|
|
height: height,
|
|
data: VP8,
|
|
riff: riff
|
|
};
|
|
}
|
|
|
|
function getStrLength(string, offset) {
|
|
return parseInt(string.substr(offset + 4, 4).split('').map(function(i) {
|
|
var unpadded = i.charCodeAt(0).toString(2);
|
|
return (new Array(8 - unpadded.length + 1)).join('0') + unpadded;
|
|
}).join(''), 2);
|
|
}
|
|
|
|
function parseRIFF(string) {
|
|
var offset = 0;
|
|
var chunks = {};
|
|
|
|
while (offset < string.length) {
|
|
var id = string.substr(offset, 4);
|
|
var len = getStrLength(string, offset);
|
|
var data = string.substr(offset + 4 + 4, len);
|
|
offset += 4 + 4 + len;
|
|
chunks[id] = chunks[id] || [];
|
|
|
|
if (id === 'RIFF' || id === 'LIST') {
|
|
chunks[id].push(parseRIFF(data));
|
|
} else {
|
|
chunks[id].push(data);
|
|
}
|
|
}
|
|
return chunks;
|
|
}
|
|
|
|
function doubleToString(num) {
|
|
return [].slice.call(
|
|
new Uint8Array((new Float64Array([num])).buffer), 0).map(function(e) {
|
|
return String.fromCharCode(e);
|
|
}).reverse().join('');
|
|
}
|
|
|
|
var webm = new ArrayToWebM(frames.map(function(frame) {
|
|
var webp = parseWebP(parseRIFF(atob(frame.image.slice(23))));
|
|
webp.duration = frame.duration;
|
|
return webp;
|
|
}));
|
|
|
|
postMessage(webm);
|
|
}
|
|
|
|
/**
|
|
* Encodes frames in WebM container. It uses WebWorkinvoke to invoke 'ArrayToWebM' method.
|
|
* @param {function} callback - Callback function, that is used to pass recorded blob back to the callee.
|
|
* @method
|
|
* @memberof Whammy
|
|
* @example
|
|
* recorder = new Whammy().Video(0.8, 100);
|
|
* recorder.compile(function(blob) {
|
|
* // blob.size - blob.type
|
|
* });
|
|
*/
|
|
WhammyVideo.prototype.compile = function(callback) {
|
|
var webWorker = processInWebWorker(whammyInWebWorker);
|
|
|
|
webWorker.onmessage = function(event) {
|
|
if (event.data.error) {
|
|
console.error(event.data.error);
|
|
return;
|
|
}
|
|
callback(event.data);
|
|
};
|
|
|
|
webWorker.postMessage(this.frames);
|
|
};
|
|
|
|
return {
|
|
/**
|
|
* A more abstract-ish API.
|
|
* @method
|
|
* @memberof Whammy
|
|
* @example
|
|
* recorder = new Whammy().Video(0.8, 100);
|
|
* @param {?number} speed - 0.8
|
|
* @param {?number} quality - 100
|
|
*/
|
|
Video: WhammyVideo
|
|
};
|
|
})();
|
|
|
|
if (typeof RecordRTC !== 'undefined') {
|
|
RecordRTC.Whammy = Whammy;
|
|
}
|
|
|
|
// ______________ (indexed-db)
|
|
// DiskStorage.js
|
|
|
|
/**
|
|
* DiskStorage is a standalone object used by {@link RecordRTC} to store recorded blobs in IndexedDB storage.
|
|
* @summary Writing blobs into IndexedDB.
|
|
* @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
|
|
* @author {@link https://MuazKhan.com|Muaz Khan}
|
|
* @example
|
|
* DiskStorage.Store({
|
|
* audioBlob: yourAudioBlob,
|
|
* videoBlob: yourVideoBlob,
|
|
* gifBlob : yourGifBlob
|
|
* });
|
|
* DiskStorage.Fetch(function(dataURL, type) {
|
|
* if(type === 'audioBlob') { }
|
|
* if(type === 'videoBlob') { }
|
|
* if(type === 'gifBlob') { }
|
|
* });
|
|
* // DiskStorage.dataStoreName = 'recordRTC';
|
|
* // DiskStorage.onError = function(error) { };
|
|
* @property {function} init - This method must be called once to initialize IndexedDB ObjectStore. Though, it is auto-used internally.
|
|
* @property {function} Fetch - This method fetches stored blobs from IndexedDB.
|
|
* @property {function} Store - This method stores blobs in IndexedDB.
|
|
* @property {function} onError - This function is invoked for any known/unknown error.
|
|
* @property {string} dataStoreName - Name of the ObjectStore created in IndexedDB storage.
|
|
* @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
|
|
*/
|
|
|
|
|
|
var DiskStorage = {
|
|
/**
|
|
* This method must be called once to initialize IndexedDB ObjectStore. Though, it is auto-used internally.
|
|
* @method
|
|
* @memberof DiskStorage
|
|
* @internal
|
|
* @example
|
|
* DiskStorage.init();
|
|
*/
|
|
init: function() {
|
|
var self = this;
|
|
|
|
if (typeof indexedDB === 'undefined' || typeof indexedDB.open === 'undefined') {
|
|
console.error('IndexedDB API are not available in this browser.');
|
|
return;
|
|
}
|
|
|
|
var dbVersion = 1;
|
|
var dbName = this.dbName || location.href.replace(/\/|:|#|%|\.|\[|\]/g, ''),
|
|
db;
|
|
var request = indexedDB.open(dbName, dbVersion);
|
|
|
|
function createObjectStore(dataBase) {
|
|
dataBase.createObjectStore(self.dataStoreName);
|
|
}
|
|
|
|
function putInDB() {
|
|
var transaction = db.transaction([self.dataStoreName], 'readwrite');
|
|
|
|
if (self.videoBlob) {
|
|
transaction.objectStore(self.dataStoreName).put(self.videoBlob, 'videoBlob');
|
|
}
|
|
|
|
if (self.gifBlob) {
|
|
transaction.objectStore(self.dataStoreName).put(self.gifBlob, 'gifBlob');
|
|
}
|
|
|
|
if (self.audioBlob) {
|
|
transaction.objectStore(self.dataStoreName).put(self.audioBlob, 'audioBlob');
|
|
}
|
|
|
|
function getFromStore(portionName) {
|
|
transaction.objectStore(self.dataStoreName).get(portionName).onsuccess = function(event) {
|
|
if (self.callback) {
|
|
self.callback(event.target.result, portionName);
|
|
}
|
|
};
|
|
}
|
|
|
|
getFromStore('audioBlob');
|
|
getFromStore('videoBlob');
|
|
getFromStore('gifBlob');
|
|
}
|
|
|
|
request.onerror = self.onError;
|
|
|
|
request.onsuccess = function() {
|
|
db = request.result;
|
|
db.onerror = self.onError;
|
|
|
|
if (db.setVersion) {
|
|
if (db.version !== dbVersion) {
|
|
var setVersion = db.setVersion(dbVersion);
|
|
setVersion.onsuccess = function() {
|
|
createObjectStore(db);
|
|
putInDB();
|
|
};
|
|
} else {
|
|
putInDB();
|
|
}
|
|
} else {
|
|
putInDB();
|
|
}
|
|
};
|
|
request.onupgradeneeded = function(event) {
|
|
createObjectStore(event.target.result);
|
|
};
|
|
},
|
|
/**
|
|
* This method fetches stored blobs from IndexedDB.
|
|
* @method
|
|
* @memberof DiskStorage
|
|
* @internal
|
|
* @example
|
|
* DiskStorage.Fetch(function(dataURL, type) {
|
|
* if(type === 'audioBlob') { }
|
|
* if(type === 'videoBlob') { }
|
|
* if(type === 'gifBlob') { }
|
|
* });
|
|
*/
|
|
Fetch: function(callback) {
|
|
this.callback = callback;
|
|
this.init();
|
|
|
|
return this;
|
|
},
|
|
/**
|
|
* This method stores blobs in IndexedDB.
|
|
* @method
|
|
* @memberof DiskStorage
|
|
* @internal
|
|
* @example
|
|
* DiskStorage.Store({
|
|
* audioBlob: yourAudioBlob,
|
|
* videoBlob: yourVideoBlob,
|
|
* gifBlob : yourGifBlob
|
|
* });
|
|
*/
|
|
Store: function(config) {
|
|
this.audioBlob = config.audioBlob;
|
|
this.videoBlob = config.videoBlob;
|
|
this.gifBlob = config.gifBlob;
|
|
|
|
this.init();
|
|
|
|
return this;
|
|
},
|
|
/**
|
|
* This function is invoked for any known/unknown error.
|
|
* @method
|
|
* @memberof DiskStorage
|
|
* @internal
|
|
* @example
|
|
* DiskStorage.onError = function(error){
|
|
* alerot( JSON.stringify(error) );
|
|
* };
|
|
*/
|
|
onError: function(error) {
|
|
console.error(JSON.stringify(error, null, '\t'));
|
|
},
|
|
|
|
/**
|
|
* @property {string} dataStoreName - Name of the ObjectStore created in IndexedDB storage.
|
|
* @memberof DiskStorage
|
|
* @internal
|
|
* @example
|
|
* DiskStorage.dataStoreName = 'recordRTC';
|
|
*/
|
|
dataStoreName: 'recordRTC',
|
|
dbName: null
|
|
};
|
|
|
|
if (typeof RecordRTC !== 'undefined') {
|
|
RecordRTC.DiskStorage = DiskStorage;
|
|
}
|
|
|
|
// ______________
|
|
// GifRecorder.js
|
|
|
|
/**
|
|
* GifRecorder is standalone calss used by {@link RecordRTC} to record video or canvas into animated gif.
|
|
* @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
|
|
* @author {@link https://MuazKhan.com|Muaz Khan}
|
|
* @typedef GifRecorder
|
|
* @class
|
|
* @example
|
|
* var recorder = new GifRecorder(mediaStream || canvas || context, { onGifPreview: function, onGifRecordingStarted: function, width: 1280, height: 720, frameRate: 200, quality: 10 });
|
|
* recorder.record();
|
|
* recorder.stop(function(blob) {
|
|
* img.src = URL.createObjectURL(blob);
|
|
* });
|
|
* @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
|
|
* @param {MediaStream} mediaStream - MediaStream object or HTMLCanvasElement or CanvasRenderingContext2D.
|
|
* @param {object} config - {disableLogs:true, initCallback: function, width: 320, height: 240, frameRate: 200, quality: 10}
|
|
*/
|
|
|
|
function GifRecorder(mediaStream, config) {
|
|
if (typeof GIFEncoder === 'undefined') {
|
|
var script = document.createElement('script');
|
|
script.src = 'https://www.webrtc-experiment.com/gif-recorder.js';
|
|
(document.body || document.documentElement).appendChild(script);
|
|
}
|
|
|
|
config = config || {};
|
|
|
|
var isHTMLObject = mediaStream instanceof CanvasRenderingContext2D || mediaStream instanceof HTMLCanvasElement;
|
|
|
|
/**
|
|
* This method records MediaStream.
|
|
* @method
|
|
* @memberof GifRecorder
|
|
* @example
|
|
* recorder.record();
|
|
*/
|
|
this.record = function() {
|
|
if (typeof GIFEncoder === 'undefined') {
|
|
setTimeout(self.record, 1000);
|
|
return;
|
|
}
|
|
|
|
if (!isLoadedMetaData) {
|
|
setTimeout(self.record, 1000);
|
|
return;
|
|
}
|
|
|
|
if (!isHTMLObject) {
|
|
if (!config.width) {
|
|
config.width = video.offsetWidth || 320;
|
|
}
|
|
|
|
if (!config.height) {
|
|
config.height = video.offsetHeight || 240;
|
|
}
|
|
|
|
if (!config.video) {
|
|
config.video = {
|
|
width: config.width,
|
|
height: config.height
|
|
};
|
|
}
|
|
|
|
if (!config.canvas) {
|
|
config.canvas = {
|
|
width: config.width,
|
|
height: config.height
|
|
};
|
|
}
|
|
|
|
canvas.width = config.canvas.width || 320;
|
|
canvas.height = config.canvas.height || 240;
|
|
|
|
video.width = config.video.width || 320;
|
|
video.height = config.video.height || 240;
|
|
}
|
|
|
|
// external library to record as GIF images
|
|
gifEncoder = new GIFEncoder();
|
|
|
|
// void setRepeat(int iter)
|
|
// Sets the number of times the set of GIF frames should be played.
|
|
// Default is 1; 0 means play indefinitely.
|
|
gifEncoder.setRepeat(0);
|
|
|
|
// void setFrameRate(Number fps)
|
|
// Sets frame rate in frames per second.
|
|
// Equivalent to setDelay(1000/fps).
|
|
// Using "setDelay" instead of "setFrameRate"
|
|
gifEncoder.setDelay(config.frameRate || 200);
|
|
|
|
// void setQuality(int quality)
|
|
// Sets quality of color quantization (conversion of images to the
|
|
// maximum 256 colors allowed by the GIF specification).
|
|
// Lower values (minimum = 1) produce better colors,
|
|
// but slow processing significantly. 10 is the default,
|
|
// and produces good color mapping at reasonable speeds.
|
|
// Values greater than 20 do not yield significant improvements in speed.
|
|
gifEncoder.setQuality(config.quality || 10);
|
|
|
|
// Boolean start()
|
|
// This writes the GIF Header and returns false if it fails.
|
|
gifEncoder.start();
|
|
|
|
if (typeof config.onGifRecordingStarted === 'function') {
|
|
config.onGifRecordingStarted();
|
|
}
|
|
|
|
function drawVideoFrame(time) {
|
|
if (self.clearedRecordedData === true) {
|
|
return;
|
|
}
|
|
|
|
if (isPausedRecording) {
|
|
return setTimeout(function() {
|
|
drawVideoFrame(time);
|
|
}, 100);
|
|
}
|
|
|
|
lastAnimationFrame = requestAnimationFrame(drawVideoFrame);
|
|
|
|
if (typeof lastFrameTime === undefined) {
|
|
lastFrameTime = time;
|
|
}
|
|
|
|
// ~10 fps
|
|
if (time - lastFrameTime < 90) {
|
|
return;
|
|
}
|
|
|
|
if (!isHTMLObject && video.paused) {
|
|
// via: https://github.com/muaz-khan/WebRTC-Experiment/pull/316
|
|
// Tweak for Android Chrome
|
|
video.play();
|
|
}
|
|
|
|
if (!isHTMLObject) {
|
|
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
}
|
|
|
|
if (config.onGifPreview) {
|
|
config.onGifPreview(canvas.toDataURL('image/png'));
|
|
}
|
|
|
|
gifEncoder.addFrame(context);
|
|
lastFrameTime = time;
|
|
}
|
|
|
|
lastAnimationFrame = requestAnimationFrame(drawVideoFrame);
|
|
|
|
if (config.initCallback) {
|
|
config.initCallback();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This method stops recording MediaStream.
|
|
* @param {function} callback - Callback function, that is used to pass recorded blob back to the callee.
|
|
* @method
|
|
* @memberof GifRecorder
|
|
* @example
|
|
* recorder.stop(function(blob) {
|
|
* img.src = URL.createObjectURL(blob);
|
|
* });
|
|
*/
|
|
this.stop = function(callback) {
|
|
callback = callback || function() {};
|
|
|
|
if (lastAnimationFrame) {
|
|
cancelAnimationFrame(lastAnimationFrame);
|
|
}
|
|
|
|
/**
|
|
* @property {Blob} blob - The recorded blob object.
|
|
* @memberof GifRecorder
|
|
* @example
|
|
* recorder.stop(function(){
|
|
* var blob = recorder.blob;
|
|
* });
|
|
*/
|
|
this.blob = new Blob([new Uint8Array(gifEncoder.stream().bin)], {
|
|
type: 'image/gif'
|
|
});
|
|
|
|
callback(this.blob);
|
|
|
|
// bug: find a way to clear old recorded blobs
|
|
gifEncoder.stream().bin = [];
|
|
};
|
|
|
|
var isPausedRecording = false;
|
|
|
|
/**
|
|
* This method pauses the recording process.
|
|
* @method
|
|
* @memberof GifRecorder
|
|
* @example
|
|
* recorder.pause();
|
|
*/
|
|
this.pause = function() {
|
|
isPausedRecording = true;
|
|
};
|
|
|
|
/**
|
|
* This method resumes the recording process.
|
|
* @method
|
|
* @memberof GifRecorder
|
|
* @example
|
|
* recorder.resume();
|
|
*/
|
|
this.resume = function() {
|
|
isPausedRecording = false;
|
|
};
|
|
|
|
/**
|
|
* This method resets currently recorded data.
|
|
* @method
|
|
* @memberof GifRecorder
|
|
* @example
|
|
* recorder.clearRecordedData();
|
|
*/
|
|
this.clearRecordedData = function() {
|
|
self.clearedRecordedData = true;
|
|
clearRecordedDataCB();
|
|
};
|
|
|
|
function clearRecordedDataCB() {
|
|
if (gifEncoder) {
|
|
gifEncoder.stream().bin = [];
|
|
}
|
|
}
|
|
|
|
// for debugging
|
|
this.name = 'GifRecorder';
|
|
this.toString = function() {
|
|
return this.name;
|
|
};
|
|
|
|
var canvas = document.createElement('canvas');
|
|
var context = canvas.getContext('2d');
|
|
|
|
if (isHTMLObject) {
|
|
if (mediaStream instanceof CanvasRenderingContext2D) {
|
|
context = mediaStream;
|
|
canvas = context.canvas;
|
|
} else if (mediaStream instanceof HTMLCanvasElement) {
|
|
context = mediaStream.getContext('2d');
|
|
canvas = mediaStream;
|
|
}
|
|
}
|
|
|
|
var isLoadedMetaData = true;
|
|
|
|
if (!isHTMLObject) {
|
|
var video = document.createElement('video');
|
|
video.muted = true;
|
|
video.autoplay = true;
|
|
video.playsInline = true;
|
|
|
|
isLoadedMetaData = false;
|
|
video.onloadedmetadata = function() {
|
|
isLoadedMetaData = true;
|
|
};
|
|
|
|
setSrcObject(mediaStream, video);
|
|
|
|
video.play();
|
|
}
|
|
|
|
var lastAnimationFrame = null;
|
|
var lastFrameTime;
|
|
|
|
var gifEncoder;
|
|
|
|
var self = this;
|
|
}
|
|
|
|
if (typeof RecordRTC !== 'undefined') {
|
|
RecordRTC.GifRecorder = GifRecorder;
|
|
}
|
|
|
|
// Last time updated: 2019-06-21 4:09:42 AM UTC
|
|
|
|
// ________________________
|
|
// MultiStreamsMixer v1.2.2
|
|
|
|
// Open-Sourced: https://github.com/muaz-khan/MultiStreamsMixer
|
|
|
|
// --------------------------------------------------
|
|
// Muaz Khan - www.MuazKhan.com
|
|
// MIT License - www.WebRTC-Experiment.com/licence
|
|
// --------------------------------------------------
|
|
|
|
function MultiStreamsMixer(arrayOfMediaStreams, elementClass) {
|
|
|
|
var browserFakeUserAgent = 'Fake/5.0 (FakeOS) AppleWebKit/123 (KHTML, like Gecko) Fake/12.3.4567.89 Fake/123.45';
|
|
|
|
(function(that) {
|
|
if (typeof RecordRTC !== 'undefined') {
|
|
return;
|
|
}
|
|
|
|
if (!that) {
|
|
return;
|
|
}
|
|
|
|
if (typeof window !== 'undefined') {
|
|
return;
|
|
}
|
|
|
|
if (typeof commonjsGlobal === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
commonjsGlobal.navigator = {
|
|
userAgent: browserFakeUserAgent,
|
|
getUserMedia: function() {}
|
|
};
|
|
|
|
if (!commonjsGlobal.console) {
|
|
commonjsGlobal.console = {};
|
|
}
|
|
|
|
if (typeof commonjsGlobal.console.log === 'undefined' || typeof commonjsGlobal.console.error === 'undefined') {
|
|
commonjsGlobal.console.error = commonjsGlobal.console.log = commonjsGlobal.console.log || function() {
|
|
console.log(arguments);
|
|
};
|
|
}
|
|
|
|
if (typeof document === 'undefined') {
|
|
/*global document:true */
|
|
that.document = {
|
|
documentElement: {
|
|
appendChild: function() {
|
|
return '';
|
|
}
|
|
}
|
|
};
|
|
|
|
document.createElement = document.captureStream = document.mozCaptureStream = function() {
|
|
var obj = {
|
|
getContext: function() {
|
|
return obj;
|
|
},
|
|
play: function() {},
|
|
pause: function() {},
|
|
drawImage: function() {},
|
|
toDataURL: function() {
|
|
return '';
|
|
},
|
|
style: {}
|
|
};
|
|
return obj;
|
|
};
|
|
|
|
that.HTMLVideoElement = function() {};
|
|
}
|
|
|
|
if (typeof location === 'undefined') {
|
|
/*global location:true */
|
|
that.location = {
|
|
protocol: 'file:',
|
|
href: '',
|
|
hash: ''
|
|
};
|
|
}
|
|
|
|
if (typeof screen === 'undefined') {
|
|
/*global screen:true */
|
|
that.screen = {
|
|
width: 0,
|
|
height: 0
|
|
};
|
|
}
|
|
|
|
if (typeof URL === 'undefined') {
|
|
/*global screen:true */
|
|
that.URL = {
|
|
createObjectURL: function() {
|
|
return '';
|
|
},
|
|
revokeObjectURL: function() {
|
|
return '';
|
|
}
|
|
};
|
|
}
|
|
|
|
/*global window:true */
|
|
that.window = commonjsGlobal;
|
|
})(typeof commonjsGlobal !== 'undefined' ? commonjsGlobal : null);
|
|
|
|
// requires: chrome://flags/#enable-experimental-web-platform-features
|
|
|
|
elementClass = elementClass || 'multi-streams-mixer';
|
|
|
|
var videos = [];
|
|
var isStopDrawingFrames = false;
|
|
|
|
var canvas = document.createElement('canvas');
|
|
var context = canvas.getContext('2d');
|
|
canvas.style.opacity = 0;
|
|
canvas.style.position = 'absolute';
|
|
canvas.style.zIndex = -1;
|
|
canvas.style.top = '-1000em';
|
|
canvas.style.left = '-1000em';
|
|
canvas.className = elementClass;
|
|
(document.body || document.documentElement).appendChild(canvas);
|
|
|
|
this.disableLogs = false;
|
|
this.frameInterval = 10;
|
|
|
|
this.width = 360;
|
|
this.height = 240;
|
|
|
|
// use gain node to prevent echo
|
|
this.useGainNode = true;
|
|
|
|
var self = this;
|
|
|
|
// _____________________________
|
|
// Cross-Browser-Declarations.js
|
|
|
|
// WebAudio API representer
|
|
var AudioContext = window.AudioContext;
|
|
|
|
if (typeof AudioContext === 'undefined') {
|
|
if (typeof webkitAudioContext !== 'undefined') {
|
|
/*global AudioContext:true */
|
|
AudioContext = webkitAudioContext;
|
|
}
|
|
|
|
if (typeof mozAudioContext !== 'undefined') {
|
|
/*global AudioContext:true */
|
|
AudioContext = mozAudioContext;
|
|
}
|
|
}
|
|
|
|
/*jshint -W079 */
|
|
var URL = window.URL;
|
|
|
|
if (typeof URL === 'undefined' && typeof webkitURL !== 'undefined') {
|
|
/*global URL:true */
|
|
URL = webkitURL;
|
|
}
|
|
|
|
if (typeof navigator !== 'undefined' && typeof navigator.getUserMedia === 'undefined') { // maybe window.navigator?
|
|
if (typeof navigator.webkitGetUserMedia !== 'undefined') {
|
|
navigator.getUserMedia = navigator.webkitGetUserMedia;
|
|
}
|
|
|
|
if (typeof navigator.mozGetUserMedia !== 'undefined') {
|
|
navigator.getUserMedia = navigator.mozGetUserMedia;
|
|
}
|
|
}
|
|
|
|
var MediaStream = window.MediaStream;
|
|
|
|
if (typeof MediaStream === 'undefined' && typeof webkitMediaStream !== 'undefined') {
|
|
MediaStream = webkitMediaStream;
|
|
}
|
|
|
|
/*global MediaStream:true */
|
|
if (typeof MediaStream !== 'undefined') {
|
|
// override "stop" method for all browsers
|
|
if (typeof MediaStream.prototype.stop === 'undefined') {
|
|
MediaStream.prototype.stop = function() {
|
|
this.getTracks().forEach(function(track) {
|
|
track.stop();
|
|
});
|
|
};
|
|
}
|
|
}
|
|
|
|
var Storage = {};
|
|
|
|
if (typeof AudioContext !== 'undefined') {
|
|
Storage.AudioContext = AudioContext;
|
|
} else if (typeof webkitAudioContext !== 'undefined') {
|
|
Storage.AudioContext = webkitAudioContext;
|
|
}
|
|
|
|
function setSrcObject(stream, element) {
|
|
if ('srcObject' in element) {
|
|
element.srcObject = stream;
|
|
} else if ('mozSrcObject' in element) {
|
|
element.mozSrcObject = stream;
|
|
} else {
|
|
element.srcObject = stream;
|
|
}
|
|
}
|
|
|
|
this.startDrawingFrames = function() {
|
|
drawVideosToCanvas();
|
|
};
|
|
|
|
function drawVideosToCanvas() {
|
|
if (isStopDrawingFrames) {
|
|
return;
|
|
}
|
|
|
|
var videosLength = videos.length;
|
|
|
|
var fullcanvas = false;
|
|
var remaining = [];
|
|
videos.forEach(function(video) {
|
|
if (!video.stream) {
|
|
video.stream = {};
|
|
}
|
|
|
|
if (video.stream.fullcanvas) {
|
|
fullcanvas = video;
|
|
} else {
|
|
// todo: video.stream.active or video.stream.live to fix blank frames issues?
|
|
remaining.push(video);
|
|
}
|
|
});
|
|
|
|
if (fullcanvas) {
|
|
canvas.width = fullcanvas.stream.width;
|
|
canvas.height = fullcanvas.stream.height;
|
|
} else if (remaining.length) {
|
|
canvas.width = videosLength > 1 ? remaining[0].width * 2 : remaining[0].width;
|
|
|
|
var height = 1;
|
|
if (videosLength === 3 || videosLength === 4) {
|
|
height = 2;
|
|
}
|
|
if (videosLength === 5 || videosLength === 6) {
|
|
height = 3;
|
|
}
|
|
if (videosLength === 7 || videosLength === 8) {
|
|
height = 4;
|
|
}
|
|
if (videosLength === 9 || videosLength === 10) {
|
|
height = 5;
|
|
}
|
|
canvas.height = remaining[0].height * height;
|
|
} else {
|
|
canvas.width = self.width || 360;
|
|
canvas.height = self.height || 240;
|
|
}
|
|
|
|
if (fullcanvas && fullcanvas instanceof HTMLVideoElement) {
|
|
drawImage(fullcanvas);
|
|
}
|
|
|
|
remaining.forEach(function(video, idx) {
|
|
drawImage(video, idx);
|
|
});
|
|
|
|
setTimeout(drawVideosToCanvas, self.frameInterval);
|
|
}
|
|
|
|
function drawImage(video, idx) {
|
|
if (isStopDrawingFrames) {
|
|
return;
|
|
}
|
|
|
|
var x = 0;
|
|
var y = 0;
|
|
var width = video.width;
|
|
var height = video.height;
|
|
|
|
if (idx === 1) {
|
|
x = video.width;
|
|
}
|
|
|
|
if (idx === 2) {
|
|
y = video.height;
|
|
}
|
|
|
|
if (idx === 3) {
|
|
x = video.width;
|
|
y = video.height;
|
|
}
|
|
|
|
if (idx === 4) {
|
|
y = video.height * 2;
|
|
}
|
|
|
|
if (idx === 5) {
|
|
x = video.width;
|
|
y = video.height * 2;
|
|
}
|
|
|
|
if (idx === 6) {
|
|
y = video.height * 3;
|
|
}
|
|
|
|
if (idx === 7) {
|
|
x = video.width;
|
|
y = video.height * 3;
|
|
}
|
|
|
|
if (typeof video.stream.left !== 'undefined') {
|
|
x = video.stream.left;
|
|
}
|
|
|
|
if (typeof video.stream.top !== 'undefined') {
|
|
y = video.stream.top;
|
|
}
|
|
|
|
if (typeof video.stream.width !== 'undefined') {
|
|
width = video.stream.width;
|
|
}
|
|
|
|
if (typeof video.stream.height !== 'undefined') {
|
|
height = video.stream.height;
|
|
}
|
|
|
|
context.drawImage(video, x, y, width, height);
|
|
|
|
if (typeof video.stream.onRender === 'function') {
|
|
video.stream.onRender(context, x, y, width, height, idx);
|
|
}
|
|
}
|
|
|
|
function getMixedStream() {
|
|
isStopDrawingFrames = false;
|
|
var mixedVideoStream = getMixedVideoStream();
|
|
|
|
var mixedAudioStream = getMixedAudioStream();
|
|
if (mixedAudioStream) {
|
|
mixedAudioStream.getTracks().filter(function(t) {
|
|
return t.kind === 'audio';
|
|
}).forEach(function(track) {
|
|
mixedVideoStream.addTrack(track);
|
|
});
|
|
}
|
|
arrayOfMediaStreams.forEach(function(stream) {
|
|
if (stream.fullcanvas) ;
|
|
});
|
|
|
|
// mixedVideoStream.prototype.appendStreams = appendStreams;
|
|
// mixedVideoStream.prototype.resetVideoStreams = resetVideoStreams;
|
|
// mixedVideoStream.prototype.clearRecordedData = clearRecordedData;
|
|
|
|
return mixedVideoStream;
|
|
}
|
|
|
|
function getMixedVideoStream() {
|
|
resetVideoStreams();
|
|
|
|
var capturedStream;
|
|
|
|
if ('captureStream' in canvas) {
|
|
capturedStream = canvas.captureStream();
|
|
} else if ('mozCaptureStream' in canvas) {
|
|
capturedStream = canvas.mozCaptureStream();
|
|
} else if (!self.disableLogs) {
|
|
console.error('Upgrade to latest Chrome or otherwise enable this flag: chrome://flags/#enable-experimental-web-platform-features');
|
|
}
|
|
|
|
var videoStream = new MediaStream();
|
|
|
|
capturedStream.getTracks().filter(function(t) {
|
|
return t.kind === 'video';
|
|
}).forEach(function(track) {
|
|
videoStream.addTrack(track);
|
|
});
|
|
|
|
canvas.stream = videoStream;
|
|
|
|
return videoStream;
|
|
}
|
|
|
|
function getMixedAudioStream() {
|
|
// via: @pehrsons
|
|
if (!Storage.AudioContextConstructor) {
|
|
Storage.AudioContextConstructor = new Storage.AudioContext();
|
|
}
|
|
|
|
self.audioContext = Storage.AudioContextConstructor;
|
|
|
|
self.audioSources = [];
|
|
|
|
if (self.useGainNode === true) {
|
|
self.gainNode = self.audioContext.createGain();
|
|
self.gainNode.connect(self.audioContext.destination);
|
|
self.gainNode.gain.value = 0; // don't hear self
|
|
}
|
|
|
|
var audioTracksLength = 0;
|
|
arrayOfMediaStreams.forEach(function(stream) {
|
|
if (!stream.getTracks().filter(function(t) {
|
|
return t.kind === 'audio';
|
|
}).length) {
|
|
return;
|
|
}
|
|
|
|
audioTracksLength++;
|
|
|
|
var audioSource = self.audioContext.createMediaStreamSource(stream);
|
|
|
|
if (self.useGainNode === true) {
|
|
audioSource.connect(self.gainNode);
|
|
}
|
|
|
|
self.audioSources.push(audioSource);
|
|
});
|
|
|
|
if (!audioTracksLength) {
|
|
// because "self.audioContext" is not initialized
|
|
// that's why we've to ignore rest of the code
|
|
return;
|
|
}
|
|
|
|
self.audioDestination = self.audioContext.createMediaStreamDestination();
|
|
self.audioSources.forEach(function(audioSource) {
|
|
audioSource.connect(self.audioDestination);
|
|
});
|
|
return self.audioDestination.stream;
|
|
}
|
|
|
|
function getVideo(stream) {
|
|
var video = document.createElement('video');
|
|
|
|
setSrcObject(stream, video);
|
|
|
|
video.className = elementClass;
|
|
|
|
video.muted = true;
|
|
video.volume = 0;
|
|
|
|
video.width = stream.width || self.width || 360;
|
|
video.height = stream.height || self.height || 240;
|
|
|
|
video.play();
|
|
|
|
return video;
|
|
}
|
|
|
|
this.appendStreams = function(streams) {
|
|
if (!streams) {
|
|
throw 'First parameter is required.';
|
|
}
|
|
|
|
if (!(streams instanceof Array)) {
|
|
streams = [streams];
|
|
}
|
|
|
|
streams.forEach(function(stream) {
|
|
var newStream = new MediaStream();
|
|
|
|
if (stream.getTracks().filter(function(t) {
|
|
return t.kind === 'video';
|
|
}).length) {
|
|
var video = getVideo(stream);
|
|
video.stream = stream;
|
|
videos.push(video);
|
|
|
|
newStream.addTrack(stream.getTracks().filter(function(t) {
|
|
return t.kind === 'video';
|
|
})[0]);
|
|
}
|
|
|
|
if (stream.getTracks().filter(function(t) {
|
|
return t.kind === 'audio';
|
|
}).length) {
|
|
var audioSource = self.audioContext.createMediaStreamSource(stream);
|
|
self.audioDestination = self.audioContext.createMediaStreamDestination();
|
|
audioSource.connect(self.audioDestination);
|
|
|
|
newStream.addTrack(self.audioDestination.stream.getTracks().filter(function(t) {
|
|
return t.kind === 'audio';
|
|
})[0]);
|
|
}
|
|
|
|
arrayOfMediaStreams.push(newStream);
|
|
});
|
|
};
|
|
|
|
this.releaseStreams = function() {
|
|
videos = [];
|
|
isStopDrawingFrames = true;
|
|
|
|
if (self.gainNode) {
|
|
self.gainNode.disconnect();
|
|
self.gainNode = null;
|
|
}
|
|
|
|
if (self.audioSources.length) {
|
|
self.audioSources.forEach(function(source) {
|
|
source.disconnect();
|
|
});
|
|
self.audioSources = [];
|
|
}
|
|
|
|
if (self.audioDestination) {
|
|
self.audioDestination.disconnect();
|
|
self.audioDestination = null;
|
|
}
|
|
|
|
if (self.audioContext) {
|
|
self.audioContext.close();
|
|
}
|
|
|
|
self.audioContext = null;
|
|
|
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
if (canvas.stream) {
|
|
canvas.stream.stop();
|
|
canvas.stream = null;
|
|
}
|
|
};
|
|
|
|
this.resetVideoStreams = function(streams) {
|
|
if (streams && !(streams instanceof Array)) {
|
|
streams = [streams];
|
|
}
|
|
|
|
resetVideoStreams(streams);
|
|
};
|
|
|
|
function resetVideoStreams(streams) {
|
|
videos = [];
|
|
streams = streams || arrayOfMediaStreams;
|
|
|
|
// via: @adrian-ber
|
|
streams.forEach(function(stream) {
|
|
if (!stream.getTracks().filter(function(t) {
|
|
return t.kind === 'video';
|
|
}).length) {
|
|
return;
|
|
}
|
|
|
|
var video = getVideo(stream);
|
|
video.stream = stream;
|
|
videos.push(video);
|
|
});
|
|
}
|
|
|
|
// for debugging
|
|
this.name = 'MultiStreamsMixer';
|
|
this.toString = function() {
|
|
return this.name;
|
|
};
|
|
|
|
this.getMixedStream = getMixedStream;
|
|
|
|
}
|
|
|
|
if (typeof RecordRTC === 'undefined') {
|
|
{
|
|
module.exports = MultiStreamsMixer;
|
|
}
|
|
}
|
|
|
|
// ______________________
|
|
// MultiStreamRecorder.js
|
|
|
|
/*
|
|
* Video conference recording, using captureStream API along with WebAudio and Canvas2D API.
|
|
*/
|
|
|
|
/**
|
|
* MultiStreamRecorder can record multiple videos in single container.
|
|
* @summary Multi-videos recorder.
|
|
* @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
|
|
* @author {@link https://MuazKhan.com|Muaz Khan}
|
|
* @typedef MultiStreamRecorder
|
|
* @class
|
|
* @example
|
|
* var options = {
|
|
* mimeType: 'video/webm'
|
|
* }
|
|
* var recorder = new MultiStreamRecorder(ArrayOfMediaStreams, options);
|
|
* recorder.record();
|
|
* recorder.stop(function(blob) {
|
|
* video.src = URL.createObjectURL(blob);
|
|
*
|
|
* // or
|
|
* var blob = recorder.blob;
|
|
* });
|
|
* @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
|
|
* @param {MediaStreams} mediaStreams - Array of MediaStreams.
|
|
* @param {object} config - {disableLogs:true, frameInterval: 1, mimeType: "video/webm"}
|
|
*/
|
|
|
|
function MultiStreamRecorder(arrayOfMediaStreams, options) {
|
|
arrayOfMediaStreams = arrayOfMediaStreams || [];
|
|
var self = this;
|
|
|
|
var mixer;
|
|
var mediaRecorder;
|
|
|
|
options = options || {
|
|
elementClass: 'multi-streams-mixer',
|
|
mimeType: 'video/webm',
|
|
video: {
|
|
width: 360,
|
|
height: 240
|
|
}
|
|
};
|
|
|
|
if (!options.frameInterval) {
|
|
options.frameInterval = 10;
|
|
}
|
|
|
|
if (!options.video) {
|
|
options.video = {};
|
|
}
|
|
|
|
if (!options.video.width) {
|
|
options.video.width = 360;
|
|
}
|
|
|
|
if (!options.video.height) {
|
|
options.video.height = 240;
|
|
}
|
|
|
|
/**
|
|
* This method records all MediaStreams.
|
|
* @method
|
|
* @memberof MultiStreamRecorder
|
|
* @example
|
|
* recorder.record();
|
|
*/
|
|
this.record = function() {
|
|
// github/muaz-khan/MultiStreamsMixer
|
|
mixer = new MultiStreamsMixer(arrayOfMediaStreams, options.elementClass || 'multi-streams-mixer');
|
|
|
|
if (getAllVideoTracks().length) {
|
|
mixer.frameInterval = options.frameInterval || 10;
|
|
mixer.width = options.video.width || 360;
|
|
mixer.height = options.video.height || 240;
|
|
mixer.startDrawingFrames();
|
|
}
|
|
|
|
if (options.previewStream && typeof options.previewStream === 'function') {
|
|
options.previewStream(mixer.getMixedStream());
|
|
}
|
|
|
|
// record using MediaRecorder API
|
|
mediaRecorder = new MediaStreamRecorder(mixer.getMixedStream(), options);
|
|
mediaRecorder.record();
|
|
};
|
|
|
|
function getAllVideoTracks() {
|
|
var tracks = [];
|
|
arrayOfMediaStreams.forEach(function(stream) {
|
|
getTracks(stream, 'video').forEach(function(track) {
|
|
tracks.push(track);
|
|
});
|
|
});
|
|
return tracks;
|
|
}
|
|
|
|
/**
|
|
* This method stops recording MediaStream.
|
|
* @param {function} callback - Callback function, that is used to pass recorded blob back to the callee.
|
|
* @method
|
|
* @memberof MultiStreamRecorder
|
|
* @example
|
|
* recorder.stop(function(blob) {
|
|
* video.src = URL.createObjectURL(blob);
|
|
* });
|
|
*/
|
|
this.stop = function(callback) {
|
|
if (!mediaRecorder) {
|
|
return;
|
|
}
|
|
|
|
mediaRecorder.stop(function(blob) {
|
|
self.blob = blob;
|
|
|
|
callback(blob);
|
|
|
|
self.clearRecordedData();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* This method pauses the recording process.
|
|
* @method
|
|
* @memberof MultiStreamRecorder
|
|
* @example
|
|
* recorder.pause();
|
|
*/
|
|
this.pause = function() {
|
|
if (mediaRecorder) {
|
|
mediaRecorder.pause();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This method resumes the recording process.
|
|
* @method
|
|
* @memberof MultiStreamRecorder
|
|
* @example
|
|
* recorder.resume();
|
|
*/
|
|
this.resume = function() {
|
|
if (mediaRecorder) {
|
|
mediaRecorder.resume();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This method resets currently recorded data.
|
|
* @method
|
|
* @memberof MultiStreamRecorder
|
|
* @example
|
|
* recorder.clearRecordedData();
|
|
*/
|
|
this.clearRecordedData = function() {
|
|
if (mediaRecorder) {
|
|
mediaRecorder.clearRecordedData();
|
|
mediaRecorder = null;
|
|
}
|
|
|
|
if (mixer) {
|
|
mixer.releaseStreams();
|
|
mixer = null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Add extra media-streams to existing recordings.
|
|
* @method
|
|
* @memberof MultiStreamRecorder
|
|
* @param {MediaStreams} mediaStreams - Array of MediaStreams
|
|
* @example
|
|
* recorder.addStreams([newAudioStream, newVideoStream]);
|
|
*/
|
|
this.addStreams = function(streams) {
|
|
if (!streams) {
|
|
throw 'First parameter is required.';
|
|
}
|
|
|
|
if (!(streams instanceof Array)) {
|
|
streams = [streams];
|
|
}
|
|
|
|
arrayOfMediaStreams.concat(streams);
|
|
|
|
if (!mediaRecorder || !mixer) {
|
|
return;
|
|
}
|
|
|
|
mixer.appendStreams(streams);
|
|
|
|
if (options.previewStream && typeof options.previewStream === 'function') {
|
|
options.previewStream(mixer.getMixedStream());
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Reset videos during live recording. Replace old videos e.g. replace cameras with full-screen.
|
|
* @method
|
|
* @memberof MultiStreamRecorder
|
|
* @param {MediaStreams} mediaStreams - Array of MediaStreams
|
|
* @example
|
|
* recorder.resetVideoStreams([newVideo1, newVideo2]);
|
|
*/
|
|
this.resetVideoStreams = function(streams) {
|
|
if (!mixer) {
|
|
return;
|
|
}
|
|
|
|
if (streams && !(streams instanceof Array)) {
|
|
streams = [streams];
|
|
}
|
|
|
|
mixer.resetVideoStreams(streams);
|
|
};
|
|
|
|
/**
|
|
* Returns MultiStreamsMixer
|
|
* @method
|
|
* @memberof MultiStreamRecorder
|
|
* @example
|
|
* let mixer = recorder.getMixer();
|
|
* mixer.appendStreams([newStream]);
|
|
*/
|
|
this.getMixer = function() {
|
|
return mixer;
|
|
};
|
|
|
|
// for debugging
|
|
this.name = 'MultiStreamRecorder';
|
|
this.toString = function() {
|
|
return this.name;
|
|
};
|
|
}
|
|
|
|
if (typeof RecordRTC !== 'undefined') {
|
|
RecordRTC.MultiStreamRecorder = MultiStreamRecorder;
|
|
}
|
|
|
|
// _____________________
|
|
// RecordRTC.promises.js
|
|
|
|
/**
|
|
* RecordRTCPromisesHandler adds promises support in {@link RecordRTC}. Try a {@link https://github.com/muaz-khan/RecordRTC/blob/master/simple-demos/RecordRTCPromisesHandler.html|demo here}
|
|
* @summary Promises for {@link RecordRTC}
|
|
* @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
|
|
* @author {@link https://MuazKhan.com|Muaz Khan}
|
|
* @typedef RecordRTCPromisesHandler
|
|
* @class
|
|
* @example
|
|
* var recorder = new RecordRTCPromisesHandler(mediaStream, options);
|
|
* recorder.startRecording()
|
|
* .then(successCB)
|
|
* .catch(errorCB);
|
|
* // Note: You can access all RecordRTC API using "recorder.recordRTC" e.g.
|
|
* recorder.recordRTC.onStateChanged = function(state) {};
|
|
* recorder.recordRTC.setRecordingDuration(5000);
|
|
* @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
|
|
* @param {MediaStream} mediaStream - Single media-stream object, array of media-streams, html-canvas-element, etc.
|
|
* @param {object} config - {type:"video", recorderType: MediaStreamRecorder, disableLogs: true, numberOfAudioChannels: 1, bufferSize: 0, sampleRate: 0, video: HTMLVideoElement, etc.}
|
|
* @throws Will throw an error if "new" keyword is not used to initiate "RecordRTCPromisesHandler". Also throws error if first argument "MediaStream" is missing.
|
|
* @requires {@link RecordRTC}
|
|
*/
|
|
|
|
function RecordRTCPromisesHandler(mediaStream, options) {
|
|
if (!this) {
|
|
throw 'Use "new RecordRTCPromisesHandler()"';
|
|
}
|
|
|
|
if (typeof mediaStream === 'undefined') {
|
|
throw 'First argument "MediaStream" is required.';
|
|
}
|
|
|
|
var self = this;
|
|
|
|
/**
|
|
* @property {Blob} blob - Access/reach the native {@link RecordRTC} object.
|
|
* @memberof RecordRTCPromisesHandler
|
|
* @example
|
|
* let internal = recorder.recordRTC.getInternalRecorder();
|
|
* alert(internal instanceof MediaStreamRecorder);
|
|
* recorder.recordRTC.onStateChanged = function(state) {};
|
|
*/
|
|
self.recordRTC = new RecordRTC(mediaStream, options);
|
|
|
|
/**
|
|
* This method records MediaStream.
|
|
* @method
|
|
* @memberof RecordRTCPromisesHandler
|
|
* @example
|
|
* recorder.startRecording()
|
|
* .then(successCB)
|
|
* .catch(errorCB);
|
|
*/
|
|
this.startRecording = function() {
|
|
return new Promise(function(resolve, reject) {
|
|
try {
|
|
self.recordRTC.startRecording();
|
|
resolve();
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* This method stops the recording.
|
|
* @method
|
|
* @memberof RecordRTCPromisesHandler
|
|
* @example
|
|
* recorder.stopRecording().then(function() {
|
|
* var blob = recorder.getBlob();
|
|
* }).catch(errorCB);
|
|
*/
|
|
this.stopRecording = function() {
|
|
return new Promise(function(resolve, reject) {
|
|
try {
|
|
self.recordRTC.stopRecording(function(url) {
|
|
self.blob = self.recordRTC.getBlob();
|
|
|
|
if (!self.blob || !self.blob.size) {
|
|
reject('Empty blob.', self.blob);
|
|
return;
|
|
}
|
|
|
|
resolve(url);
|
|
});
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* This method pauses the recording. You can resume recording using "resumeRecording" method.
|
|
* @method
|
|
* @memberof RecordRTCPromisesHandler
|
|
* @example
|
|
* recorder.pauseRecording()
|
|
* .then(successCB)
|
|
* .catch(errorCB);
|
|
*/
|
|
this.pauseRecording = function() {
|
|
return new Promise(function(resolve, reject) {
|
|
try {
|
|
self.recordRTC.pauseRecording();
|
|
resolve();
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* This method resumes the recording.
|
|
* @method
|
|
* @memberof RecordRTCPromisesHandler
|
|
* @example
|
|
* recorder.resumeRecording()
|
|
* .then(successCB)
|
|
* .catch(errorCB);
|
|
*/
|
|
this.resumeRecording = function() {
|
|
return new Promise(function(resolve, reject) {
|
|
try {
|
|
self.recordRTC.resumeRecording();
|
|
resolve();
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* This method returns data-url for the recorded blob.
|
|
* @method
|
|
* @memberof RecordRTCPromisesHandler
|
|
* @example
|
|
* recorder.stopRecording().then(function() {
|
|
* recorder.getDataURL().then(function(dataURL) {
|
|
* window.open(dataURL);
|
|
* }).catch(errorCB);;
|
|
* }).catch(errorCB);
|
|
*/
|
|
this.getDataURL = function(callback) {
|
|
return new Promise(function(resolve, reject) {
|
|
try {
|
|
self.recordRTC.getDataURL(function(dataURL) {
|
|
resolve(dataURL);
|
|
});
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* This method returns the recorded blob.
|
|
* @method
|
|
* @memberof RecordRTCPromisesHandler
|
|
* @example
|
|
* recorder.stopRecording().then(function() {
|
|
* recorder.getBlob().then(function(blob) {})
|
|
* }).catch(errorCB);
|
|
*/
|
|
this.getBlob = function() {
|
|
return new Promise(function(resolve, reject) {
|
|
try {
|
|
resolve(self.recordRTC.getBlob());
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* This method returns the internal recording object.
|
|
* @method
|
|
* @memberof RecordRTCPromisesHandler
|
|
* @example
|
|
* let internalRecorder = await recorder.getInternalRecorder();
|
|
* if(internalRecorder instanceof MultiStreamRecorder) {
|
|
* internalRecorder.addStreams([newAudioStream]);
|
|
* internalRecorder.resetVideoStreams([screenStream]);
|
|
* }
|
|
* @returns {Object}
|
|
*/
|
|
this.getInternalRecorder = function() {
|
|
return new Promise(function(resolve, reject) {
|
|
try {
|
|
resolve(self.recordRTC.getInternalRecorder());
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* This method resets the recorder. So that you can reuse single recorder instance many times.
|
|
* @method
|
|
* @memberof RecordRTCPromisesHandler
|
|
* @example
|
|
* await recorder.reset();
|
|
* recorder.startRecording(); // record again
|
|
*/
|
|
this.reset = function() {
|
|
return new Promise(function(resolve, reject) {
|
|
try {
|
|
resolve(self.recordRTC.reset());
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Destroy RecordRTC instance. Clear all recorders and objects.
|
|
* @method
|
|
* @memberof RecordRTCPromisesHandler
|
|
* @example
|
|
* recorder.destroy().then(successCB).catch(errorCB);
|
|
*/
|
|
this.destroy = function() {
|
|
return new Promise(function(resolve, reject) {
|
|
try {
|
|
resolve(self.recordRTC.destroy());
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Get recorder's readonly state.
|
|
* @method
|
|
* @memberof RecordRTCPromisesHandler
|
|
* @example
|
|
* let state = await recorder.getState();
|
|
* // or
|
|
* recorder.getState().then(state => { console.log(state); })
|
|
* @returns {String} Returns recording state.
|
|
*/
|
|
this.getState = function() {
|
|
return new Promise(function(resolve, reject) {
|
|
try {
|
|
resolve(self.recordRTC.getState());
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @property {Blob} blob - Recorded data as "Blob" object.
|
|
* @memberof RecordRTCPromisesHandler
|
|
* @example
|
|
* await recorder.stopRecording();
|
|
* let blob = recorder.getBlob(); // or "recorder.recordRTC.blob"
|
|
* invokeSaveAsDialog(blob);
|
|
*/
|
|
this.blob = null;
|
|
|
|
/**
|
|
* RecordRTC version number
|
|
* @property {String} version - Release version number.
|
|
* @memberof RecordRTCPromisesHandler
|
|
* @static
|
|
* @readonly
|
|
* @example
|
|
* alert(recorder.version);
|
|
*/
|
|
this.version = '5.6.2';
|
|
}
|
|
|
|
if (typeof RecordRTC !== 'undefined') {
|
|
RecordRTC.RecordRTCPromisesHandler = RecordRTCPromisesHandler;
|
|
}
|
|
|
|
// ______________________
|
|
// WebAssemblyRecorder.js
|
|
|
|
/**
|
|
* WebAssemblyRecorder lets you create webm videos in JavaScript via WebAssembly. The library consumes raw RGBA32 buffers (4 bytes per pixel) and turns them into a webm video with the given framerate and quality. This makes it compatible out-of-the-box with ImageData from a CANVAS. With realtime mode you can also use webm-wasm for streaming webm videos.
|
|
* @summary Video recording feature in Chrome, Firefox and maybe Edge.
|
|
* @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT}
|
|
* @author {@link https://MuazKhan.com|Muaz Khan}
|
|
* @typedef WebAssemblyRecorder
|
|
* @class
|
|
* @example
|
|
* var recorder = new WebAssemblyRecorder(mediaStream);
|
|
* recorder.record();
|
|
* recorder.stop(function(blob) {
|
|
* video.src = URL.createObjectURL(blob);
|
|
* });
|
|
* @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code}
|
|
* @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API.
|
|
* @param {object} config - {webAssemblyPath:'webm-wasm.wasm',workerPath: 'webm-worker.js', frameRate: 30, width: 1920, height: 1080, bitrate: 1024, realtime: true}
|
|
*/
|
|
function WebAssemblyRecorder(stream, config) {
|
|
// based on: github.com/GoogleChromeLabs/webm-wasm
|
|
|
|
if (typeof ReadableStream === 'undefined' || typeof WritableStream === 'undefined') {
|
|
// because it fixes readable/writable streams issues
|
|
console.error('Following polyfill is strongly recommended: https://unpkg.com/@mattiasbuelens/web-streams-polyfill/dist/polyfill.min.js');
|
|
}
|
|
|
|
config = config || {};
|
|
|
|
config.width = config.width || 640;
|
|
config.height = config.height || 480;
|
|
config.frameRate = config.frameRate || 30;
|
|
config.bitrate = config.bitrate || 1200;
|
|
config.realtime = config.realtime || true;
|
|
|
|
var finished;
|
|
|
|
function cameraStream() {
|
|
return new ReadableStream({
|
|
start: function(controller) {
|
|
var cvs = document.createElement('canvas');
|
|
var video = document.createElement('video');
|
|
var first = true;
|
|
video.srcObject = stream;
|
|
video.muted = true;
|
|
video.height = config.height;
|
|
video.width = config.width;
|
|
video.volume = 0;
|
|
video.onplaying = function() {
|
|
cvs.width = config.width;
|
|
cvs.height = config.height;
|
|
var ctx = cvs.getContext('2d');
|
|
var frameTimeout = 1000 / config.frameRate;
|
|
var cameraTimer = setInterval(function f() {
|
|
if (finished) {
|
|
clearInterval(cameraTimer);
|
|
controller.close();
|
|
}
|
|
|
|
if (first) {
|
|
first = false;
|
|
if (config.onVideoProcessStarted) {
|
|
config.onVideoProcessStarted();
|
|
}
|
|
}
|
|
|
|
ctx.drawImage(video, 0, 0);
|
|
if (controller._controlledReadableStream.state !== 'closed') {
|
|
try {
|
|
controller.enqueue(
|
|
ctx.getImageData(0, 0, config.width, config.height)
|
|
);
|
|
} catch (e) {}
|
|
}
|
|
}, frameTimeout);
|
|
};
|
|
video.play();
|
|
}
|
|
});
|
|
}
|
|
|
|
var worker;
|
|
|
|
function startRecording(stream, buffer) {
|
|
if (!config.workerPath && !buffer) {
|
|
finished = false;
|
|
|
|
// is it safe to use @latest ?
|
|
|
|
fetch(
|
|
'https://unpkg.com/webm-wasm@latest/dist/webm-worker.js'
|
|
).then(function(r) {
|
|
r.arrayBuffer().then(function(buffer) {
|
|
startRecording(stream, buffer);
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (!config.workerPath && buffer instanceof ArrayBuffer) {
|
|
var blob = new Blob([buffer], {
|
|
type: 'text/javascript'
|
|
});
|
|
config.workerPath = URL.createObjectURL(blob);
|
|
}
|
|
|
|
if (!config.workerPath) {
|
|
console.error('workerPath parameter is missing.');
|
|
}
|
|
|
|
worker = new Worker(config.workerPath);
|
|
|
|
worker.postMessage(config.webAssemblyPath || 'https://unpkg.com/webm-wasm@latest/dist/webm-wasm.wasm');
|
|
worker.addEventListener('message', function(event) {
|
|
if (event.data === 'READY') {
|
|
worker.postMessage({
|
|
width: config.width,
|
|
height: config.height,
|
|
bitrate: config.bitrate || 1200,
|
|
timebaseDen: config.frameRate || 30,
|
|
realtime: config.realtime
|
|
});
|
|
|
|
cameraStream().pipeTo(new WritableStream({
|
|
write: function(image) {
|
|
if (finished) {
|
|
console.error('Got image, but recorder is finished!');
|
|
return;
|
|
}
|
|
|
|
worker.postMessage(image.data.buffer, [image.data.buffer]);
|
|
}
|
|
}));
|
|
} else if (!!event.data) {
|
|
if (!isPaused) {
|
|
arrayOfBuffers.push(event.data);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* This method records video.
|
|
* @method
|
|
* @memberof WebAssemblyRecorder
|
|
* @example
|
|
* recorder.record();
|
|
*/
|
|
this.record = function() {
|
|
arrayOfBuffers = [];
|
|
isPaused = false;
|
|
this.blob = null;
|
|
startRecording(stream);
|
|
|
|
if (typeof config.initCallback === 'function') {
|
|
config.initCallback();
|
|
}
|
|
};
|
|
|
|
var isPaused;
|
|
|
|
/**
|
|
* This method pauses the recording process.
|
|
* @method
|
|
* @memberof WebAssemblyRecorder
|
|
* @example
|
|
* recorder.pause();
|
|
*/
|
|
this.pause = function() {
|
|
isPaused = true;
|
|
};
|
|
|
|
/**
|
|
* This method resumes the recording process.
|
|
* @method
|
|
* @memberof WebAssemblyRecorder
|
|
* @example
|
|
* recorder.resume();
|
|
*/
|
|
this.resume = function() {
|
|
isPaused = false;
|
|
};
|
|
|
|
function terminate(callback) {
|
|
if (!worker) {
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Wait for null event data to indicate that the encoding is complete
|
|
worker.addEventListener('message', function(event) {
|
|
if (event.data === null) {
|
|
worker.terminate();
|
|
worker = null;
|
|
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
}
|
|
});
|
|
|
|
worker.postMessage(null);
|
|
}
|
|
|
|
var arrayOfBuffers = [];
|
|
|
|
/**
|
|
* This method stops recording video.
|
|
* @param {function} callback - Callback function, that is used to pass recorded blob back to the callee.
|
|
* @method
|
|
* @memberof WebAssemblyRecorder
|
|
* @example
|
|
* recorder.stop(function(blob) {
|
|
* video.src = URL.createObjectURL(blob);
|
|
* });
|
|
*/
|
|
this.stop = function(callback) {
|
|
finished = true;
|
|
|
|
var recorder = this;
|
|
|
|
terminate(function() {
|
|
recorder.blob = new Blob(arrayOfBuffers, {
|
|
type: 'video/webm'
|
|
});
|
|
|
|
callback(recorder.blob);
|
|
});
|
|
};
|
|
|
|
// for debugging
|
|
this.name = 'WebAssemblyRecorder';
|
|
this.toString = function() {
|
|
return this.name;
|
|
};
|
|
|
|
/**
|
|
* This method resets currently recorded data.
|
|
* @method
|
|
* @memberof WebAssemblyRecorder
|
|
* @example
|
|
* recorder.clearRecordedData();
|
|
*/
|
|
this.clearRecordedData = function() {
|
|
arrayOfBuffers = [];
|
|
isPaused = false;
|
|
this.blob = null;
|
|
|
|
// todo: if recording-ON then STOP it first
|
|
};
|
|
|
|
/**
|
|
* @property {Blob} blob - The recorded blob object.
|
|
* @memberof WebAssemblyRecorder
|
|
* @example
|
|
* recorder.stop(function(){
|
|
* var blob = recorder.blob;
|
|
* });
|
|
*/
|
|
this.blob = null;
|
|
}
|
|
|
|
if (typeof RecordRTC !== 'undefined') {
|
|
RecordRTC.WebAssemblyRecorder = WebAssemblyRecorder;
|
|
}
|
|
});
|
|
|
|
class RecordRTCLoader extends Emitter {
|
|
constructor(player) {
|
|
super();
|
|
this.player = player;
|
|
this.fileName = '';
|
|
this.fileType = player._opt.recordType || FILE_SUFFIX.webm;
|
|
this.isRecording = false;
|
|
this.recordingTimestamp = 0;
|
|
this.recordingInterval = null;
|
|
player.debug.log('Recorder', 'init');
|
|
}
|
|
|
|
destroy() {
|
|
this._reset();
|
|
|
|
this.player.debug.log('Recorder', 'destroy');
|
|
}
|
|
|
|
setFileName(fileName, fileType) {
|
|
this.fileName = fileName;
|
|
|
|
if (FILE_SUFFIX.mp4 === fileType || FILE_SUFFIX.webm === fileType) {
|
|
this.fileType = fileType;
|
|
}
|
|
}
|
|
|
|
get recording() {
|
|
return this.isRecording;
|
|
}
|
|
|
|
get recordTime() {
|
|
return this.recordingTimestamp;
|
|
}
|
|
|
|
startRecord() {
|
|
const debug = this.player.debug;
|
|
const options = {
|
|
type: 'video',
|
|
mimeType: 'video/webm;codecs=h264',
|
|
onTimeStamp: timestamp => {
|
|
debug.log('Recorder', 'record timestamp :' + timestamp);
|
|
},
|
|
disableLogs: !this.player._opt.debug
|
|
};
|
|
|
|
try {
|
|
const stream = this.player.video.$videoElement.captureStream(25);
|
|
|
|
if (this.player.audio && this.player.audio.mediaStreamAudioDestinationNode && this.player.audio.mediaStreamAudioDestinationNode.stream && !this.player.audio.isStateSuspended() && this.player.audio.hasAudio && this.player._opt.hasAudio) {
|
|
const audioStream = this.player.audio.mediaStreamAudioDestinationNode.stream;
|
|
|
|
if (audioStream.getAudioTracks().length > 0) {
|
|
const audioTrack = audioStream.getAudioTracks()[0];
|
|
|
|
if (audioTrack && audioTrack.enabled) {
|
|
stream.addTrack(audioTrack);
|
|
}
|
|
}
|
|
}
|
|
|
|
this.recorder = RecordRTC_1(stream, options);
|
|
} catch (e) {
|
|
debug.error('Recorder', 'startRecord error', e);
|
|
this.emit(EVENTS.recordCreateError);
|
|
}
|
|
|
|
if (this.recorder) {
|
|
this.isRecording = true;
|
|
this.player.emit(EVENTS.recording, true);
|
|
this.recorder.startRecording();
|
|
debug.log('Recorder', 'start recording');
|
|
this.player.emit(EVENTS.recordStart);
|
|
this.recordingInterval = window.setInterval(() => {
|
|
this.recordingTimestamp += 1;
|
|
this.player.emit(EVENTS.recordingTimestamp, this.recordingTimestamp);
|
|
}, 1000);
|
|
}
|
|
}
|
|
|
|
stopRecordAndSave() {
|
|
if (!this.recorder || !this.isRecording) {
|
|
return;
|
|
}
|
|
|
|
this.recorder.stopRecording(() => {
|
|
this.player.debug.log('Recorder', 'stop recording');
|
|
this.player.emit(EVENTS.recordEnd);
|
|
const fileName = (this.fileName || now()) + '.' + (this.fileType || FILE_SUFFIX.webm);
|
|
saveAs(this.recorder.getBlob(), fileName);
|
|
|
|
this._reset();
|
|
|
|
this.player.emit(EVENTS.recording, false);
|
|
});
|
|
}
|
|
|
|
_reset() {
|
|
this.isRecording = false;
|
|
this.recordingTimestamp = 0;
|
|
|
|
if (this.recorder) {
|
|
this.recorder.destroy();
|
|
this.recorder = null;
|
|
}
|
|
|
|
this.fileName = null;
|
|
|
|
if (this.recordingInterval) {
|
|
clearInterval(this.recordingInterval);
|
|
}
|
|
|
|
this.recordingInterval = null;
|
|
}
|
|
|
|
}
|
|
|
|
class Recorder {
|
|
constructor(player) {
|
|
const Loader = Recorder.getLoaderFactory();
|
|
return new Loader(player);
|
|
}
|
|
|
|
static getLoaderFactory() {
|
|
return RecordRTCLoader;
|
|
}
|
|
|
|
}
|
|
|
|
class DecoderWorker {
|
|
constructor(player) {
|
|
this.player = player;
|
|
this.decoderWorker = new Worker(player._opt.decoder);
|
|
|
|
this._initDecoderWorker();
|
|
|
|
player.debug.log('decoderWorker', 'init');
|
|
}
|
|
|
|
destroy() {
|
|
this.decoderWorker.postMessage({
|
|
cmd: WORKER_SEND_TYPE.close
|
|
});
|
|
this.decoderWorker.terminate();
|
|
this.decoderWorker = null;
|
|
this.player.debug.log(`decoderWorker`, 'destroy');
|
|
}
|
|
|
|
_initDecoderWorker() {
|
|
const {
|
|
debug,
|
|
events: {
|
|
proxy
|
|
}
|
|
} = this.player;
|
|
|
|
this.decoderWorker.onmessage = event => {
|
|
const msg = event.data;
|
|
|
|
switch (msg.cmd) {
|
|
case WORKER_CMD_TYPE.init:
|
|
debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.init);
|
|
|
|
if (!this.player.loaded) {
|
|
this.player.emit(EVENTS.load);
|
|
}
|
|
|
|
this.player.emit(EVENTS.decoderWorkerInit);
|
|
|
|
this._initWork();
|
|
|
|
break;
|
|
|
|
case WORKER_CMD_TYPE.videoCode:
|
|
debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.videoCode, msg.code);
|
|
|
|
if (!this.player._times.decodeStart) {
|
|
this.player._times.decodeStart = now();
|
|
}
|
|
|
|
this.player.video.updateVideoInfo({
|
|
encTypeCode: msg.code
|
|
});
|
|
break;
|
|
|
|
case WORKER_CMD_TYPE.audioCode:
|
|
debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.audioCode, msg.code);
|
|
this.player.audio && this.player.audio.updateAudioInfo({
|
|
encTypeCode: msg.code
|
|
});
|
|
break;
|
|
|
|
case WORKER_CMD_TYPE.initVideo:
|
|
debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.initVideo, `width:${msg.w},height:${msg.h}`);
|
|
this.player.video.updateVideoInfo({
|
|
width: msg.w,
|
|
height: msg.h
|
|
});
|
|
|
|
if (!this.player._opt.openWebglAlignment && !isWebglRenderSupport(msg.w)) {
|
|
this.player.emit(EVENTS_ERROR.webglAlignmentError);
|
|
return;
|
|
}
|
|
|
|
this.player.video.initCanvasViewSize();
|
|
break;
|
|
|
|
case WORKER_CMD_TYPE.initAudio:
|
|
debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.initAudio, `channels:${msg.channels},sampleRate:${msg.sampleRate}`);
|
|
|
|
if (this.player.audio) {
|
|
this.player.audio.updateAudioInfo(msg);
|
|
this.player.audio.initScriptNode(msg);
|
|
}
|
|
|
|
break;
|
|
|
|
case WORKER_CMD_TYPE.render:
|
|
// debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.render, `msg ts:${msg.ts}`);
|
|
this.player.handleRender();
|
|
this.player.video.render(msg);
|
|
this.player.emit(EVENTS.timeUpdate, msg.ts);
|
|
this.player.updateStats({
|
|
fps: true,
|
|
ts: msg.ts,
|
|
buf: msg.delay
|
|
});
|
|
|
|
if (!this.player._times.videoStart) {
|
|
this.player._times.videoStart = now();
|
|
this.player.handlePlayToRenderTimes();
|
|
}
|
|
|
|
break;
|
|
|
|
case WORKER_CMD_TYPE.playAudio:
|
|
// debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.playAudio, `msg ts:${msg.ts}`);
|
|
// 只有在 playing 的时候。
|
|
if (this.player.playing && this.player.audio) {
|
|
this.player.audio.play(msg.buffer, msg.ts);
|
|
}
|
|
|
|
break;
|
|
|
|
case WORKER_CMD_TYPE.wasmError:
|
|
if (msg.message) {
|
|
if (msg.message.indexOf(WASM_ERROR.invalidNalUnitSize) !== -1) {
|
|
this.player.emitError(EVENTS_ERROR.wasmDecodeError);
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
this.player[msg.cmd] && this.player[msg.cmd](msg);
|
|
}
|
|
};
|
|
}
|
|
|
|
_initWork() {
|
|
const opt = {
|
|
debug: this.player._opt.debug,
|
|
useOffscreen: this.player._opt.useOffscreen,
|
|
useWCS: this.player._opt.useWCS,
|
|
videoBuffer: this.player._opt.videoBuffer,
|
|
videoBufferDelay: this.player._opt.videoBufferDelay,
|
|
openWebglAlignment: this.player._opt.openWebglAlignment
|
|
};
|
|
this.decoderWorker.postMessage({
|
|
cmd: WORKER_SEND_TYPE.init,
|
|
opt: JSON.stringify(opt),
|
|
sampleRate: this.player.audio && this.player.audio.audioContext.sampleRate || 0
|
|
});
|
|
}
|
|
|
|
decodeVideo(arrayBuffer, ts, isIFrame) {
|
|
const options = {
|
|
type: MEDIA_TYPE.video,
|
|
ts: Math.max(ts, 0),
|
|
isIFrame
|
|
}; // this.player.debug.log('decoderWorker', 'decodeVideo', options);
|
|
|
|
this.decoderWorker.postMessage({
|
|
cmd: WORKER_SEND_TYPE.decode,
|
|
buffer: arrayBuffer,
|
|
options
|
|
}, [arrayBuffer.buffer]);
|
|
}
|
|
|
|
decodeAudio(arrayBuffer, ts) {
|
|
if (this.player._opt.useWCS) {
|
|
this._decodeAudioNoDelay(arrayBuffer, ts);
|
|
} else if (this.player._opt.useMSE) {
|
|
this._decodeAudioNoDelay(arrayBuffer, ts);
|
|
} else {
|
|
this._decodeAudio(arrayBuffer, ts);
|
|
}
|
|
} //
|
|
|
|
|
|
_decodeAudio(arrayBuffer, ts) {
|
|
const options = {
|
|
type: MEDIA_TYPE.audio,
|
|
ts: Math.max(ts, 0)
|
|
}; // this.player.debug.log('decoderWorker', 'decodeAudio',options);
|
|
|
|
this.decoderWorker.postMessage({
|
|
cmd: WORKER_SEND_TYPE.decode,
|
|
buffer: arrayBuffer,
|
|
options
|
|
}, [arrayBuffer.buffer]);
|
|
}
|
|
|
|
_decodeAudioNoDelay(arrayBuffer, ts) {
|
|
// console.log('_decodeAudioNoDelay', arrayBuffer);
|
|
this.decoderWorker.postMessage({
|
|
cmd: WORKER_SEND_TYPE.audioDecode,
|
|
buffer: arrayBuffer,
|
|
ts: Math.max(ts, 0)
|
|
}, [arrayBuffer.buffer]);
|
|
}
|
|
|
|
updateWorkConfig(config) {
|
|
this.decoderWorker.postMessage({
|
|
cmd: WORKER_SEND_TYPE.updateConfig,
|
|
key: config.key,
|
|
value: config.value
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
class CommonLoader extends Emitter {
|
|
constructor(player) {
|
|
super();
|
|
this.player = player;
|
|
this.stopId = null;
|
|
this.firstTimestamp = null;
|
|
this.startTimestamp = null;
|
|
this.delay = -1;
|
|
this.bufferList = [];
|
|
this.dropping = false;
|
|
this.initInterval();
|
|
}
|
|
|
|
destroy() {
|
|
if (this.stopId) {
|
|
clearInterval(this.stopId);
|
|
this.stopId = null;
|
|
}
|
|
|
|
this.firstTimestamp = null;
|
|
this.startTimestamp = null;
|
|
this.delay = -1;
|
|
this.bufferList = [];
|
|
this.dropping = false;
|
|
this.off();
|
|
this.player.debug.log('CommonDemux', 'destroy');
|
|
}
|
|
|
|
getDelay(timestamp) {
|
|
if (!timestamp) {
|
|
return -1;
|
|
}
|
|
|
|
if (!this.firstTimestamp) {
|
|
this.firstTimestamp = timestamp;
|
|
this.startTimestamp = Date.now();
|
|
this.delay = -1;
|
|
} else {
|
|
if (timestamp) {
|
|
const localTimestamp = Date.now() - this.startTimestamp;
|
|
const timeTimestamp = timestamp - this.firstTimestamp;
|
|
|
|
if (localTimestamp >= timeTimestamp) {
|
|
this.delay = localTimestamp - timeTimestamp;
|
|
} else {
|
|
this.delay = timeTimestamp - localTimestamp;
|
|
}
|
|
}
|
|
}
|
|
|
|
return this.delay;
|
|
}
|
|
|
|
resetDelay() {
|
|
this.firstTimestamp = null;
|
|
this.startTimestamp = null;
|
|
this.delay = -1;
|
|
this.dropping = false;
|
|
} //
|
|
|
|
|
|
initInterval() {
|
|
this.player.debug.log('common dumex', `init Interval`);
|
|
|
|
let _loop = () => {
|
|
let data;
|
|
const videoBuffer = this.player._opt.videoBuffer;
|
|
const videoBufferDelay = this.player._opt.videoBufferDelay;
|
|
|
|
if (this.bufferList.length) {
|
|
if (this.dropping) {
|
|
// this.player.debug.log('common dumex', `is dropping`);
|
|
data = this.bufferList.shift();
|
|
|
|
if (data.type === MEDIA_TYPE.audio && data.payload[1] === 0) {
|
|
this._doDecoderDecode(data);
|
|
}
|
|
|
|
while (!data.isIFrame && this.bufferList.length) {
|
|
data = this.bufferList.shift();
|
|
|
|
if (data.type === MEDIA_TYPE.audio && data.payload[1] === 0) {
|
|
this._doDecoderDecode(data);
|
|
}
|
|
} // i frame
|
|
|
|
|
|
if (data.isIFrame && this.getDelay(data.ts) <= Math.min(videoBuffer, 200)) {
|
|
this.dropping = false;
|
|
|
|
this._doDecoderDecode(data);
|
|
}
|
|
} else {
|
|
data = this.bufferList[0];
|
|
|
|
if (this.getDelay(data.ts) === -1) {
|
|
// this.player.debug.log('common dumex', `delay is -1`);
|
|
this.bufferList.shift();
|
|
|
|
this._doDecoderDecode(data);
|
|
} else if (this.delay > videoBuffer + videoBufferDelay) {
|
|
// this.player.debug.log('common dumex', `delay is ${this.delay}, set dropping is true`);
|
|
this.resetDelay();
|
|
this.dropping = true;
|
|
} else {
|
|
data = this.bufferList[0];
|
|
|
|
if (this.getDelay(data.ts) > videoBuffer) {
|
|
// drop frame
|
|
this.bufferList.shift();
|
|
|
|
this._doDecoderDecode(data);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
_loop();
|
|
|
|
this.stopId = setInterval(_loop, 10);
|
|
}
|
|
|
|
_doDecode(payload, type, ts, isIFrame, cts) {
|
|
const player = this.player;
|
|
let options = {
|
|
ts: ts,
|
|
cts: cts,
|
|
type: type,
|
|
isIFrame: false
|
|
}; // use offscreen
|
|
|
|
if (player._opt.useWCS && !player._opt.useOffscreen) {
|
|
if (type === MEDIA_TYPE.video) {
|
|
options.isIFrame = isIFrame;
|
|
}
|
|
|
|
this.pushBuffer(payload, options);
|
|
} else if (player._opt.useMSE) {
|
|
// use mse
|
|
if (type === MEDIA_TYPE.video) {
|
|
options.isIFrame = isIFrame;
|
|
}
|
|
|
|
this.pushBuffer(payload, options);
|
|
} else {
|
|
//
|
|
if (type === MEDIA_TYPE.video) {
|
|
player.decoderWorker && player.decoderWorker.decodeVideo(payload, ts, isIFrame);
|
|
} else if (type === MEDIA_TYPE.audio) {
|
|
if (player._opt.hasAudio) {
|
|
player.decoderWorker && player.decoderWorker.decodeAudio(payload, ts);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_doDecoderDecode(data) {
|
|
const player = this.player;
|
|
const {
|
|
webcodecsDecoder,
|
|
mseDecoder
|
|
} = player;
|
|
|
|
if (data.type === MEDIA_TYPE.audio) {
|
|
if (player._opt.hasAudio) {
|
|
player.decoderWorker && player.decoderWorker.decodeAudio(data.payload, data.ts);
|
|
}
|
|
} else if (data.type === MEDIA_TYPE.video) {
|
|
if (player._opt.useWCS && !player._opt.useOffscreen) {
|
|
webcodecsDecoder.decodeVideo(data.payload, data.ts, data.isIFrame);
|
|
} else if (player._opt.useMSE) {
|
|
mseDecoder.decodeVideo(data.payload, data.ts, data.isIFrame, data.cts);
|
|
}
|
|
}
|
|
}
|
|
|
|
pushBuffer(payload, options) {
|
|
// 音频
|
|
if (options.type === MEDIA_TYPE.audio) {
|
|
this.bufferList.push({
|
|
ts: options.ts,
|
|
payload: payload,
|
|
type: MEDIA_TYPE.audio
|
|
});
|
|
} else if (options.type === MEDIA_TYPE.video) {
|
|
this.bufferList.push({
|
|
ts: options.ts,
|
|
cts: options.cts,
|
|
payload: payload,
|
|
type: MEDIA_TYPE.video,
|
|
isIFrame: options.isIFrame
|
|
});
|
|
}
|
|
}
|
|
|
|
close() {}
|
|
|
|
}
|
|
|
|
class FlvLoader extends CommonLoader {
|
|
constructor(player) {
|
|
super(player);
|
|
this.input = this._inputFlv();
|
|
this.flvDemux = this.dispatchFlvData(this.input);
|
|
player.debug.log('FlvDemux', 'init');
|
|
}
|
|
|
|
destroy() {
|
|
super.destroy();
|
|
this.input = null;
|
|
this.flvDemux = null;
|
|
this.player.debug.log('FlvDemux', 'destroy');
|
|
}
|
|
|
|
dispatch(data) {
|
|
this.flvDemux(data);
|
|
}
|
|
|
|
*_inputFlv() {
|
|
yield 9;
|
|
const tmp = new ArrayBuffer(4);
|
|
const tmp8 = new Uint8Array(tmp);
|
|
const tmp32 = new Uint32Array(tmp);
|
|
const player = this.player;
|
|
|
|
while (true) {
|
|
tmp8[3] = 0;
|
|
const t = yield 15;
|
|
const type = t[4];
|
|
tmp8[0] = t[7];
|
|
tmp8[1] = t[6];
|
|
tmp8[2] = t[5];
|
|
const length = tmp32[0];
|
|
tmp8[0] = t[10];
|
|
tmp8[1] = t[9];
|
|
tmp8[2] = t[8];
|
|
let ts = tmp32[0];
|
|
|
|
if (ts === 0xFFFFFF) {
|
|
tmp8[3] = t[11];
|
|
ts = tmp32[0];
|
|
}
|
|
|
|
const payload = yield length;
|
|
|
|
switch (type) {
|
|
case FLV_MEDIA_TYPE.audio:
|
|
if (player._opt.hasAudio) {
|
|
player.updateStats({
|
|
abps: payload.byteLength
|
|
});
|
|
|
|
if (payload.byteLength > 0) {
|
|
this._doDecode(payload, MEDIA_TYPE.audio, ts);
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case FLV_MEDIA_TYPE.video:
|
|
if (!player._times.demuxStart) {
|
|
player._times.demuxStart = now();
|
|
}
|
|
|
|
if (player._opt.hasVideo) {
|
|
player.updateStats({
|
|
vbps: payload.byteLength
|
|
});
|
|
const isIFrame = payload[0] >> 4 === 1;
|
|
|
|
if (payload.byteLength > 0) {
|
|
tmp32[0] = payload[4];
|
|
tmp32[1] = payload[3];
|
|
tmp32[2] = payload[2];
|
|
tmp32[3] = 0;
|
|
let cts = tmp32[0];
|
|
|
|
this._doDecode(payload, MEDIA_TYPE.video, ts, isIFrame, cts);
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
dispatchFlvData(input) {
|
|
let need = input.next();
|
|
let buffer = null;
|
|
return value => {
|
|
let data = new Uint8Array(value);
|
|
|
|
if (buffer) {
|
|
let combine = new Uint8Array(buffer.length + data.length);
|
|
combine.set(buffer);
|
|
combine.set(data, buffer.length);
|
|
data = combine;
|
|
buffer = null;
|
|
}
|
|
|
|
while (data.length >= need.value) {
|
|
let remain = data.slice(need.value);
|
|
need = input.next(data.slice(0, need.value));
|
|
data = remain;
|
|
}
|
|
|
|
if (data.length > 0) {
|
|
buffer = data;
|
|
}
|
|
};
|
|
}
|
|
|
|
close() {
|
|
this.input && this.input.return(null);
|
|
}
|
|
|
|
}
|
|
|
|
class M7sLoader extends CommonLoader {
|
|
constructor(player) {
|
|
super(player);
|
|
player.debug.log('M7sDemux', 'init');
|
|
}
|
|
|
|
destroy() {
|
|
super.destroy();
|
|
this.player.debug.log('M7sDemux', 'destroy');
|
|
this.player = null;
|
|
}
|
|
|
|
dispatch(data) {
|
|
const player = this.player;
|
|
const dv = new DataView(data);
|
|
const type = dv.getUint8(0);
|
|
const ts = dv.getUint32(1, false);
|
|
|
|
switch (type) {
|
|
case MEDIA_TYPE.audio:
|
|
if (player._opt.hasAudio) {
|
|
const payload = new Uint8Array(data, 5);
|
|
player.updateStats({
|
|
abps: payload.byteLength
|
|
});
|
|
|
|
if (payload.byteLength > 0) {
|
|
this._doDecode(payload, type, ts);
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case MEDIA_TYPE.video:
|
|
if (player._opt.hasVideo) {
|
|
if (!player._times.demuxStart) {
|
|
player._times.demuxStart = now();
|
|
}
|
|
|
|
if (dv.byteLength > 5) {
|
|
const payload = new Uint8Array(data, 5);
|
|
const isIframe = dv.getUint8(5) >> 4 === 1;
|
|
player.updateStats({
|
|
vbps: payload.byteLength
|
|
});
|
|
|
|
if (payload.byteLength > 0) {
|
|
this._doDecode(payload, type, ts, isIframe);
|
|
}
|
|
} else {
|
|
this.player.debug.warn('M7sDemux', 'dispatch', 'dv byteLength is', dv.byteLength);
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
class Demux {
|
|
constructor(player) {
|
|
const Loader = Demux.getLoaderFactory(player._opt.demuxType);
|
|
return new Loader(player);
|
|
}
|
|
|
|
static getLoaderFactory(type) {
|
|
if (type === DEMUX_TYPE.m7s) {
|
|
return M7sLoader;
|
|
} else if (type === DEMUX_TYPE.flv) {
|
|
return FlvLoader;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/*
|
|
* Copyright (C) 2016 Bilibili. All Rights Reserved.
|
|
*
|
|
* @author zheng qian <xqq@xqq.im>
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
// Exponential-Golomb buffer decoder
|
|
class ExpGolomb {
|
|
constructor(uint8array) {
|
|
this.TAG = 'ExpGolomb';
|
|
this._buffer = uint8array;
|
|
this._buffer_index = 0;
|
|
this._total_bytes = uint8array.byteLength;
|
|
this._total_bits = uint8array.byteLength * 8;
|
|
this._current_word = 0;
|
|
this._current_word_bits_left = 0;
|
|
}
|
|
|
|
destroy() {
|
|
this._buffer = null;
|
|
}
|
|
|
|
_fillCurrentWord() {
|
|
let buffer_bytes_left = this._total_bytes - this._buffer_index;
|
|
|
|
let bytes_read = Math.min(4, buffer_bytes_left);
|
|
let word = new Uint8Array(4);
|
|
word.set(this._buffer.subarray(this._buffer_index, this._buffer_index + bytes_read));
|
|
this._current_word = new DataView(word.buffer).getUint32(0, false);
|
|
this._buffer_index += bytes_read;
|
|
this._current_word_bits_left = bytes_read * 8;
|
|
}
|
|
|
|
readBits(bits) {
|
|
|
|
if (bits <= this._current_word_bits_left) {
|
|
let result = this._current_word >>> 32 - bits;
|
|
this._current_word <<= bits;
|
|
this._current_word_bits_left -= bits;
|
|
return result;
|
|
}
|
|
|
|
let result = this._current_word_bits_left ? this._current_word : 0;
|
|
result = result >>> 32 - this._current_word_bits_left;
|
|
let bits_need_left = bits - this._current_word_bits_left;
|
|
|
|
this._fillCurrentWord();
|
|
|
|
let bits_read_next = Math.min(bits_need_left, this._current_word_bits_left);
|
|
let result2 = this._current_word >>> 32 - bits_read_next;
|
|
this._current_word <<= bits_read_next;
|
|
this._current_word_bits_left -= bits_read_next;
|
|
result = result << bits_read_next | result2;
|
|
return result;
|
|
}
|
|
|
|
readBool() {
|
|
return this.readBits(1) === 1;
|
|
}
|
|
|
|
readByte() {
|
|
return this.readBits(8);
|
|
}
|
|
|
|
_skipLeadingZero() {
|
|
let zero_count;
|
|
|
|
for (zero_count = 0; zero_count < this._current_word_bits_left; zero_count++) {
|
|
if (0 !== (this._current_word & 0x80000000 >>> zero_count)) {
|
|
this._current_word <<= zero_count;
|
|
this._current_word_bits_left -= zero_count;
|
|
return zero_count;
|
|
}
|
|
}
|
|
|
|
this._fillCurrentWord();
|
|
|
|
return zero_count + this._skipLeadingZero();
|
|
}
|
|
|
|
readUEG() {
|
|
// unsigned exponential golomb
|
|
let leading_zeros = this._skipLeadingZero();
|
|
|
|
return this.readBits(leading_zeros + 1) - 1;
|
|
}
|
|
|
|
readSEG() {
|
|
// signed exponential golomb
|
|
let value = this.readUEG();
|
|
|
|
if (value & 0x01) {
|
|
return value + 1 >>> 1;
|
|
} else {
|
|
return -1 * (value >>> 1);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/*
|
|
* Copyright (C) 2016 Bilibili. All Rights Reserved.
|
|
*
|
|
* @author zheng qian <xqq@xqq.im>
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
class SPSParser {
|
|
static _ebsp2rbsp(uint8array) {
|
|
let src = uint8array;
|
|
let src_length = src.byteLength;
|
|
let dst = new Uint8Array(src_length);
|
|
let dst_idx = 0;
|
|
|
|
for (let i = 0; i < src_length; i++) {
|
|
if (i >= 2) {
|
|
// Unescape: Skip 0x03 after 00 00
|
|
if (src[i] === 0x03 && src[i - 1] === 0x00 && src[i - 2] === 0x00) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
dst[dst_idx] = src[i];
|
|
dst_idx++;
|
|
}
|
|
|
|
return new Uint8Array(dst.buffer, 0, dst_idx);
|
|
} // 解析 SPS
|
|
// https://zhuanlan.zhihu.com/p/27896239
|
|
|
|
|
|
static parseSPS(uint8array) {
|
|
let rbsp = SPSParser._ebsp2rbsp(uint8array);
|
|
|
|
let gb = new ExpGolomb(rbsp);
|
|
gb.readByte(); // 标识当前H.264码流的profile。
|
|
// 我们知道,H.264中定义了三种常用的档次profile: 基准档次:baseline profile;主要档次:main profile; 扩展档次:extended profile;
|
|
|
|
let profile_idc = gb.readByte(); // profile_idc
|
|
|
|
gb.readByte(); // constraint_set_flags[5] + reserved_zero[3]
|
|
// 标识当前码流的Level。编码的Level定义了某种条件下的最大视频分辨率、最大视频帧率等参数,码流所遵从的level由level_idc指定。
|
|
|
|
let level_idc = gb.readByte(); // level_idc
|
|
// 表示当前的序列参数集的id。通过该id值,图像参数集pps可以引用其代表的sps中的参数。
|
|
|
|
gb.readUEG(); // seq_parameter_set_id
|
|
|
|
let profile_string = SPSParser.getProfileString(profile_idc);
|
|
let level_string = SPSParser.getLevelString(level_idc);
|
|
let chroma_format_idc = 1;
|
|
let chroma_format = 420;
|
|
let chroma_format_table = [0, 420, 422, 444];
|
|
let bit_depth = 8; //
|
|
|
|
if (profile_idc === 100 || profile_idc === 110 || profile_idc === 122 || profile_idc === 244 || profile_idc === 44 || profile_idc === 83 || profile_idc === 86 || profile_idc === 118 || profile_idc === 128 || profile_idc === 138 || profile_idc === 144) {
|
|
//
|
|
chroma_format_idc = gb.readUEG();
|
|
|
|
if (chroma_format_idc === 3) {
|
|
gb.readBits(1); // separate_colour_plane_flag
|
|
}
|
|
|
|
if (chroma_format_idc <= 3) {
|
|
chroma_format = chroma_format_table[chroma_format_idc];
|
|
}
|
|
|
|
bit_depth = gb.readUEG() + 8; // bit_depth_luma_minus8
|
|
|
|
gb.readUEG(); // bit_depth_chroma_minus8
|
|
|
|
gb.readBits(1); // qpprime_y_zero_transform_bypass_flag
|
|
|
|
if (gb.readBool()) {
|
|
// seq_scaling_matrix_present_flag
|
|
let scaling_list_count = chroma_format_idc !== 3 ? 8 : 12;
|
|
|
|
for (let i = 0; i < scaling_list_count; i++) {
|
|
if (gb.readBool()) {
|
|
// seq_scaling_list_present_flag
|
|
if (i < 6) {
|
|
SPSParser._skipScalingList(gb, 16);
|
|
} else {
|
|
SPSParser._skipScalingList(gb, 64);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} // 用于计算MaxFrameNum的值。计算公式为MaxFrameNum = 2^(log2_max_frame_num_minus4 +
|
|
|
|
|
|
gb.readUEG(); // log2_max_frame_num_minus4
|
|
// 表示解码picture order count(POC)的方法。POC是另一种计量图像序号的方式,与frame_num有着不同的计算方法。该语法元素的取值为0、1或2。
|
|
|
|
let pic_order_cnt_type = gb.readUEG();
|
|
|
|
if (pic_order_cnt_type === 0) {
|
|
gb.readUEG(); // log2_max_pic_order_cnt_lsb_minus_4
|
|
} else if (pic_order_cnt_type === 1) {
|
|
gb.readBits(1); // delta_pic_order_always_zero_flag
|
|
|
|
gb.readSEG(); // offset_for_non_ref_pic
|
|
|
|
gb.readSEG(); // offset_for_top_to_bottom_field
|
|
|
|
let num_ref_frames_in_pic_order_cnt_cycle = gb.readUEG();
|
|
|
|
for (let i = 0; i < num_ref_frames_in_pic_order_cnt_cycle; i++) {
|
|
gb.readSEG(); // offset_for_ref_frame
|
|
}
|
|
} // 用于表示参考帧的最大数目。
|
|
|
|
|
|
let ref_frames = gb.readUEG(); // max_num_ref_frames
|
|
// 标识位,说明frame_num中是否允许不连续的值。
|
|
|
|
gb.readBits(1); // gaps_in_frame_num_value_allowed_flag
|
|
// 用于计算图像的宽度。单位为宏块个数,因此图像的实际宽度为:
|
|
|
|
let pic_width_in_mbs_minus1 = gb.readUEG(); // 使用PicHeightInMapUnits来度量视频中一帧图像的高度。
|
|
// PicHeightInMapUnits并非图像明确的以像素或宏块为单位的高度,而需要考虑该宏块是帧编码或场编码。PicHeightInMapUnits的计算方式为:
|
|
|
|
let pic_height_in_map_units_minus1 = gb.readUEG(); // 标识位,说明宏块的编码方式。当该标识位为0时,宏块可能为帧编码或场编码;
|
|
// 该标识位为1时,所有宏块都采用帧编码。根据该标识位取值不同,PicHeightInMapUnits的含义也不同,
|
|
// 为0时表示一场数据按宏块计算的高度,为1时表示一帧数据按宏块计算的高度。
|
|
|
|
let frame_mbs_only_flag = gb.readBits(1);
|
|
|
|
if (frame_mbs_only_flag === 0) {
|
|
// 标识位,说明是否采用了宏块级的帧场自适应编码。当该标识位为0时,不存在帧编码和场编码之间的切换;当标识位为1时,宏块可能在帧编码和场编码模式之间进行选择。
|
|
gb.readBits(1); // mb_adaptive_frame_field_flag
|
|
} // 标识位,用于B_Skip、B_Direct模式运动矢量的推导计算。
|
|
|
|
|
|
gb.readBits(1); // direct_8x8_inference_flag
|
|
|
|
let frame_crop_left_offset = 0;
|
|
let frame_crop_right_offset = 0;
|
|
let frame_crop_top_offset = 0;
|
|
let frame_crop_bottom_offset = 0;
|
|
let frame_cropping_flag = gb.readBool();
|
|
|
|
if (frame_cropping_flag) {
|
|
frame_crop_left_offset = gb.readUEG();
|
|
frame_crop_right_offset = gb.readUEG();
|
|
frame_crop_top_offset = gb.readUEG();
|
|
frame_crop_bottom_offset = gb.readUEG();
|
|
}
|
|
|
|
let sar_width = 1,
|
|
sar_height = 1;
|
|
let fps = 0,
|
|
fps_fixed = true,
|
|
fps_num = 0,
|
|
fps_den = 0; // 标识位,说明SPS中是否存在VUI信息。
|
|
|
|
let vui_parameters_present_flag = gb.readBool();
|
|
|
|
if (vui_parameters_present_flag) {
|
|
if (gb.readBool()) {
|
|
// aspect_ratio_info_present_flag
|
|
let aspect_ratio_idc = gb.readByte();
|
|
let sar_w_table = [1, 12, 10, 16, 40, 24, 20, 32, 80, 18, 15, 64, 160, 4, 3, 2];
|
|
let sar_h_table = [1, 11, 11, 11, 33, 11, 11, 11, 33, 11, 11, 33, 99, 3, 2, 1];
|
|
|
|
if (aspect_ratio_idc > 0 && aspect_ratio_idc < 16) {
|
|
sar_width = sar_w_table[aspect_ratio_idc - 1];
|
|
sar_height = sar_h_table[aspect_ratio_idc - 1];
|
|
} else if (aspect_ratio_idc === 255) {
|
|
sar_width = gb.readByte() << 8 | gb.readByte();
|
|
sar_height = gb.readByte() << 8 | gb.readByte();
|
|
}
|
|
}
|
|
|
|
if (gb.readBool()) {
|
|
// overscan_info_present_flag
|
|
gb.readBool(); // overscan_appropriate_flag
|
|
}
|
|
|
|
if (gb.readBool()) {
|
|
// video_signal_type_present_flag
|
|
gb.readBits(4); // video_format & video_full_range_flag
|
|
|
|
if (gb.readBool()) {
|
|
// colour_description_present_flag
|
|
gb.readBits(24); // colour_primaries & transfer_characteristics & matrix_coefficients
|
|
}
|
|
}
|
|
|
|
if (gb.readBool()) {
|
|
// chroma_loc_info_present_flag
|
|
gb.readUEG(); // chroma_sample_loc_type_top_field
|
|
|
|
gb.readUEG(); // chroma_sample_loc_type_bottom_field
|
|
}
|
|
|
|
if (gb.readBool()) {
|
|
// timing_info_present_flag
|
|
let num_units_in_tick = gb.readBits(32);
|
|
let time_scale = gb.readBits(32);
|
|
fps_fixed = gb.readBool(); // fixed_frame_rate_flag
|
|
|
|
fps_num = time_scale;
|
|
fps_den = num_units_in_tick * 2;
|
|
fps = fps_num / fps_den;
|
|
}
|
|
}
|
|
|
|
let sarScale = 1;
|
|
|
|
if (sar_width !== 1 || sar_height !== 1) {
|
|
sarScale = sar_width / sar_height;
|
|
}
|
|
|
|
let crop_unit_x = 0,
|
|
crop_unit_y = 0;
|
|
|
|
if (chroma_format_idc === 0) {
|
|
crop_unit_x = 1;
|
|
crop_unit_y = 2 - frame_mbs_only_flag;
|
|
} else {
|
|
let sub_wc = chroma_format_idc === 3 ? 1 : 2;
|
|
let sub_hc = chroma_format_idc === 1 ? 2 : 1;
|
|
crop_unit_x = sub_wc;
|
|
crop_unit_y = sub_hc * (2 - frame_mbs_only_flag);
|
|
}
|
|
|
|
let codec_width = (pic_width_in_mbs_minus1 + 1) * 16;
|
|
let codec_height = (2 - frame_mbs_only_flag) * ((pic_height_in_map_units_minus1 + 1) * 16);
|
|
codec_width -= (frame_crop_left_offset + frame_crop_right_offset) * crop_unit_x;
|
|
codec_height -= (frame_crop_top_offset + frame_crop_bottom_offset) * crop_unit_y;
|
|
let present_width = Math.ceil(codec_width * sarScale);
|
|
gb.destroy();
|
|
gb = null; // 解析出来的SPS 内容。
|
|
|
|
return {
|
|
profile_string: profile_string,
|
|
// baseline, high, high10, ...
|
|
level_string: level_string,
|
|
// 3, 3.1, 4, 4.1, 5, 5.1, ...
|
|
bit_depth: bit_depth,
|
|
// 8bit, 10bit, ...
|
|
ref_frames: ref_frames,
|
|
chroma_format: chroma_format,
|
|
// 4:2:0, 4:2:2, ...
|
|
chroma_format_string: SPSParser.getChromaFormatString(chroma_format),
|
|
frame_rate: {
|
|
fixed: fps_fixed,
|
|
fps: fps,
|
|
fps_den: fps_den,
|
|
fps_num: fps_num
|
|
},
|
|
sar_ratio: {
|
|
width: sar_width,
|
|
height: sar_height
|
|
},
|
|
codec_size: {
|
|
width: codec_width,
|
|
height: codec_height
|
|
},
|
|
present_size: {
|
|
width: present_width,
|
|
height: codec_height
|
|
}
|
|
};
|
|
}
|
|
|
|
static _skipScalingList(gb, count) {
|
|
let last_scale = 8,
|
|
next_scale = 8;
|
|
let delta_scale = 0;
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
if (next_scale !== 0) {
|
|
delta_scale = gb.readSEG();
|
|
next_scale = (last_scale + delta_scale + 256) % 256;
|
|
}
|
|
|
|
last_scale = next_scale === 0 ? last_scale : next_scale;
|
|
}
|
|
} // profile_idc = 66 → baseline profile;
|
|
// profile_idc = 77 → main profile;
|
|
// profile_idc = 88 → extended profile;
|
|
// 在新版的标准中,还包括了High、High 10、High 4:2:2、High 4:4:4、High 10 Intra、High
|
|
// 4:2:2 Intra、High 4:4:4 Intra、CAVLC 4:4:4 Intra
|
|
|
|
|
|
static getProfileString(profile_idc) {
|
|
switch (profile_idc) {
|
|
case 66:
|
|
return 'Baseline';
|
|
|
|
case 77:
|
|
return 'Main';
|
|
|
|
case 88:
|
|
return 'Extended';
|
|
|
|
case 100:
|
|
return 'High';
|
|
|
|
case 110:
|
|
return 'High10';
|
|
|
|
case 122:
|
|
return 'High422';
|
|
|
|
case 244:
|
|
return 'High444';
|
|
|
|
default:
|
|
return 'Unknown';
|
|
}
|
|
}
|
|
|
|
static getLevelString(level_idc) {
|
|
return (level_idc / 10).toFixed(1);
|
|
}
|
|
|
|
static getChromaFormatString(chroma) {
|
|
switch (chroma) {
|
|
case 420:
|
|
return '4:2:0';
|
|
|
|
case 422:
|
|
return '4:2:2';
|
|
|
|
case 444:
|
|
return '4:4:4';
|
|
|
|
default:
|
|
return 'Unknown';
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
function parseAVCDecoderConfigurationRecord(arrayBuffer) {
|
|
const meta = {};
|
|
const v = new DataView(arrayBuffer.buffer);
|
|
let version = v.getUint8(0); // configurationVersion
|
|
|
|
let avcProfile = v.getUint8(1); // avcProfileIndication
|
|
|
|
v.getUint8(2); // profile_compatibil
|
|
|
|
v.getUint8(3); // AVCLevelIndication
|
|
|
|
if (version !== 1 || avcProfile === 0) {
|
|
// this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid AVCDecoderConfigurationRecord');
|
|
return meta;
|
|
}
|
|
|
|
const _naluLengthSize = (v.getUint8(4) & 3) + 1; // lengthSizeMinusOne
|
|
|
|
|
|
if (_naluLengthSize !== 3 && _naluLengthSize !== 4) {
|
|
// holy shit!!!
|
|
// this._onError(DemuxErrors.FORMAT_ERROR, `Flv: Strange NaluLengthSizeMinusOne: ${_naluLengthSize - 1}`);
|
|
return meta;
|
|
}
|
|
|
|
let spsCount = v.getUint8(5) & 31; // numOfSequenceParameterSets
|
|
|
|
if (spsCount === 0) {
|
|
// this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid AVCDecoderConfigurationRecord: No SPS');
|
|
return;
|
|
}
|
|
|
|
let offset = 6;
|
|
|
|
for (let i = 0; i < spsCount; i++) {
|
|
let len = v.getUint16(offset, false); // sequenceParameterSetLength
|
|
|
|
offset += 2;
|
|
|
|
if (len === 0) {
|
|
continue;
|
|
} // Notice: Nalu without startcode header (00 00 00 01)
|
|
|
|
|
|
let sps = new Uint8Array(arrayBuffer.buffer, offset, len);
|
|
offset += len; // flv.js作者选择了自己来解析这个数据结构,也是迫不得已,因为JS环境下没有ffmpeg,解析这个结构主要是为了提取 sps和pps。虽然理论上sps允许有多个,但其实一般就一个。
|
|
// packetTtype 为 1 表示 NALU,NALU= network abstract layer unit,这是H.264的概念,网络抽象层数据单元,其实简单理解就是一帧视频数据。
|
|
// pps的信息没什么用,所以作者只实现了sps的分析器,说明作者下了很大功夫去学习264的标准,其中的Golomb解码还是挺复杂的,能解对不容易,我在PC和手机平台都是用ffmpeg去解析的。
|
|
// SPS里面包括了视频分辨率,帧率,profile level等视频重要信息。
|
|
|
|
let config = SPSParser.parseSPS(sps);
|
|
|
|
if (i !== 0) {
|
|
// ignore other sps's config
|
|
continue;
|
|
}
|
|
|
|
meta.codecWidth = config.codec_size.width;
|
|
meta.codecHeight = config.codec_size.height;
|
|
meta.presentWidth = config.present_size.width;
|
|
meta.presentHeight = config.present_size.height;
|
|
meta.profile = config.profile_string;
|
|
meta.level = config.level_string;
|
|
meta.bitDepth = config.bit_depth;
|
|
meta.chromaFormat = config.chroma_format;
|
|
meta.sarRatio = config.sar_ratio;
|
|
meta.frameRate = config.frame_rate;
|
|
|
|
if (config.frame_rate.fixed === false || config.frame_rate.fps_num === 0 || config.frame_rate.fps_den === 0) {
|
|
meta.frameRate = {};
|
|
}
|
|
|
|
let fps_den = meta.frameRate.fps_den;
|
|
let fps_num = meta.frameRate.fps_num;
|
|
meta.refSampleDuration = meta.timescale * (fps_den / fps_num);
|
|
let codecArray = sps.subarray(1, 4);
|
|
let codecString = 'avc1.';
|
|
|
|
for (let j = 0; j < 3; j++) {
|
|
let h = codecArray[j].toString(16);
|
|
|
|
if (h.length < 2) {
|
|
h = '0' + h;
|
|
}
|
|
|
|
codecString += h;
|
|
} // codec
|
|
|
|
|
|
meta.codec = codecString;
|
|
}
|
|
|
|
let ppsCount = v.getUint8(offset); // numOfPictureParameterSets
|
|
|
|
if (ppsCount === 0) {
|
|
// this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid AVCDecoderConfigurationRecord: No PPS');
|
|
return meta;
|
|
}
|
|
|
|
offset++;
|
|
|
|
for (let i = 0; i < ppsCount; i++) {
|
|
let len = v.getUint16(offset, false); // pictureParameterSetLength
|
|
|
|
offset += 2;
|
|
|
|
if (len === 0) {
|
|
continue;
|
|
}
|
|
|
|
new Uint8Array(arrayBuffer.buffer, offset, len); // pps is useless for extracting video information
|
|
|
|
offset += len;
|
|
}
|
|
|
|
meta.videoType = 'avc'; // meta.avcc = arrayBuffer;
|
|
|
|
return meta;
|
|
}
|
|
|
|
class WebcodecsDecoder extends Emitter {
|
|
constructor(player) {
|
|
super();
|
|
this.player = player;
|
|
this.hasInit = false;
|
|
this.isDecodeFirstIIframe = false;
|
|
this.isInitInfo = false;
|
|
this.decoder = null;
|
|
this.initDecoder();
|
|
player.debug.log('Webcodecs', 'init');
|
|
}
|
|
|
|
destroy() {
|
|
if (this.decoder) {
|
|
if (this.decoder.state !== 'closed') {
|
|
this.decoder.close();
|
|
}
|
|
|
|
this.decoder = null;
|
|
}
|
|
|
|
this.hasInit = false;
|
|
this.isInitInfo = false;
|
|
this.isDecodeFirstIIframe = false;
|
|
this.off();
|
|
this.player.debug.log('Webcodecs', 'destroy');
|
|
}
|
|
|
|
initDecoder() {
|
|
const _this = this;
|
|
|
|
this.decoder = new VideoDecoder({
|
|
output(videoFrame) {
|
|
_this.handleDecode(videoFrame);
|
|
},
|
|
|
|
error(error) {
|
|
_this.handleError(error);
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
handleDecode(videoFrame) {
|
|
if (!this.isInitInfo) {
|
|
this.player.video.updateVideoInfo({
|
|
width: videoFrame.codedWidth,
|
|
height: videoFrame.codedHeight
|
|
});
|
|
this.player.video.initCanvasViewSize();
|
|
this.isInitInfo = true;
|
|
}
|
|
|
|
if (!this.player._times.videoStart) {
|
|
this.player._times.videoStart = now();
|
|
this.player.handlePlayToRenderTimes();
|
|
}
|
|
|
|
this.player.handleRender();
|
|
this.player.video.render({
|
|
videoFrame
|
|
});
|
|
this.player.updateStats({
|
|
fps: true,
|
|
ts: 0,
|
|
buf: this.player.demux.delay
|
|
});
|
|
}
|
|
|
|
handleError(error) {
|
|
this.player.debug.error('Webcodecs', 'VideoDecoder handleError', error);
|
|
}
|
|
|
|
decodeVideo(payload, ts, isIframe) {
|
|
// this.player.debug.log('Webcodecs decoder', 'decodeVideo', ts, isIframe);
|
|
if (!this.hasInit) {
|
|
if (isIframe && payload[1] === 0) {
|
|
const videoCodec = payload[0] & 0x0F;
|
|
this.player.video.updateVideoInfo({
|
|
encTypeCode: videoCodec
|
|
}); // 如果解码出来的是
|
|
|
|
if (videoCodec === VIDEO_ENC_CODE.h265) {
|
|
this.emit(EVENTS_ERROR.webcodecsH265NotSupport);
|
|
return;
|
|
}
|
|
|
|
if (!this.player._times.decodeStart) {
|
|
this.player._times.decodeStart = now();
|
|
}
|
|
|
|
const config = formatVideoDecoderConfigure(payload.slice(5));
|
|
this.decoder.configure(config);
|
|
this.hasInit = true;
|
|
}
|
|
} else {
|
|
// check width or height change
|
|
if (isIframe && payload[1] === 0) {
|
|
let data = payload.slice(5);
|
|
const config = parseAVCDecoderConfigurationRecord(data);
|
|
const videoInfo = this.player.video.videoInfo;
|
|
|
|
if (config.codecWidth !== videoInfo.width || config.codecHeight !== videoInfo.height) {
|
|
this.player.debug.log('Webcodecs', `width or height is update, width ${videoInfo.width}-> ${config.codecWidth}, height ${videoInfo.height}-> ${config.codecHeight}`);
|
|
this.player.emit(EVENTS_ERROR.webcodecsWidthOrHeightChange);
|
|
return;
|
|
}
|
|
} // fix : Uncaught DOMException: Failed to execute 'decode' on 'VideoDecoder': A key frame is required after configure() or flush().
|
|
|
|
|
|
if (!this.isDecodeFirstIIframe && isIframe) {
|
|
this.isDecodeFirstIIframe = true;
|
|
}
|
|
|
|
if (this.isDecodeFirstIIframe) {
|
|
const chunk = new EncodedVideoChunk({
|
|
data: payload.slice(5),
|
|
timestamp: ts,
|
|
type: isIframe ? ENCODED_VIDEO_TYPE.key : ENCODED_VIDEO_TYPE.delta
|
|
});
|
|
this.player.emit(EVENTS.timeUpdate, ts);
|
|
|
|
try {
|
|
if (this.isDecodeStateClosed()) {
|
|
this.player.debug.warn('Webcodecs', 'VideoDecoder isDecodeStateClosed true');
|
|
return;
|
|
}
|
|
|
|
this.decoder.decode(chunk);
|
|
} catch (e) {
|
|
this.player.debug.error('Webcodecs', 'VideoDecoder', e);
|
|
|
|
if (e.toString().indexOf(WCS_ERROR.keyframeIsRequiredError) !== -1) {
|
|
this.player.emitError(EVENTS_ERROR.webcodecsDecodeError);
|
|
} else if (e.toString().indexOf(WCS_ERROR.canNotDecodeClosedCodec) !== -1) {
|
|
this.player.emitError(EVENTS_ERROR.webcodecsDecodeError);
|
|
}
|
|
}
|
|
} else {
|
|
this.player.debug.warn('Webcodecs', 'VideoDecoder isDecodeFirstIIframe false');
|
|
}
|
|
}
|
|
}
|
|
|
|
isDecodeStateClosed() {
|
|
return this.decoder.state === 'closed';
|
|
}
|
|
|
|
}
|
|
|
|
const iconsMap = {
|
|
play: '播放',
|
|
pause: '暂停',
|
|
audio: '',
|
|
mute: '',
|
|
screenshot: '截图',
|
|
loading: '加载',
|
|
fullscreen: '全屏',
|
|
fullscreenExit: '退出全屏',
|
|
record: '录制',
|
|
recordStop: '停止录制'
|
|
};
|
|
var icons = Object.keys(iconsMap).reduce((icons, key) => {
|
|
icons[key] = `
|
|
<i class="jessibuca-icon jessibuca-icon-${key}"></i>
|
|
${iconsMap[key] ? `<span class="icon-title-tips"><span class="icon-title">${iconsMap[key]}</span></span>` : ''}
|
|
`;
|
|
return icons;
|
|
}, {});
|
|
|
|
var template = ((player, control) => {
|
|
if (player._opt.hasControl && player._opt.controlAutoHide) {
|
|
player.$container.classList.add('jessibuca-controls-show-auto-hide');
|
|
} else {
|
|
player.$container.classList.add('jessibuca-controls-show');
|
|
}
|
|
|
|
const options = player._opt;
|
|
const operateBtns = options.operateBtns;
|
|
player.$container.insertAdjacentHTML('beforeend', `
|
|
${options.background ? `<div class="jessibuca-poster" style="background-image: url(${options.background})"></div>` : ''}
|
|
<div class="jessibuca-loading">
|
|
${icons.loading}
|
|
${options.loadingText ? `<div class="jessibuca-loading-text">${options.loadingText}</div>` : ''}
|
|
</div>
|
|
${options.hasControl && operateBtns.play ? `<div class="jessibuca-play-big"></div>` : ''}
|
|
${options.hasControl ? `
|
|
<div class="jessibuca-recording">
|
|
<div class="jessibuca-recording-red-point"></div>
|
|
<div class="jessibuca-recording-time">00:00:01</div>
|
|
<div class="jessibuca-icon-recordStop jessibuca-recording-stop">${icons.recordStop}</div>
|
|
</div>
|
|
` : ''}
|
|
${options.hasControl ? `
|
|
<div class="jessibuca-controls">
|
|
<div class="jessibuca-controls-bottom">
|
|
<div class="jessibuca-controls-left">
|
|
${options.showBandwidth ? `<div class="jessibuca-controls-item jessibuca-speed"></div>` : ''}
|
|
</div>
|
|
<div class="jessibuca-controls-right">
|
|
${operateBtns.audio ? `
|
|
<div class="jessibuca-controls-item jessibuca-volume">
|
|
${icons.audio}
|
|
${icons.mute}
|
|
<div class="jessibuca-volume-panel-wrap">
|
|
<div class="jessibuca-volume-panel">
|
|
<div class="jessibuca-volume-panel-handle"></div>
|
|
</div>
|
|
<div class="jessibuca-volume-panel-text"></div>
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
${operateBtns.play ? `<div class="jessibuca-controls-item jessibuca-play">${icons.play}</div><div class="jessibuca-controls-item jessibuca-pause">${icons.pause}</div>` : ''}
|
|
${operateBtns.screenshot ? `<div class="jessibuca-controls-item jessibuca-screenshot">${icons.screenshot}</div>` : ''}
|
|
${operateBtns.record ? ` <div class="jessibuca-controls-item jessibuca-record">${icons.record}</div><div class="jessibuca-controls-item jessibuca-record-stop">${icons.recordStop}</div>` : ''}
|
|
${operateBtns.fullscreen ? `<div class="jessibuca-controls-item jessibuca-fullscreen">${icons.fullscreen}</div><div class="jessibuca-controls-item jessibuca-fullscreen-exit">${icons.fullscreenExit}</div>` : ''}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
|
|
`);
|
|
Object.defineProperty(control, '$poster', {
|
|
value: player.$container.querySelector('.jessibuca-poster')
|
|
});
|
|
Object.defineProperty(control, '$loading', {
|
|
value: player.$container.querySelector('.jessibuca-loading')
|
|
});
|
|
Object.defineProperty(control, '$play', {
|
|
value: player.$container.querySelector('.jessibuca-play')
|
|
});
|
|
Object.defineProperty(control, '$playBig', {
|
|
value: player.$container.querySelector('.jessibuca-play-big')
|
|
});
|
|
Object.defineProperty(control, '$recording', {
|
|
value: player.$container.querySelector('.jessibuca-recording')
|
|
});
|
|
Object.defineProperty(control, '$recordingTime', {
|
|
value: player.$container.querySelector('.jessibuca-recording-time')
|
|
});
|
|
Object.defineProperty(control, '$recordingStop', {
|
|
value: player.$container.querySelector('.jessibuca-recording-stop')
|
|
});
|
|
Object.defineProperty(control, '$pause', {
|
|
value: player.$container.querySelector('.jessibuca-pause')
|
|
});
|
|
Object.defineProperty(control, '$controls', {
|
|
value: player.$container.querySelector('.jessibuca-controls')
|
|
});
|
|
Object.defineProperty(control, '$fullscreen', {
|
|
value: player.$container.querySelector('.jessibuca-fullscreen')
|
|
});
|
|
Object.defineProperty(control, '$fullscreen', {
|
|
value: player.$container.querySelector('.jessibuca-fullscreen')
|
|
});
|
|
Object.defineProperty(control, '$volume', {
|
|
value: player.$container.querySelector('.jessibuca-volume')
|
|
});
|
|
Object.defineProperty(control, '$volumePanelWrap', {
|
|
value: player.$container.querySelector('.jessibuca-volume-panel-wrap')
|
|
});
|
|
Object.defineProperty(control, '$volumePanelText', {
|
|
value: player.$container.querySelector('.jessibuca-volume-panel-text')
|
|
});
|
|
Object.defineProperty(control, '$volumePanel', {
|
|
value: player.$container.querySelector('.jessibuca-volume-panel')
|
|
});
|
|
Object.defineProperty(control, '$volumeHandle', {
|
|
value: player.$container.querySelector('.jessibuca-volume-panel-handle')
|
|
});
|
|
Object.defineProperty(control, '$volumeOn', {
|
|
value: player.$container.querySelector('.jessibuca-icon-audio')
|
|
});
|
|
Object.defineProperty(control, '$volumeOff', {
|
|
value: player.$container.querySelector('.jessibuca-icon-mute')
|
|
});
|
|
Object.defineProperty(control, '$fullscreen', {
|
|
value: player.$container.querySelector('.jessibuca-fullscreen')
|
|
});
|
|
Object.defineProperty(control, '$fullscreenExit', {
|
|
value: player.$container.querySelector('.jessibuca-fullscreen-exit')
|
|
});
|
|
Object.defineProperty(control, '$record', {
|
|
value: player.$container.querySelector('.jessibuca-record')
|
|
});
|
|
Object.defineProperty(control, '$recordStop', {
|
|
value: player.$container.querySelector('.jessibuca-record-stop')
|
|
});
|
|
Object.defineProperty(control, '$screenshot', {
|
|
value: player.$container.querySelector('.jessibuca-screenshot')
|
|
});
|
|
Object.defineProperty(control, '$speed', {
|
|
value: player.$container.querySelector('.jessibuca-speed')
|
|
});
|
|
});
|
|
|
|
var observer$1 = ((player, control) => {
|
|
const {
|
|
events: {
|
|
proxy
|
|
}
|
|
} = player;
|
|
const object = document.createElement('object');
|
|
object.setAttribute('aria-hidden', 'true');
|
|
object.setAttribute('tabindex', -1);
|
|
object.type = 'text/html';
|
|
object.data = 'about:blank';
|
|
setStyle(object, {
|
|
display: 'block',
|
|
position: 'absolute',
|
|
top: '0',
|
|
left: '0',
|
|
height: '100%',
|
|
width: '100%',
|
|
overflow: 'hidden',
|
|
pointerEvents: 'none',
|
|
zIndex: '-1'
|
|
});
|
|
let playerWidth = player.width;
|
|
let playerHeight = player.height;
|
|
proxy(object, 'load', () => {
|
|
proxy(object.contentDocument.defaultView, 'resize', () => {
|
|
if (player.width !== playerWidth || player.height !== playerHeight) {
|
|
playerWidth = player.width;
|
|
playerHeight = player.height;
|
|
player.emit(EVENTS.resize);
|
|
screenfullH5Control();
|
|
}
|
|
});
|
|
});
|
|
player.$container.appendChild(object);
|
|
player.on(EVENTS.destroy, () => {
|
|
player.$container.removeChild(object);
|
|
});
|
|
|
|
function setVolumeHandle(percentage) {
|
|
if (percentage === 0) {
|
|
setStyle(control.$volumeOn, 'display', 'none');
|
|
setStyle(control.$volumeOff, 'display', 'flex');
|
|
setStyle(control.$volumeHandle, 'top', `${48}px`);
|
|
} else {
|
|
if (control.$volumeHandle && control.$volumePanel) {
|
|
const panelHeight = getStyle(control.$volumePanel, 'height') || 60;
|
|
const handleHeight = getStyle(control.$volumeHandle, 'height');
|
|
const top = panelHeight - (panelHeight - handleHeight) * percentage - handleHeight;
|
|
setStyle(control.$volumeHandle, 'top', `${top}px`);
|
|
setStyle(control.$volumeOn, 'display', 'flex');
|
|
setStyle(control.$volumeOff, 'display', 'none');
|
|
}
|
|
}
|
|
|
|
control.$volumePanelText && (control.$volumePanelText.innerHTML = parseInt(percentage * 100));
|
|
}
|
|
|
|
player.on(EVENTS.volumechange, () => {
|
|
setVolumeHandle(player.volume);
|
|
});
|
|
player.on(EVENTS.loading, flag => {
|
|
setStyle(control.$loading, 'display', flag ? 'flex' : 'none');
|
|
setStyle(control.$poster, 'display', 'none');
|
|
|
|
if (flag) {
|
|
setStyle(control.$playBig, 'display', 'none');
|
|
}
|
|
});
|
|
|
|
const screenfullChange = fullscreen => {
|
|
let isFullScreen = isBoolean(fullscreen) ? fullscreen : player.fullscreen;
|
|
setStyle(control.$fullscreenExit, 'display', isFullScreen ? 'flex' : 'none');
|
|
setStyle(control.$fullscreen, 'display', isFullScreen ? 'none' : 'flex'); // control.autoSize();
|
|
};
|
|
|
|
const screenfullH5Control = () => {
|
|
if (isMobile() && control.$controls && player._opt.useWebFullScreen) {
|
|
setTimeout(() => {
|
|
if (player.fullscreen) {
|
|
// console.log(player.width, player.height);
|
|
let translateX = player.height / 2 - player.width + CONTROL_HEIGHT / 2;
|
|
let translateY = player.height / 2 - CONTROL_HEIGHT / 2;
|
|
control.$controls.style.transform = `translateX(${-translateX}px) translateY(-${translateY}px) rotate(-90deg)`;
|
|
} else {
|
|
control.$controls.style.transform = `translateX(0) translateY(0) rotate(0)`;
|
|
}
|
|
}, 10);
|
|
}
|
|
};
|
|
|
|
try {
|
|
screenfull.on('change', screenfullChange);
|
|
player.events.destroys.push(() => {
|
|
screenfull.off('change', screenfullChange);
|
|
});
|
|
} catch (error) {//
|
|
} //
|
|
|
|
|
|
player.on(EVENTS.webFullscreen, value => {
|
|
screenfullChange(value);
|
|
screenfullH5Control();
|
|
});
|
|
player.on(EVENTS.recording, () => {
|
|
setStyle(control.$record, 'display', player.recording ? 'none' : 'flex');
|
|
setStyle(control.$recordStop, 'display', player.recording ? 'flex' : 'none');
|
|
setStyle(control.$recording, 'display', player.recording ? 'flex' : 'none');
|
|
}); //
|
|
|
|
player.on(EVENTS.recordingTimestamp, timestamp => {
|
|
// console.log(timestamp);
|
|
control.$recordingTime && (control.$recordingTime.innerHTML = formatTimeTips(timestamp));
|
|
});
|
|
player.on(EVENTS.playing, flag => {
|
|
setStyle(control.$play, 'display', flag ? 'none' : 'flex');
|
|
setStyle(control.$playBig, 'display', flag ? 'none' : 'block');
|
|
setStyle(control.$pause, 'display', flag ? 'flex' : 'none');
|
|
setStyle(control.$screenshot, 'display', flag ? 'flex' : 'none');
|
|
setStyle(control.$record, 'display', flag ? 'flex' : 'none');
|
|
setStyle(control.$qualityMenu, 'display', flag ? 'flex' : 'none');
|
|
setStyle(control.$volume, 'display', flag ? 'flex' : 'none'); // setStyle(control.$fullscreen, 'display', flag ? 'flex' : 'none');
|
|
|
|
screenfullChange(); // 不在播放
|
|
|
|
if (!flag) {
|
|
control.$speed && (control.$speed.innerHTML = bpsSize(''));
|
|
}
|
|
});
|
|
player.on(EVENTS.kBps, rate => {
|
|
const bps = bpsSize(rate);
|
|
control.$speed && (control.$speed.innerHTML = bps);
|
|
});
|
|
});
|
|
|
|
var property = ((player, control) => {
|
|
Object.defineProperty(control, 'controlsRect', {
|
|
get: () => {
|
|
return control.$controls.getBoundingClientRect();
|
|
}
|
|
});
|
|
});
|
|
|
|
var events = ((player, control) => {
|
|
const {
|
|
events: {
|
|
proxy
|
|
},
|
|
debug
|
|
} = player;
|
|
|
|
function volumeChangeFromEvent(event) {
|
|
const {
|
|
bottom: panelBottom,
|
|
height: panelHeight
|
|
} = control.$volumePanel.getBoundingClientRect();
|
|
const {
|
|
height: handleHeight
|
|
} = control.$volumeHandle.getBoundingClientRect();
|
|
let moveLen = event.y; // if (isMobile() && player.fullscreen) {
|
|
// moveLen = event.x;
|
|
// }
|
|
|
|
const percentage = clamp(panelBottom - moveLen - handleHeight / 2, 0, panelHeight - handleHeight / 2) / (panelHeight - handleHeight);
|
|
return percentage;
|
|
} //
|
|
|
|
|
|
proxy(window, ['click', 'contextmenu'], event => {
|
|
if (event.composedPath().indexOf(player.$container) > -1) {
|
|
control.isFocus = true;
|
|
} else {
|
|
control.isFocus = false;
|
|
}
|
|
}); //
|
|
|
|
proxy(window, 'orientationchange', () => {
|
|
setTimeout(() => {
|
|
player.resize();
|
|
}, 300);
|
|
});
|
|
proxy(control.$controls, 'click', e => {
|
|
e.stopPropagation();
|
|
});
|
|
proxy(control.$pause, 'click', e => {
|
|
player.pause();
|
|
}); // 监听 play 方法
|
|
|
|
proxy(control.$play, 'click', e => {
|
|
player.play();
|
|
player.resumeAudioAfterPause();
|
|
}); // 监听 play 方法
|
|
|
|
proxy(control.$playBig, 'click', e => {
|
|
player.play();
|
|
player.resumeAudioAfterPause();
|
|
});
|
|
proxy(control.$volume, 'mouseover', () => {
|
|
control.$volumePanelWrap.classList.add('jessibuca-volume-panel-wrap-show');
|
|
});
|
|
proxy(control.$volume, 'mouseout', () => {
|
|
control.$volumePanelWrap.classList.remove('jessibuca-volume-panel-wrap-show');
|
|
});
|
|
proxy(control.$volumeOn, 'click', e => {
|
|
e.stopPropagation();
|
|
setStyle(control.$volumeOn, 'display', 'none');
|
|
setStyle(control.$volumeOff, 'display', 'block');
|
|
const lastVolume = player.volume;
|
|
player.volume = 0;
|
|
player._lastVolume = lastVolume;
|
|
});
|
|
proxy(control.$volumeOff, 'click', e => {
|
|
e.stopPropagation();
|
|
setStyle(control.$volumeOn, 'display', 'block');
|
|
setStyle(control.$volumeOff, 'display', 'none');
|
|
player.volume = player.lastVolume || 0.5;
|
|
});
|
|
proxy(control.$screenshot, 'click', e => {
|
|
e.stopPropagation();
|
|
player.video.screenshot();
|
|
});
|
|
proxy(control.$volumePanel, 'click', event => {
|
|
event.stopPropagation();
|
|
player.volume = volumeChangeFromEvent(event);
|
|
});
|
|
proxy(control.$volumeHandle, 'mousedown', () => {
|
|
control.isVolumeDroging = true;
|
|
});
|
|
proxy(control.$volumeHandle, 'mousemove', event => {
|
|
if (control.isVolumeDroging) {
|
|
player.volume = volumeChangeFromEvent(event);
|
|
}
|
|
});
|
|
proxy(document, 'mouseup', () => {
|
|
if (control.isVolumeDroging) {
|
|
control.isVolumeDroging = false;
|
|
}
|
|
});
|
|
proxy(control.$record, 'click', e => {
|
|
e.stopPropagation();
|
|
player.recording = true;
|
|
});
|
|
proxy(control.$recordStop, 'click', e => {
|
|
e.stopPropagation();
|
|
player.recording = false;
|
|
});
|
|
proxy(control.$recordingStop, 'click', e => {
|
|
e.stopPropagation();
|
|
player.recording = false;
|
|
});
|
|
proxy(control.$fullscreen, 'click', e => {
|
|
e.stopPropagation();
|
|
player.fullscreen = true;
|
|
});
|
|
proxy(control.$fullscreenExit, 'click', e => {
|
|
e.stopPropagation();
|
|
player.fullscreen = false;
|
|
});
|
|
|
|
if (player._opt.hasControl && player._opt.controlAutoHide) {
|
|
//
|
|
proxy(player.$container, 'mouseover', () => {
|
|
if (!player.fullscreen) {
|
|
setStyle(control.$controls, 'display', 'block');
|
|
startDelayControlHidden();
|
|
}
|
|
});
|
|
proxy(player.$container, 'mousemove', () => {
|
|
if (player.$container && control.$controls) {
|
|
if (!player.fullscreen) {
|
|
if (control.$controls.style.display === 'none') {
|
|
setStyle(control.$controls, 'display', 'block');
|
|
startDelayControlHidden();
|
|
}
|
|
} else {
|
|
if (control.$controls.style.display === 'none') {
|
|
setStyle(control.$controls, 'display', 'block');
|
|
startDelayControlHidden();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
proxy(player.$container, 'mouseout', () => {
|
|
stopDelayControlHidden();
|
|
setStyle(control.$controls, 'display', 'none');
|
|
});
|
|
let delayHiddenTimeout = null;
|
|
|
|
const startDelayControlHidden = () => {
|
|
stopDelayControlHidden();
|
|
delayHiddenTimeout = setTimeout(() => {
|
|
setStyle(control.$controls, 'display', 'none');
|
|
}, 5 * 1000);
|
|
};
|
|
|
|
const stopDelayControlHidden = () => {
|
|
if (delayHiddenTimeout) {
|
|
clearTimeout(delayHiddenTimeout);
|
|
delayHiddenTimeout = null;
|
|
}
|
|
};
|
|
}
|
|
});
|
|
|
|
function styleInject(css, ref) {
|
|
if ( ref === void 0 ) ref = {};
|
|
var insertAt = ref.insertAt;
|
|
|
|
if (!css || typeof document === 'undefined') { return; }
|
|
|
|
var head = document.head || document.getElementsByTagName('head')[0];
|
|
var style = document.createElement('style');
|
|
style.type = 'text/css';
|
|
|
|
if (insertAt === 'top') {
|
|
if (head.firstChild) {
|
|
head.insertBefore(style, head.firstChild);
|
|
} else {
|
|
head.appendChild(style);
|
|
}
|
|
} else {
|
|
head.appendChild(style);
|
|
}
|
|
|
|
if (style.styleSheet) {
|
|
style.styleSheet.cssText = css;
|
|
} else {
|
|
style.appendChild(document.createTextNode(css));
|
|
}
|
|
}
|
|
|
|
var css_248z$1 = "@keyframes rotation{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(1turn)}}@keyframes magentaPulse{0%{background-color:#630030;-webkit-box-shadow:0 0 9px #333}50%{background-color:#a9014b;-webkit-box-shadow:0 0 18px #a9014b}to{background-color:#630030;-webkit-box-shadow:0 0 9px #333}}.jessibuca-container .jessibuca-icon{cursor:pointer;width:16px;height:16px}.jessibuca-container .jessibuca-poster{position:absolute;z-index:10;left:0;top:0;right:0;bottom:0;height:100%;width:100%;background-position:50%;background-repeat:no-repeat;background-size:contain;pointer-events:none}.jessibuca-container .jessibuca-play-big{position:absolute;display:none;height:100%;width:100%;background:rgba(0,0,0,.4)}.jessibuca-container .jessibuca-play-big:after{cursor:pointer;content:\"\";position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);display:block;width:48px;height:48px;background-image:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACgklEQVRoQ+3ZPYsTQRjA8eeZZCFlWttAwCIkZOaZJt8hlvkeHrlccuAFT6wEG0FQOeQQLCIWih6chQgKgkkKIyqKCVYip54IWmiQkTmyYhFvd3Zn3yDb7szu/7cv7GaDkPEFM94PK0DSZ9DzDAyHw7uI2HRDlVJX5/N5r9FoHCYdr/fvCRiNRmpJ6AEidoUQ15NG+AH8BgD2n9AHANAmohdJQfwAfgGA4xF4bjabnW21Whob62ILoKNfAsAGEd2PU2ATcNSNiDf0/cE5/xAHxDpgEf0NADaJ6HLUiKgAbvcjpdSGlPJZVJCoAUfdSqkLxWLxTLlc/mkbEgtgET1TSnWklLdtIuIEuN23crlcp16vv7cBSQKgu38AwBYRXQyLSArg3hsjRDxNRE+CQhIF/BN9qVAobFYqle+mkLQAdLd+8K0T0U0TRJoAbvc9fVkJId75gaQRoLv1C2STiPTb7rFLWgE6+g0RncwyYEJEtawCvjDGmpzzp5kD6NfxfD7frtVqB17xen2a7oG3ALBm+oMoFQBEPD+dTvtBfpImDXjIGFvjnD/3c7ksG5MU4HDxWeZa0HB3XhKAXcdxOn5vUi9gnIDXSqm2lHLPK8pkfVyAbSLqm4T5HRs1YB8RO0KIid8g03FRAT4rpbpSyh3TINPxUQB2GGM9zvkn05gg420CJovLZT9ISNA5tgB9ItoOGhFmnh/AcZ/X9xhj65zzV2Eiwsz1A1j2B8dHAOgS0W6YnduY6wkYj8d3lFKn/j66Ea84jtOrVqtfbQSE3YYnYDAY5Eql0hYAnNDv6kKIx2F3anO+J8DmzqLY1goQxVE12ebqDJgcrSjGrs5AFEfVZJt/AF0m+jHzUTtnAAAAAElFTkSuQmCC\");background-repeat:no-repeat;background-position:50%}.jessibuca-container .jessibuca-play-big:hover:after{background-image:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACEElEQVRoQ+2ZXStEQRjH/3/yIXwDdz7J+i7kvdisXCk3SiFJW27kglBcSFFKbqwQSa4krykuKB09Naf2Yndn5jgzc06d53Znd36/mWfeniVyHsw5PwqB0DOonYEoijYBlOpAFwCMkHwLDS/9mwhEDUCfAAyTXA4tYSLwC6CtCegegH6S56FETAR+AHRoACcBTJAUWa+RloBAXwAYIrnt0yBNgZi7qtbHgw8RFwLC/QFglOScawlXAjH3gUqrE1cirgVi7mkAYyS/0xbxJSDcdwAGSa6nKeFTIOZeUyL3aYiEEBDuLwDjJGf+KxFKIOY+BdBL8iipSGiBmHtWbbuftiJZERBuOfgGSK7aSGRJIObeUml1ayKSRQHhlgtkiaTcdltGVgUE+ppkV54FaiS78yrwqlLoOI8Cch2XV548W7WRpTVwA6DP9kGUFYEpAOUkT9LQAvtq1M+0udKkQSgBqSlJWWYxKXj8vRACK+o6bbRIdYI+Ba7U7rKjg7L53JdAhWTZBsy0rWuBXZUuNVMg23auBF7UIl2yBbJt70JAoKV6/WwLk6R9mgKSJlJ1kLTxFmkJyCla8UZd15GJQKvyumyJ8gy8DAEvfZoINPqD41EtUjmUgoaJwAaAnjrKebVI34OSq85NBNqlCAWgE0CV5GEWwI3vQlmCbcSinYFCwPEIFDPgeIC1P1/MgHaIHDf4Aydx2TF7wnKeAAAAAElFTkSuQmCC\")}.jessibuca-container .jessibuca-recording{display:none;position:absolute;left:50%;top:0;padding:0 3px;transform:translateX(-50%);justify-content:space-around;align-items:center;width:95px;height:20px;background:#000;opacity:1;border-radius:0 0 8px 8px;z-index:1}.jessibuca-container .jessibuca-recording .jessibuca-recording-red-point{width:8px;height:8px;background:#ff1f1f;border-radius:50%;animation:magentaPulse 1s linear infinite}.jessibuca-container .jessibuca-recording .jessibuca-recording-time{font-size:14px;font-weight:500;color:#ddd}.jessibuca-container .jessibuca-recording .jessibuca-icon-recordStop{width:16px;height:16px;cursor:pointer}.jessibuca-container .jessibuca-loading{display:none;flex-direction:column;justify-content:center;align-items:center;position:absolute;z-index:20;left:0;top:0;right:0;bottom:0;width:100%;height:100%;pointer-events:none}.jessibuca-container .jessibuca-loading-text{line-height:20px;font-size:13px;color:#fff;margin-top:10px}.jessibuca-container .jessibuca-controls{background-color:#161616;box-sizing:border-box;display:flex;flex-direction:column;justify-content:flex-end;position:absolute;z-index:40;left:0;right:0;bottom:0;height:38px;width:100%;padding-left:13px;padding-right:13px;font-size:14px;color:#fff;opacity:0;visibility:hidden;transition:all .2s ease-in-out;-webkit-user-select:none;user-select:none;transition:width .5s ease-in}.jessibuca-container .jessibuca-controls .jessibuca-controls-item{position:relative;display:flex;justify-content:center;padding:0 8px}.jessibuca-container .jessibuca-controls .jessibuca-controls-item:hover .icon-title-tips{visibility:visible;opacity:1}.jessibuca-container .jessibuca-controls .jessibuca-fullscreen,.jessibuca-container .jessibuca-controls .jessibuca-fullscreen-exit,.jessibuca-container .jessibuca-controls .jessibuca-icon-audio,.jessibuca-container .jessibuca-controls .jessibuca-microphone-close,.jessibuca-container .jessibuca-controls .jessibuca-pause,.jessibuca-container .jessibuca-controls .jessibuca-play,.jessibuca-container .jessibuca-controls .jessibuca-record,.jessibuca-container .jessibuca-controls .jessibuca-record-stop,.jessibuca-container .jessibuca-controls .jessibuca-screenshot{display:none}.jessibuca-container .jessibuca-controls .jessibuca-icon-audio,.jessibuca-container .jessibuca-controls .jessibuca-icon-mute{z-index:1}.jessibuca-container .jessibuca-controls .jessibuca-controls-bottom{display:flex;justify-content:space-between;height:100%}.jessibuca-container .jessibuca-controls .jessibuca-controls-bottom .jessibuca-controls-left,.jessibuca-container .jessibuca-controls .jessibuca-controls-bottom .jessibuca-controls-right{display:flex;align-items:center}.jessibuca-container.jessibuca-controls-show .jessibuca-controls{opacity:1;visibility:visible}.jessibuca-container.jessibuca-controls-show-auto-hide .jessibuca-controls{opacity:.8;visibility:visible;display:none}.jessibuca-container.jessibuca-hide-cursor *{cursor:none!important}.jessibuca-container .jessibuca-icon-loading{width:50px;height:50px;background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAHHklEQVRoQ91bfYwdVRX/nTvbPuuqlEQM0q4IRYMSP0KkaNTEEAokNUEDFr9iEIOiuCC2++4dl+Tti9nOmbfWFgryESPhH7V+IIpG8SN+Fr8qqKgQEKoUkQREwXTLs8495mze1tf35s2bfTu7ndf758y55/x+c879OvcMYYnbxMTEy4IgOImIxkRkrYisNsasUrPe+wNE9C8ielRE9iVJsndmZubBpYRES6E8DMNXeu83ENHrAJwO4OUARvrY+i+ABwDcLSJ7jDF3RlF0f9H4CiNcrVZPCIJgk4hcCOCNBQH9EYBveO93NRqNx4rQuWjCExMT64IguEJE3kdEq4sA1alDRDTsb02SZOfMzMxDi7ExMGFr7THGGCciVwKYG5PL0HTMb69UKtNTU1Ozg9gbiLC1diMRXQ/gxEGMFtDnQRHZHMfxHQvVtWDCzrkdANSredvfRWQ3Ee0F8DCAJwDs994nQRCM6qxNROu892uI6A0ATs2rWER2xHF8VV55lctN2Dl3LICvA3hzDgMPENFXROT2SqVyb71efzZHnzkRnRNGRkY2isj5AM7K0e/HAN7OzP/MIZuP8OTk5FiSJDpjnpylVER+YIzZEUXRN/MY7ydTrVbXE9FlRPT+LFkiesh7f1Ycx4/009nXw9balxDRLwC8OEPZ/SLi4jjWCCi8WWtfA2CKiN6WofzxIAhePz09/dfMj5P1slqtPj8IgntEZF0vORH51Ozs7NU7d+5sFs60Q2EYhpeKyDUZq8LDInJ6HMdP98KS6WHn3E8BvKlHZx2X72Xmry410Xb91trTiOjLAF7Rw+5uZu6FufcYds7pl7wiTSkRPSUi5zHzr5eT7LytWq32gmaz+a0MZ1zDzB9LxZ72sFqtbjDGfLcHmWeI6IwoinTfe8RarVYzzWbzJxnb2A3M/P1OgF0hPT4+XhkdHd0H4LgUNv8xxpy5devW3x4xpm2Gt2zZMjoyMnJ363DSCemJ/fv3j3XOLV2EnXMNXQ57hPIFURTdVgay8xhaq4geKVem4Jph5mr788MIV6vVtcYY9W5XI6Iboij6SJnIzmNxzl0E4Itp2IIgWDs9Pf23+XeHEQ7D8EYR+VBKx8eYeU0ZybaR1s3OxhSMNzLzh7sIb968+YUrVqxQ7z6na6ATlS6UOzG2Qlv366bj3bMHDx4c27Zt25P6/JCHnXO6Cf90yhe6l5lfXWbvto3nm4no0hSHXRVFkR56/k/YWvsbItJ0zGFNRC6K4/hLQ0JYt8FdW0si2hNF0RmHCLcSbWnr6pPM/CIAMgyEFaNz7tsAzuvEmyTJKZotmQtpa+04EV2bQuo6Zh4fFrItwu8C8PmUSP1oHMfXzxEOw3CXiGzqFPLen9NoNL43TIQ19UREmmRY0YF7FzO/k5xzLwWgYdCZaZj13h/faDT+PUyEW15OO/T8MQiCjUr4HAC6Ee/MG/+MmfNkN0r3Pay124jo4x3ADuiBRwl/EMBNKTF/SxzHl5SOTQ5AzrnLANyQsjxdooRrmk1I0TPFzPUc+ksnYq09l4i+k8aJrLXbiajr7EhEV0ZRlDZzl45gJyDNhRljfpkCdLt6WF2vIdDZPsDMnys9uxSA1tpXEdHvU1599qgknHHqu/moDOlWNkTTyu2rTGKMOfeonLQ0lFunv08AOBPAXu/9jkajsafnsgTgVma+eBjHcBbmrI3HXcxc1D1vab5b1tbyQKVSOb5erz9TGrQFAMk8POhWLI7jOwuwUxoV/Y6Hn2Hmy0uDtgAgc4RbZQt/Ttl7PrVy5crj6vW6L8BWKVS057TuAqAX0p3t3cz8hVKgLQDEIcLW2suJ6LoUnX9i5tMKsFUKFYcIZ6VpAWxiZr2xG/p2WCI+4yDxeKVSWXM0jOXDCE9OTq5JkuTRNDcS0U1RFKWdqobK612XaWEYflJEru7BYuhDu4tw66ShxSFpd0laD7meme8ZKre2gU0teXDOnQ2gV3q2FBfig37wnjUevVI/auhIlzwMSnYOe1bnPkUtWrXznuUualkM2b6EtWzJGKMlBaf0MrScZUuLJduXsAq07l1/DuCEDIP3iUi4VIVpRRCd19G3Ek8FtfTQe//DrAI1lSu69LBIogsirMK1Wm11s9n8GoC35AByH4DbvPe3r1q16g8LKS7NoXtRIrk83G4ha/bugURL93cD+Mt8+TAR6YT3j0ql8rtBC70HZb1gwmooDMO3eu+vJaKTBjXc6rfPe39ho9H41SL15O4+EOFWiGv5n2sViz83t8VuwWW9pRyY8Dxu59zJIqJVAhcP+JPHI8y8bL8SLJrwPHH9jYeI3kFEF+Ssmp/rqjN7HMe6lV2WVhjhdrRhGJ7a+lFrPYDXAtB667Q/X5723p+tNwLLwrbf1rIIEBryxpgTkyQZA6DlFccS0fMA6G84d6RVvBZht5eO/wEB1Kvsoc6vtAAAAABJRU5ErkJggg==\") no-repeat 50%;background-size:100% 100%;animation:rotation 1s linear infinite}.jessibuca-container .jessibuca-icon-screenshot{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAE5UlEQVRoQ+1YW2sdVRT+1s7JxbsoVkEUrIIX0ouz15zYNA+N1RdtQfCltlUfvLbqL/BCwZ8grbHtizQqPojgBSr0JkiMmT2nxgapqBURtPVCq7HxJCeZJVPmxDlzZubMmXOSEsnAvOy917fXt9e39tp7E5b4R0vcfywTuNgRbBgBx3HuJqLVzPzmYjprjHkcwAlmLqXNm4XAISLaSESPaq2HF4OE67rbRGRYRA7btn1fbgLGmKsA/Azg0gBkGzO/vZAkHMd5hIiqc5wHcCMz/5k0Z2oExsfHV1QqldPAf8lORNu11m8tBAljzFYAYWxRSl1vWdZvuQj4RsYYF4AVBlgIOVVlE55HRIxt23ZuCfmGjuOsJ6LPoiAistW27XfaEYmIbOYhPc9bXywWR1oiEJDYQkR1zrYjEjGyqfqbKd8a7kJVtLgQ+30i8pht2wfyRKIdmJkJBPkQTbILfudJ7CTZNBvVpggEcgpvc/ML38zESbLJsxBNE/A9biX0rdjGyTQXgbxyapdsarb0PMlXtWnGoXbKpm0Essqp3bJpK4E0OXmed3+hUBDP8w5FI91M0rdcyLLILElOCbaZilSWeXMncRx4klTCY1spfG3dhZJWx3GcDUR0EEB3ZMw0ET2gtT6SZWWzjmlrBIJCl0hAKfWgZVmHszqXZVxbCSxpCS2JJA6umIhe8ZKKVLPbaBJ+S9toqVRa53nedgAbAKwIwH4FcAzAa0R0l4i8F7PPz189k6RFRA+LyNcAXojDV0oNW5b1eW4Cxpg9AHZkSaaa6hhzb065uDSCH2LmRB8Sk9gY4293g43Qo/1pV80m8yQMfZSZ781cB1zXHRKRZ2IMpgD8A+DamL4ZItqitX4/jbQx5iEA7wLoihn3V/ACckWMJN/QWj9b1x5tGBsbW6uUOh5pPy0iL3Z2dn6ilJqanp5ep5TaJSLhF4NppdRNaU8gPmapVLrO87yfIoXuWyJ6uVKp+HmFjo6OQSJ6FcBtYT+UUmstyxqvkWuUgDFmP4AnQu2/e563qlgs+u9DNZ8xZhRAX7VRRPbath0XuXk7Y8xeAE+FgL6fnJzsHRwcLIfBR0ZGLunq6poAsDLUvp+Zw7b1r9PGmJMAbg8Z7WDmoThZuK67WkS+DD18fcPMdzSQUBR/EzN/nIC/SUQ+DPXV4dclsTHmHAD/SfHCNzc3t7Kvr++HJKeMMacA3BL0nyuXyzcPDAxMxo0fHR29slAo/Ajg6qD/fE9Pzw29vb1/x42fmJi4vFwu+5G/LOg/y8zXNJLQ2dAES5JANMQ7mfn1jBI6ycx3NiMhItqstf4oAX+ziHwQ6qvDj5NQNIn/ALCKmX+JSeIvABRD7fuY+ekGBPYBeDI05tTMzExvf3+/vz2Hk91/ET8RSeI6/DoCpVJpjed5fmKGvzMAXpqdnT3oed5Ud3d3v4jsAqBr9Ei0Rmv9VRqBBPzvROQVETnq2xJRdRu9tRF+bCVOKWT+Kvl/TSIFk6SW/LAjKfjV5K8rZABi8dOOEv7FI7Z8x6zwEWbemLbyMfJr5qiSiJ96oclymBOR3bZtP9+M89WxxpjdAHY2sN3DzM8ljWl4I3Nd9x7/OE1ENcdpETnmH3e11n41zv0l4J8RkU+J6AAz+xtF4teQQG7PFslwmcAiLfSyhC72Qv9/I/Avns2OT7QJskoAAAAASUVORK5CYII=\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-screenshot:hover{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAED0lEQVRoQ+2ZycsdRRTFf2ejqHFAMQqiYBTUoElUHLNx3GgCgpuYRF2o0UT9CxwQ/BMkMSbZSKLiQgQHUDCJgjiAxiEiESdEcJbEedgcKaj3UV+/6q7u/jovPPkK3qbr1ql76p5bt6qemPKmKfefeQKHOoLFCNg+H1gi6fFJOmv7VmCvpD1N87Yh8ApwNXCzpB2TIGF7DRDm2inpmt4EbB8LfAMcGUHWSHryYJKwfRMwmuMP4BRJv9TN2RgB2wuB72BWsq+V9MTBIGF7NZBiGzhJ0o+9CIRBtt8FLqgADC6nRDbpVO9Iuqi3hCKB5cDrGZDVkp4aIhIV2aSQyyW9MScCkcQqIOfsnCORkc3I31b5VtyFRmg1IQ7dt0ja3icSQ2C2JhAjUU2ykd+dE7tBNp2i2olAJJFuc+nCt564QTadF6IzgUhiVGiqyinKaQjZpJP2ItBXTkPJZhACXeU0pGwGI9BWTkPLZlACBTldG4o5EA6E1dY66edcyNrs8Q36zg1vVaTazNs7iXPgDVJJzYs7VRvHRzaDEohyugJ4CTi84sg/wHWSdnVxsGQ7aQLXS9pZcqpL/6AEplpCU5HE8YpJ9YrXUKQ6baN1+HPaRm1fBqwFQnKGK2ZoPwCvAo8Ai4FnMpPMHMwapHUj8DFwbw3+Dklv9iZgexOwvktSRduxU2VDlErwmyXV+lCbxLbDdndlCT3TX3vV7JgnKfRuSVflfMkSsL0ZuDMz4E/gL+CETN+/wCpJzzaRtn0D8DRwWMbu1/gCcnSm7zFJd1W/jxGwvQx4r2IYnlbuA14GAomQFw8B6YtBKFSnNj2BxEJ3IvB1pdB9CjwQ8yqYhcg/DJxZ8WOZpA/SbzkC24DbEqOfgPMkBRKzmu23gEuSj1sk5SI3Y2J7C3BHMuZz4FxJf6fgto8APgIWJd+3SUrHjr9O294HnJUMWi8pSGqs2V4CvJ88fH0i6eyChKr4KyS9WIO/Ang+6RvDz0XgABCeFEdtkaQv65yy/QVweuwPY0+T9FuNQ8cAXwHHxf7wdHiypN9r7BfEl8GjYv9+SceXJLQ/mSDYTh2Baog3SHq0pYT2STqno4RWSnqhBn8l8FzSN4bfJol/jkn8bXUS228DFyfft0paVyCwFbg9sQkSDEkctueZZju8iO+tJPEYfo7A0piYKd73wP3xnB+20cvjNnphxdmlkj4sEMjhfwY8COyOY0fb6Bkl/K6FLKxS+M1KpDhJY8mvrG5doRwlf66QZfGbjhLh4pEt35kV3iUp/IvTunU8qtTil/7gaHOY2yjpntaez9b5RmBDYewmSXfX2RRvZLYvbThOh+NuqMa9Ww1+yLnXgO2SwkZR24oEens2oYHzBCa00PMSOtQL/f+NwH+Hg8hAnbrYgQAAAABJRU5ErkJggg==\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-play{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACgklEQVRoQ+3ZPYsTQRjA8eeZZCFlWttAwCIkZOaZJt8hlvkeHrlccuAFT6wEG0FQOeQQLCIWih6chQgKgkkKIyqKCVYip54IWmiQkTmyYhFvd3Zn3yDb7szu/7cv7GaDkPEFM94PK0DSZ9DzDAyHw7uI2HRDlVJX5/N5r9FoHCYdr/fvCRiNRmpJ6AEidoUQ15NG+AH8BgD2n9AHANAmohdJQfwAfgGA4xF4bjabnW21Whob62ILoKNfAsAGEd2PU2ATcNSNiDf0/cE5/xAHxDpgEf0NADaJ6HLUiKgAbvcjpdSGlPJZVJCoAUfdSqkLxWLxTLlc/mkbEgtgET1TSnWklLdtIuIEuN23crlcp16vv7cBSQKgu38AwBYRXQyLSArg3hsjRDxNRE+CQhIF/BN9qVAobFYqle+mkLQAdLd+8K0T0U0TRJoAbvc9fVkJId75gaQRoLv1C2STiPTb7rFLWgE6+g0RncwyYEJEtawCvjDGmpzzp5kD6NfxfD7frtVqB17xen2a7oG3ALBm+oMoFQBEPD+dTvtBfpImDXjIGFvjnD/3c7ksG5MU4HDxWeZa0HB3XhKAXcdxOn5vUi9gnIDXSqm2lHLPK8pkfVyAbSLqm4T5HRs1YB8RO0KIid8g03FRAT4rpbpSyh3TINPxUQB2GGM9zvkn05gg420CJovLZT9ISNA5tgB9ItoOGhFmnh/AcZ/X9xhj65zzV2Eiwsz1A1j2B8dHAOgS0W6YnduY6wkYj8d3lFKn/j66Ea84jtOrVqtfbQSE3YYnYDAY5Eql0hYAnNDv6kKIx2F3anO+J8DmzqLY1goQxVE12ebqDJgcrSjGrs5AFEfVZJt/AF0m+jHzUTtnAAAAAElFTkSuQmCC\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-play:hover{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACEElEQVRoQ+2ZXStEQRjH/3/yIXwDdz7J+i7kvdisXCk3SiFJW27kglBcSFFKbqwQSa4krykuKB09Naf2Yndn5jgzc06d53Znd36/mWfeniVyHsw5PwqB0DOonYEoijYBlOpAFwCMkHwLDS/9mwhEDUCfAAyTXA4tYSLwC6CtCegegH6S56FETAR+AHRoACcBTJAUWa+RloBAXwAYIrnt0yBNgZi7qtbHgw8RFwLC/QFglOScawlXAjH3gUqrE1cirgVi7mkAYyS/0xbxJSDcdwAGSa6nKeFTIOZeUyL3aYiEEBDuLwDjJGf+KxFKIOY+BdBL8iipSGiBmHtWbbuftiJZERBuOfgGSK7aSGRJIObeUml1ayKSRQHhlgtkiaTcdltGVgUE+ppkV54FaiS78yrwqlLoOI8Cch2XV548W7WRpTVwA6DP9kGUFYEpAOUkT9LQAvtq1M+0udKkQSgBqSlJWWYxKXj8vRACK+o6bbRIdYI+Ba7U7rKjg7L53JdAhWTZBsy0rWuBXZUuNVMg23auBF7UIl2yBbJt70JAoKV6/WwLk6R9mgKSJlJ1kLTxFmkJyCla8UZd15GJQKvyumyJ8gy8DAEvfZoINPqD41EtUjmUgoaJwAaAnjrKebVI34OSq85NBNqlCAWgE0CV5GEWwI3vQlmCbcSinYFCwPEIFDPgeIC1P1/MgHaIHDf4Aydx2TF7wnKeAAAAAElFTkSuQmCC\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-pause{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAABA0lEQVRoQ+1YwQqCUBAcfWXXsLr2AXWTPXno8yVB8AP6Aa3oHI+kCDqYaawJljSe133uzO44bx0M/HEG/v1gAd9mkAyQgY4I/F8LJUlyrQFtD2AtIkcNoFEU+Z7n7QD4DfFHEVlocrVmgAUAIAOl3mILPcDgEFcUhyrUKMGUUcroc3NQRimj9XJBGaWMvvPydKN0o6/9QTdKN6rZANxj6EbpRulGuZnjYqs8BbyR8Ub2Izeys+u6yyAIDpo/ehzHM2NMDsA0xFsRmWhyfTIDWSXxCEBmrd2EYXjSHJqm6bQoii2AOYBL5Z0xgFxEVppcrQvQJO0zhgX0iXbdWWSADHRE4AZQ731AhEUeNwAAAABJRU5ErkJggg==\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-pause:hover{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAA7klEQVRoQ+2YSwrCQBBEX6HiVvxsPYDewfN7By/gD9ciQkvERQwJdBSiYs0mEDo96aruombEjy/9+P/jAj7NoBkwA28i8H8tFBFRA9oeWEo6ZgCNiDGwAYpn3TpKmmVytWbABQBmoNRbbqEHGB7iiuJYhRol2DJqGX1uDsuoZdRmLuNZSzGWUcuoZdRHSp/IylNgK2ErYSthK3FHwLcSvpXIjoLt9Jfa6TMwl3TIMBkRE2AH9BriL5KGmVyvWIltJXEfKN6tJJ0ym0bECFgDU+Ba+WZQFCdpkcnVuoBM0i5jXECXaNftZQbMwJsI3AAPN3dAQflHegAAAABJRU5ErkJggg==\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-record{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAC+UlEQVRoQ+1ZS2sTURT+zlDJYE3XSq219QHVuEjnJDT+Bff9Abqw2voAEfGxqygUqWhVFHGl/yMLu9BwByxk5SNI66ML6U7axjhHbmhgWiftncxoOiV3FcI53z3f/e65594zhIQPSnj86BBot4IdBToKRFyBnbeFlFIScVEiuYvIWC6Xe2YK8pcC7SYA4CMzH4mDQBXAqilQBDsLQLfPf9FxnF4i8kwwmypARI+Wl5dvmIBEsUmlUkNE9NaHsVCpVAZGR0d/m+A2JSAid3K53E0TkCg2pVKpz7KseR/GfKVSGYxMAMA0M1+JEpyJb6lUOm5ZVnkrAsVisaunp+esiByr1Wp3R0ZGvmifzZK4XQQWHMc52MgBpdQuAOcAXABwuB400ZTjONdaIjA7O5u2bVsnWU1EujzP+5nP5xdMVjvIJkCBD8x8VCm1G8AYgAkAAxt8Z5j5YmgCSqlTAJ4D2OcD/AXgATNfbYVEAIFPIvKKiE4D6GuCea8xX6gtpJT6DmBvECgRFRzHeROWRAABE4iWCbwHEFhkPM/L5vP5dyaz+23+KwHXdR3P854S0YG1ILSCuthNMfNM2OC1/RYENLY+ygcBnPfht6ZAA6BYLNr6dyqVokKhsGpaNQ2TWJstreXaE2aed133sojcj41AKyvdzCdAgSXLsk4MDw9/a/i4rntbRPxFNZoC/5jAV2be759DKTUJ4FZSFFi0bbs/k8noy2R9dAjEuWU2YgXkQOK3kD6BMsysi2Z9JC2Jdcw/ALzwPO+xvmcl7Rj177JVEbkO4BARjSflFDJJuW1dBxJPoCIiL4noDIB1BS0pW6j+oJmbm+uuVqvjRKQfLr0bZHnIzJf0f6HeAybahrUJqAPruhLlcnnPysqKfpXp11n/Gv62zoHAroS+AafT6QkiGrIsazKbzX7eVIHEt1US39gCkOzWYthkjNE+tuZujDGZQ8XRXn8N4KT5lLFZ6uaYPt+nwyDuvC80YdhvB9uOAu1WoaNAR4GIK/AHvdr+QAexB7EAAAAASUVORK5CYII=\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-record:hover{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACfUlEQVRoQ+2ZSYsUQRCFvycK4nJXXEbHBdwO4kn/gv9CD467ICIutxEFkREdFUU86T/xojcPntyQcT2INw+uISFVkD1Wd2dWlU7nUHlqisiX+fJFZGREi8yHMt8/HYG5VrBToFOg4QnMPxcyM2t4KE2nT0i6EwvylwIjQOCFpE1tEPgGfI0FamC3AFgazP8IrJL0KwZzkAI3gLMxIA1ttgCPA4w3wHpJP2NwBxG4KOlcDEgTGzNbA8wEGP57vA0CU5JONtlczFwz2wY8HUbAzBYCB4CtwCVJb33OIAXmioC70LoyBsxsEXAQOApsLIhelnS6FgEzW+5BBvwA/FS+SPJFa40KBZ5L2mxmS4AJ4IjHxCzwaUnHkgmY2V7gLrAyAPwOXJN0qg6DCgIvgQfAPsDjo2pcKddLciEz+wCs6AO6W9KjVBIVBGIgahN4BvRLMjslPYlZPbT53wR2AbeBtcUmXEFPdh5U06mbd/shBBzbr/Jx4FCAX0+BEsDMFocEYrNmFcE+BD4XsXZL0oyZnQCutkagzkn3m1NBwDe/Q9L74MAuFEqUn5op8I8JvJO0elacTALnc1HAH3Njkvwx+WeYWUegTa/pwaqIgexdyIN4uyRPmqULZRXEvulPwD3gpr+zcrtGQxfzRHYG2AAczuUWiom3kc4D2RN4BdwH9gM9CS0XFyoLGu9UuN974eIFVDiuSzruH5LqgRhtU20q8kBPV8LMlhVVmVdnYwX+SMdAZVeieAF7eeltmElJr4cpkH1bJfvGVvatxdR4bMu+teZuWxtKxWncXn8I7EldtQV7vz79fp9KwZp//9CksB8F206BuVahU6BToOEJ/Ab7+KdABdTt8AAAAABJRU5ErkJggg==\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-recordStop{background:url(\"data:image/png;base64,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\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-recordStop:hover{background:url(\"data:image/png;base64,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\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-fullscreen{background:url(\"data:image/png;base64,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\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-fullscreen:hover{background:url(\"data:image/png;base64,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\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-fullscreenExit{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAADd0lEQVRoQ+2Zz2sdVRTHv+fJBDW6anDVXen6wZszYxYBiYgtFGst3VSDunKjpS0GpUlqfjVpsVVs6aaL0or4YxMVFCJZ2ZLdPUP+gq5bQnTxtNAkfTnlhnnlkmQy9yV9780rudt77tzv5/y4v4bQ4Y06XD/2ANodwec/AiJygJnvtdvTWfPnRkBEJAiCN8rl8kMfiPn5+Ve7u7v3rays0Orq6lJfX99/PuN2auMDoAD+BvA2M6/mTWSMOUtE48D6AjHGzN/kjdlNvy+AnWOOmQ/lTSYiEwDOWzsimgrDcCRvzG76GwGw8/zJzO9sN6GInAMwbW1UdSSKoqndCMwb6wNwGsB39Q+p6h/M/C4R2dTa1AoHYBWKyCkA1+pqiWi2Wq0e7e/vf7yRoJAAKcQggMtuJKIoOtoxACnE0/xOi/SXMAxPuhCFjUBdpIjYVWXSEf0TM3/g9BeriDMKdSPEz8z8vrU1xgwT0YXCrEJZy1iSJKOqOub0/8jMA0mSfKKqNwoPkHp7ioiGHIhRIvpHVa93BEBa2JcAfOlALAHo6RgAKzRJkk9V1S6xL7kpV4idOM31taxaIKJHqmpPnMMA9hcOQES2PDJkAT1XAAC+ZebPfWB3auNzmLObVsNRUNUXVHUujuM7OxXnMy4XwOcj29mIyOuq+lapVGrYCelKpkEQ3CyXy4tbzdN0AGPMxr2iYZ+sra3FcRybtgCIiK2BKw2rdgaUSqWoUqlIkQAepFDdAF7cBq5ERI9rtdr1OI7tmE2t6SmUEYFHAEaexYW/1QC2EF+ru5GIvg7D0D2GNJxprQY4o6qv1I/b6SpzOYqiLxpWng5oOQAzXxWRWwA+dkRfYOb1p5hGW6sBJpn5KytSRG4D+KguWFXHoyhy7xdeLC0F2ChSRL4H8OFuINoKYIUbY34gogHH3eeZef1K6tPaDpCm068A3nMEDzHzxY4BUNWSiPxORO6z5aDPPlGICNQ9bYyZIaLjjudzIQoFkKbTbwCO+UI0HcB9J/LdeY0xs0R02IGYYObRrWqiFQCfEZEtSHsfmGZm+4qxbbM/hQD8BeBNa0hEM2EYnmgLgP3lFARBT1dXly4vL//b29tbzQNIU+llAHeJaLFSqRzJes5vegR8xGbZLCwsHKzVav8z8/0sm0ID+MDvAfh4qZk2exFopnd9vv0ELrXBQO7fD10AAAAASUVORK5CYII=\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-fullscreenExit:hover{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAC/ElEQVRoQ+2Zy49NQRCHvx+ReK6IlZ34E7CUiCAR4xEbTLCyQRATYswwb2IQZDYWgojHZpCQECts+ResiQwLj0RClNSkb9Lu3HtPz7mZc8+V6eXt6tP1VVV3VdcVbT7U5vozC9BqD/7/HjCzlZLet9rS9fbP9ICZvQPWSfqRAmFmS4ClMHm+JiR9S1mXVyYFwIBXwEZJv7I2MrPjQH8A6JN0OWtNM/OpAL7HS0mbsjYzswGgN8gNS+rJWtPM/HQAfJ9nkrY22tDMTgMjQaZH0nAzCmatTQE4ClyNPvQU2CbJQ2vKKB2Aa2hmR4DrkbbPgQ5Jv6sJSgkQILqA0dgTkjraBiBAxPHtPz2UtDuGKK0HKkqamd8qg5HS9yXtjebLdYjrHNRqiAeS9gQvnQGGSnML1bvGzOwc0BfN35PUaWYHgRulBwjW9ju+O4JwqM/AWFsABIgLwKkIYgJY1jYAAeJQuGIXVIVcKTKxh8WfBin9J+AVpx/eFWUEqFkyNACKp0rhgWYArkg6kQibSyylmPOklQdibijBX+fSLHFRJkDid+qKmdlaYENOI0zeEcBNSZ9qbVIEQHWuyGOTNZLetgrAz8ClPFpHa1ZL8rf5lFGEB2oBfAxQi4D5DeDmAP7mGJPka0oD4LnDr9imH/xFe8AP4vLIjBclxWXItCOtaIBjwOKo3HaFRyWdnLbmYUHhAJKumdkt4ECk9JCkSitmWixFAwxKOjt5uZvdBvZH2vZLit8XSSBFA/yjpJndAfY1A9FSgOCJu0BnBNErqfIkzfRCywECxCNgR6Rtt6TzmdqHBmyKXG4ZM4sTWc04NzNPWE+AuG3ZlZInSuGBinXMbBzYGVkrE6JUACGcHgPbUyGKAIj7REmZ18y897o5ghiQ5E/bltRChwE/kF7Xj0jyLkbDYWbzgBfA+iA4LmlXqwD8LydvszjAF0lfswBCKC0E3gBeP22p186f8RBKUbaejJmtAr5L+lBPptQAKfCzAClWmkmZWQ/MpHVTvv0X9iFAQGQyevIAAAAASUVORK5CYII=\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-audio{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACrUlEQVRoQ+2ZPYgTURCAZzbBXJnCeL2Cnb87b9MEtPBUrrMQFAtrtT5/ClGs9LBWWz0RtbBUFCF4oJDsbO68wsLA2YqQSmLlvpEHu7IuMdlLcus+yUKKhJfZ+ebnvZl5CJY/aLn+MAP41x7M1QPMfFtr/crzvHfTAs8FoNPp1LTWzwHgqIg0lFLvrQHwfX8BER8DwC6jNCIecF13wwoA3/dvIuKNpLJa60Oe560XGoCZd4rICiKeTCtaeABmPg4AJmRqg6xcaABmvg4At4aFRyEBhoVM4UMoCplHADCfJTEL5YEsIVNID5iQAYCHALCYxeq5b6PMfF5EBAAEESthGK7W6/XPRpFWq7W3VCqtZg2ZcT3g+/6i4zjzIlLSWn/yPO/DIGMNLCWY2Sj/+xGRK0qpZfNDEASnROTFVi0fr8+aA8z8Ld6KEfGt67oLYwMAwEUium8EREn7OgeAjwCwPyo/nrque3YSgAtE9GDaAM1mc65arc4Zuf1+P2w0Gt9jJZl5DQAORt+fENG5wgEw8zUAMB/zbBBRwyqAIAjuiMjlSOlNItpjFUCqWl0josMzgChR/9hGAWBbknjmAdPhDdqa0gfZzAMJKyVP4v8hhJYRcSni+0JEu63ahZj5anyQici6UuqIVQDdbrfS6/UqRulyufyTiH5sF8AlIro37VpoWEHIzGZ2tM+sEZFnSqkzk9RCS0R01wjIsZz+mug53hDRia0AnI4bGgDYISItz/M2jYC8Gpp2u30MEWuO4zha665Sqp0ZYFStX/iWchRAItFGzoHSsrJ2ZFl1mHg6bfVYJeGJv85CC++BpIJZ5kSFC6G0ha0e7mYJqcJ7IOkRay84UhD2XjHFIFZf8iW9YcYoYRi+tO6aNeupOs66iU/icV46zf/MAKZpzXFk/QL+JG1PUPhRiQAAAABJRU5ErkJggg==\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-audio:hover{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACSElEQVRoQ+2Zu4sUQRCHf5+C+gf4yBXMfMYHGvjCzEBQDIzV+HwEohipGKupD0QNDE8UEwUFTe68wEDhTMVUMFJ+0tArzbjs9u3Ojt0wBR0M9MzUV1XdXVWNKhcq1189wP/2YKcesH1d0nPgdVvgnQDY3iTpqaT9kuaAt9UA2D4o6aGkzVHpXcByFQC2r0q60lB2D7BUNIDtjZIeSDoyRNGyAWwfiiET4n6YlAtg+7Kka2PCozyAMSHT5CkLIIbMfUlbMhdmOQCZIVOeB2LI3JN0NNPq6bTZe8D2aUmOY72kN8DnoIXt7eF5FSEzkQdsB+OEsFwr6RPwbpixhqYStoPyqVwAbkaAY5KeTWD5wStZHrD9XdJgK34FhBP9H8kFOAvciQBhn3/RAcBHSTvjfx4DJ6cBOAPcbRvA9gZJYQT5DfwYKGl7UdLu+PwIOFUiwCVJYQRZBuZqA7gh6XxUegXYVhtAmq0uAnt7gLhQm9vorBZx74Hcc6D3QLKH/z2JGyVnlYs4pCfzEe4rsLW2XehicpAtAftqAwiZbhhBfgE/ZwVwDrjddi40KiG0HXpHO+KcJ8CJaXKheeBWBOgqnf6W1BwvgcOrATieFDTrJL0HViJAVwXNgVgPrJH0BfiQDTDKtREiNK7KLSnHASQLLacP1PxcVkWWq8PU3emq2yqJJ0b1Qsv2QKpdZp+orBBqmrfq5m5mSJXtgUZI1XnB0YCo94opCal6L/ka3ghtlIXqrllzT9VJ5k19Ek/y0zbf6QHatOYk3/oDujC8QMWgjf4AAAAASUVORK5CYII=\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-mute{background:url(\"data:image/png;base64,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\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-mute:hover{background:url(\"data:image/png;base64,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\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-text{font-size:14px;width:30px}.jessibuca-container .jessibuca-speed{font-size:14px;color:#fff}.jessibuca-container .jessibuca-quality-menu-list{position:absolute;left:50%;bottom:100%;visibility:hidden;opacity:0;transform:translateX(-50%);transition:visibility .3s,opacity .3s;background-color:rgba(0,0,0,.5);border-radius:4px}.jessibuca-container .jessibuca-quality-menu-list.jessibuca-quality-menu-shown{visibility:visible;opacity:1}.jessibuca-container .icon-title-tips{pointer-events:none;position:absolute;left:50%;bottom:100%;visibility:hidden;opacity:0;transform:translateX(-50%);transition:visibility .3s ease 0s,opacity .3s ease 0s;background-color:rgba(0,0,0,.5);border-radius:4px}.jessibuca-container .icon-title{display:inline-block;padding:5px 10px;font-size:12px;white-space:nowrap;color:#fff}.jessibuca-container .jessibuca-quality-menu{padding:8px 0}.jessibuca-container .jessibuca-quality-menu-item{display:block;height:25px;margin:0;padding:0 10px;cursor:pointer;font-size:14px;text-align:center;width:50px;color:hsla(0,0%,100%,.5);transition:color .3s,background-color .3s}.jessibuca-container .jessibuca-quality-menu-item:hover{background-color:hsla(0,0%,100%,.2)}.jessibuca-container .jessibuca-quality-menu-item:focus{outline:none}.jessibuca-container .jessibuca-quality-menu-item.jessibuca-quality-menu-item-active{color:#2298fc}.jessibuca-container .jessibuca-volume-panel-wrap{position:absolute;left:50%;bottom:100%;visibility:hidden;opacity:0;transform:translateX(-50%) translateY(22%);transition:visibility .3s,opacity .3s;background-color:rgba(0,0,0,.5);border-radius:4px;height:120px;width:50px;overflow:hidden}.jessibuca-container .jessibuca-volume-panel-wrap.jessibuca-volume-panel-wrap-show{visibility:visible;opacity:1}.jessibuca-container .jessibuca-volume-panel{cursor:pointer;position:absolute;top:21px;height:60px;width:50px;overflow:hidden}.jessibuca-container .jessibuca-volume-panel-text{position:absolute;left:0;top:0;width:50px;height:20px;line-height:20px;text-align:center;color:#fff;font-size:12px}.jessibuca-container .jessibuca-volume-panel-handle{position:absolute;top:48px;left:50%;width:12px;height:12px;border-radius:12px;margin-left:-6px;background:#fff}.jessibuca-container .jessibuca-volume-panel-handle:before{bottom:-54px;background:#fff}.jessibuca-container .jessibuca-volume-panel-handle:after{bottom:6px;background:hsla(0,0%,100%,.2)}.jessibuca-container .jessibuca-volume-panel-handle:after,.jessibuca-container .jessibuca-volume-panel-handle:before{content:\"\";position:absolute;display:block;left:50%;width:3px;margin-left:-1px;height:60px}.jessibuca-container.jessibuca-fullscreen-web .jessibuca-controls{width:100vh}.jessibuca-container.jessibuca-fullscreen-web .jessibuca-play-big:after{transform:translate(-50%,-50%) rotate(270deg)}.jessibuca-container.jessibuca-fullscreen-web .jessibuca-loading{flex-direction:row}.jessibuca-container.jessibuca-fullscreen-web .jessibuca-loading-text{transform:rotate(270deg)}\n/*# sourceMappingURL=data:application/json;base64,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eWZyYW1lcyByb3RhdGlvbiB7XG4gIGZyb20ge1xuICAgIC13ZWJraXQtdHJhbnNmb3JtOiByb3RhdGUoMGRlZyk7IH1cbiAgdG8ge1xuICAgIC13ZWJraXQtdHJhbnNmb3JtOiByb3RhdGUoMzYwZGVnKTsgfSB9XG5cbkBrZXlmcmFtZXMgbWFnZW50YVB1bHNlIHtcbiAgZnJvbSB7XG4gICAgYmFja2dyb3VuZC1jb2xvcjogIzYzMDAzMDtcbiAgICAtd2Via2l0LWJveC1zaGFkb3c6IDAgMCA5cHggIzMzMzsgfVxuICA1MCUge1xuICAgIGJhY2tncm91bmQtY29sb3I6ICNhOTAxNGI7XG4gICAgLXdlYmtpdC1ib3gtc2hhZG93OiAwIDAgMThweCAjYTkwMTRiOyB9XG4gIHRvIHtcbiAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjNjMwMDMwO1xuICAgIC13ZWJraXQtYm94LXNoYWRvdzogMCAwIDlweCAjMzMzOyB9IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uIHtcbiAgY3Vyc29yOiBwb2ludGVyO1xuICB3aWR0aDogMTZweDtcbiAgaGVpZ2h0OiAxNnB4OyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtcG9zdGVyIHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICB6LWluZGV4OiAxMDtcbiAgbGVmdDogMDtcbiAgdG9wOiAwO1xuICByaWdodDogMDtcbiAgYm90dG9tOiAwO1xuICBoZWlnaHQ6IDEwMCU7XG4gIHdpZHRoOiAxMDAlO1xuICBiYWNrZ3JvdW5kLXBvc2l0aW9uOiBjZW50ZXIgY2VudGVyO1xuICBiYWNrZ3JvdW5kLXJlcGVhdDogbm8tcmVwZWF0O1xuICBiYWNrZ3JvdW5kLXNpemU6IGNvbnRhaW47XG4gIHBvaW50ZXItZXZlbnRzOiBub25lOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtcGxheS1iaWcge1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIGRpc3BsYXk6IG5vbmU7XG4gIGhlaWdodDogMTAwJTtcbiAgd2lkdGg6IDEwMCU7XG4gIGJhY2tncm91bmQ6IHJnYmEoMCwgMCwgMCwgMC40KTsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXBsYXktYmlnOmFmdGVyIHtcbiAgICBjdXJzb3I6IHBvaW50ZXI7XG4gICAgY29udGVudDogJyc7XG4gICAgcG9zaXRpb246IGFic29sdXRlO1xuICAgIGxlZnQ6IDUwJTtcbiAgICB0b3A6IDUwJTtcbiAgICB0cmFuc2Zvcm06IHRyYW5zbGF0ZSgtNTAlLCAtNTAlKTtcbiAgICBkaXNwbGF5OiBibG9jaztcbiAgICB3aWR0aDogNDhweDtcbiAgICBoZWlnaHQ6IDQ4cHg7XG4gICAgYmFja2dyb3VuZC1pbWFnZTogdXJsKFwiLi4vYXNzZXRzL3BsYXkucG5nXCIpO1xuICAgIGJhY2tncm91bmQtcmVwZWF0OiBuby1yZXBlYXQ7XG4gICAgYmFja2dyb3VuZC1wb3NpdGlvbjogY2VudGVyOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtcGxheS1iaWc6aG92ZXI6YWZ0ZXIge1xuICAgIGJhY2tncm91bmQtaW1hZ2U6IHVybChcIi4uL2Fzc2V0cy9wbGF5LWhvdmVyLnBuZ1wiKTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXJlY29yZGluZyB7XG4gIGRpc3BsYXk6IG5vbmU7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgbGVmdDogNTAlO1xuICB0b3A6IDA7XG4gIHBhZGRpbmc6IDAgM3B4O1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZVgoLTUwJSk7XG4gIGp1c3RpZnktY29udGVudDogc3BhY2UtYXJvdW5kO1xuICBhbGlnbi1pdGVtczogY2VudGVyO1xuICB3aWR0aDogOTVweDtcbiAgaGVpZ2h0OiAyMHB4O1xuICBiYWNrZ3JvdW5kOiAjMDAwMDAwO1xuICBvcGFjaXR5OiAxO1xuICBib3JkZXItcmFkaXVzOiAwcHggMHB4IDhweCA4cHg7XG4gIHotaW5kZXg6IDE7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1yZWNvcmRpbmcgLmplc3NpYnVjYS1yZWNvcmRpbmctcmVkLXBvaW50IHtcbiAgICB3aWR0aDogOHB4O1xuICAgIGhlaWdodDogOHB4O1xuICAgIGJhY2tncm91bmQ6ICNGRjFGMUY7XG4gICAgYm9yZGVyLXJhZGl1czogNTAlO1xuICAgIGFuaW1hdGlvbjogbWFnZW50YVB1bHNlIDFzIGxpbmVhciBpbmZpbml0ZTsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXJlY29yZGluZyAuamVzc2lidWNhLXJlY29yZGluZy10aW1lIHtcbiAgICBmb250LXNpemU6IDE0cHg7XG4gICAgZm9udC13ZWlnaHQ6IDUwMDtcbiAgICBjb2xvcjogI0RERERERDsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXJlY29yZGluZyAuamVzc2lidWNhLWljb24tcmVjb3JkU3RvcCB7XG4gICAgd2lkdGg6IDE2cHg7XG4gICAgaGVpZ2h0OiAxNnB4O1xuICAgIGN1cnNvcjogcG9pbnRlcjsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWxvYWRpbmcge1xuICBkaXNwbGF5OiBub25lO1xuICBmbGV4LWRpcmVjdGlvbjogY29sdW1uO1xuICBqdXN0aWZ5LWNvbnRlbnQ6IGNlbnRlcjtcbiAgYWxpZ24taXRlbXM6IGNlbnRlcjtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICB6LWluZGV4OiAyMDtcbiAgbGVmdDogMDtcbiAgdG9wOiAwO1xuICByaWdodDogMDtcbiAgYm90dG9tOiAwO1xuICB3aWR0aDogMTAwJTtcbiAgaGVpZ2h0OiAxMDAlO1xuICBwb2ludGVyLWV2ZW50czogbm9uZTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWxvYWRpbmctdGV4dCB7XG4gIGxpbmUtaGVpZ2h0OiAyMHB4O1xuICBmb250LXNpemU6IDEzcHg7XG4gIGNvbG9yOiAjZmZmO1xuICBtYXJnaW4tdG9wOiAxMHB4OyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMge1xuICBiYWNrZ3JvdW5kLWNvbG9yOiAjMTYxNjE2O1xuICBib3gtc2l6aW5nOiBib3JkZXItYm94O1xuICBkaXNwbGF5OiBmbGV4O1xuICBmbGV4LWRpcmVjdGlvbjogY29sdW1uO1xuICBqdXN0aWZ5LWNvbnRlbnQ6IGZsZXgtZW5kO1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIHotaW5kZXg6IDQwO1xuICBsZWZ0OiAwO1xuICByaWdodDogMDtcbiAgYm90dG9tOiAwO1xuICBoZWlnaHQ6IDM4cHg7XG4gIHdpZHRoOiAxMDAlO1xuICBwYWRkaW5nLWxlZnQ6IDEzcHg7XG4gIHBhZGRpbmctcmlnaHQ6IDEzcHg7XG4gIGZvbnQtc2l6ZTogMTRweDtcbiAgY29sb3I6ICNmZmY7XG4gIG9wYWNpdHk6IDA7XG4gIHZpc2liaWxpdHk6IGhpZGRlbjtcbiAgdHJhbnNpdGlvbjogYWxsIDAuMnMgZWFzZS1pbi1vdXQ7XG4gIHVzZXItc2VsZWN0OiBub25lO1xuICB0cmFuc2l0aW9uOiB3aWR0aCAuNXMgZWFzZS1pbjsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWNvbnRyb2xzIC5qZXNzaWJ1Y2EtY29udHJvbHMtaXRlbSB7XG4gICAgcG9zaXRpb246IHJlbGF0aXZlO1xuICAgIGRpc3BsYXk6IGZsZXg7XG4gICAganVzdGlmeS1jb250ZW50OiBjZW50ZXI7XG4gICAgcGFkZGluZzogMCA4cHg7IH1cbiAgICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWNvbnRyb2xzIC5qZXNzaWJ1Y2EtY29udHJvbHMtaXRlbTpob3ZlciAuaWNvbi10aXRsZS10aXBzIHtcbiAgICAgIHZpc2liaWxpdHk6IHZpc2libGU7XG4gICAgICBvcGFjaXR5OiAxOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1taWNyb3Bob25lLWNsb3NlIHtcbiAgICBkaXNwbGF5OiBub25lOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1pY29uLWF1ZGlvIHtcbiAgICBkaXNwbGF5OiBub25lOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1wbGF5IHtcbiAgICBkaXNwbGF5OiBub25lOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1wYXVzZSB7XG4gICAgZGlzcGxheTogbm9uZTsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWNvbnRyb2xzIC5qZXNzaWJ1Y2EtZnVsbHNjcmVlbi1leGl0IHtcbiAgICBkaXNwbGF5OiBub25lOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1zY3JlZW5zaG90IHtcbiAgICBkaXNwbGF5OiBub25lOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1yZWNvcmQge1xuICAgIGRpc3BsYXk6IG5vbmU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1jb250cm9scyAuamVzc2lidWNhLWZ1bGxzY3JlZW4ge1xuICAgIGRpc3BsYXk6IG5vbmU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1jb250cm9scyAuamVzc2lidWNhLXJlY29yZC1zdG9wIHtcbiAgICBkaXNwbGF5OiBub25lOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1pY29uLWF1ZGlvLCAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWNvbnRyb2xzIC5qZXNzaWJ1Y2EtaWNvbi1tdXRlIHtcbiAgICB6LWluZGV4OiAxOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1jb250cm9scy1ib3R0b20ge1xuICAgIGRpc3BsYXk6IGZsZXg7XG4gICAganVzdGlmeS1jb250ZW50OiBzcGFjZS1iZXR3ZWVuO1xuICAgIGhlaWdodDogMTAwJTsgfVxuICAgIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1jb250cm9scy1ib3R0b20gLmplc3NpYnVjYS1jb250cm9scy1sZWZ0IHtcbiAgICAgIGRpc3BsYXk6IGZsZXg7XG4gICAgICBhbGlnbi1pdGVtczogY2VudGVyOyB9XG4gICAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1jb250cm9scyAuamVzc2lidWNhLWNvbnRyb2xzLWJvdHRvbSAuamVzc2lidWNhLWNvbnRyb2xzLXJpZ2h0IHtcbiAgICAgIGRpc3BsYXk6IGZsZXg7XG4gICAgICBhbGlnbi1pdGVtczogY2VudGVyOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyLmplc3NpYnVjYS1jb250cm9scy1zaG93IC5qZXNzaWJ1Y2EtY29udHJvbHMge1xuICBvcGFjaXR5OiAxO1xuICB2aXNpYmlsaXR5OiB2aXNpYmxlOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyLmplc3NpYnVjYS1jb250cm9scy1zaG93LWF1dG8taGlkZSAuamVzc2lidWNhLWNvbnRyb2xzIHtcbiAgb3BhY2l0eTogMC44O1xuICB2aXNpYmlsaXR5OiB2aXNpYmxlO1xuICBkaXNwbGF5OiBub25lOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyLmplc3NpYnVjYS1oaWRlLWN1cnNvciAqIHtcbiAgY3Vyc29yOiBub25lICFpbXBvcnRhbnQ7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLWxvYWRpbmcge1xuICB3aWR0aDogNTBweDtcbiAgaGVpZ2h0OiA1MHB4O1xuICBiYWNrZ3JvdW5kOiB1cmwoXCIuLi9hc3NldHMvbG9hZGluZy5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7XG4gIGFuaW1hdGlvbjogcm90YXRpb24gMXMgbGluZWFyIGluZmluaXRlOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtaWNvbi1zY3JlZW5zaG90IHtcbiAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL3NjcmVlbnNob3QucG5nXCIpIG5vLXJlcGVhdCBjZW50ZXI7XG4gIGJhY2tncm91bmQtc2l6ZTogMTAwJSAxMDAlOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtaWNvbi1zY3JlZW5zaG90OmhvdmVyIHtcbiAgICBiYWNrZ3JvdW5kOiB1cmwoXCIuLi9hc3NldHMvc2NyZWVuc2hvdC1ob3Zlci5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tcGxheSB7XG4gIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9wbGF5LnBuZ1wiKSBuby1yZXBlYXQgY2VudGVyO1xuICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tcGxheTpob3ZlciB7XG4gICAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL3BsYXktaG92ZXIucG5nXCIpIG5vLXJlcGVhdCBjZW50ZXI7XG4gICAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLXBhdXNlIHtcbiAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL3BhdXNlLnBuZ1wiKSBuby1yZXBlYXQgY2VudGVyO1xuICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tcGF1c2U6aG92ZXIge1xuICAgIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9wYXVzZS1ob3Zlci5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tcmVjb3JkIHtcbiAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL3JlY29yZC5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLXJlY29yZDpob3ZlciB7XG4gICAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL3JlY29yZC1ob3Zlci5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tcmVjb3JkU3RvcCB7XG4gIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9yZWNvcmQtc3RvcC5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLXJlY29yZFN0b3A6aG92ZXIge1xuICAgIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9yZWNvcmQtc3RvcC1ob3Zlci5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tZnVsbHNjcmVlbiB7XG4gIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9mdWxsc2NyZWVuLnBuZ1wiKSBuby1yZXBlYXQgY2VudGVyO1xuICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tZnVsbHNjcmVlbjpob3ZlciB7XG4gICAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL2Z1bGxzY3JlZW4taG92ZXIucG5nXCIpIG5vLXJlcGVhdCBjZW50ZXI7XG4gICAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLWZ1bGxzY3JlZW5FeGl0IHtcbiAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL2V4aXQtZnVsbHNjcmVlbi5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLWZ1bGxzY3JlZW5FeGl0OmhvdmVyIHtcbiAgICBiYWNrZ3JvdW5kOiB1cmwoXCIuLi9hc3NldHMvZXhpdC1mdWxsc2NyZWVuLWhvdmVyLnBuZ1wiKSBuby1yZXBlYXQgY2VudGVyO1xuICAgIGJhY2tncm91bmQtc2l6ZTogMTAwJSAxMDAlOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtaWNvbi1hdWRpbyB7XG4gIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9hdWRpby5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLWF1ZGlvOmhvdmVyIHtcbiAgICBiYWNrZ3JvdW5kOiB1cmwoXCIuLi9hc3NldHMvYXVkaW8taG92ZXIucG5nXCIpIG5vLXJlcGVhdCBjZW50ZXI7XG4gICAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLW11dGUge1xuICBiYWNrZ3JvdW5kOiB1cmwoXCIuLi9hc3NldHMvbXV0ZS5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLW11dGU6aG92ZXIge1xuICAgIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9tdXRlLWhvdmVyLnBuZ1wiKSBuby1yZXBlYXQgY2VudGVyO1xuICAgIGJhY2tncm91bmQtc2l6ZTogMTAwJSAxMDAlOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtaWNvbi10ZXh0IHtcbiAgZm9udC1zaXplOiAxNHB4O1xuICB3aWR0aDogMzBweDsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXNwZWVkIHtcbiAgZm9udC1zaXplOiAxNHB4O1xuICBjb2xvcjogI2ZmZjsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXF1YWxpdHktbWVudS1saXN0IHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICBsZWZ0OiA1MCU7XG4gIGJvdHRvbTogMTAwJTtcbiAgdmlzaWJpbGl0eTogaGlkZGVuO1xuICBvcGFjaXR5OiAwO1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZVgoLTUwJSk7XG4gIHRyYW5zaXRpb246IHZpc2liaWxpdHkgMzAwbXMsIG9wYWNpdHkgMzAwbXM7XG4gIGJhY2tncm91bmQtY29sb3I6IHJnYmEoMCwgMCwgMCwgMC41KTtcbiAgYm9yZGVyLXJhZGl1czogNHB4OyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtcXVhbGl0eS1tZW51LWxpc3QuamVzc2lidWNhLXF1YWxpdHktbWVudS1zaG93biB7XG4gICAgdmlzaWJpbGl0eTogdmlzaWJsZTtcbiAgICBvcGFjaXR5OiAxOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5pY29uLXRpdGxlLXRpcHMge1xuICBwb2ludGVyLWV2ZW50czogbm9uZTtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICBsZWZ0OiA1MCU7XG4gIGJvdHRvbTogMTAwJTtcbiAgdmlzaWJpbGl0eTogaGlkZGVuO1xuICBvcGFjaXR5OiAwO1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZVgoLTUwJSk7XG4gIHRyYW5zaXRpb246IHZpc2liaWxpdHkgMzAwbXMgZWFzZSAwcywgb3BhY2l0eSAzMDBtcyBlYXNlIDBzO1xuICBiYWNrZ3JvdW5kLWNvbG9yOiByZ2JhKDAsIDAsIDAsIDAuNSk7XG4gIGJvcmRlci1yYWRpdXM6IDRweDsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuaWNvbi10aXRsZSB7XG4gIGRpc3BsYXk6IGlubGluZS1ibG9jaztcbiAgcGFkZGluZzogNXB4IDEwcHg7XG4gIGZvbnQtc2l6ZTogMTJweDtcbiAgd2hpdGUtc3BhY2U6IG5vd3JhcDtcbiAgY29sb3I6IHdoaXRlOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtcXVhbGl0eS1tZW51IHtcbiAgcGFkZGluZzogOHB4IDA7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1xdWFsaXR5LW1lbnUtaXRlbSB7XG4gIGRpc3BsYXk6IGJsb2NrO1xuICBoZWlnaHQ6IDI1cHg7XG4gIG1hcmdpbjogMDtcbiAgcGFkZGluZzogMCAxMHB4O1xuICBjdXJzb3I6IHBvaW50ZXI7XG4gIGZvbnQtc2l6ZTogMTRweDtcbiAgdGV4dC1hbGlnbjogY2VudGVyO1xuICB3aWR0aDogNTBweDtcbiAgY29sb3I6IHJnYmEoMjU1LCAyNTUsIDI1NSwgMC41KTtcbiAgdHJhbnNpdGlvbjogY29sb3IgMzAwbXMsIGJhY2tncm91bmQtY29sb3IgMzAwbXM7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1xdWFsaXR5LW1lbnUtaXRlbTpob3ZlciB7XG4gICAgYmFja2dyb3VuZC1jb2xvcjogcmdiYSgyNTUsIDI1NSwgMjU1LCAwLjIpOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtcXVhbGl0eS1tZW51LWl0ZW06Zm9jdXMge1xuICAgIG91dGxpbmU6IG5vbmU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1xdWFsaXR5LW1lbnUtaXRlbS5qZXNzaWJ1Y2EtcXVhbGl0eS1tZW51LWl0ZW0tYWN0aXZlIHtcbiAgICBjb2xvcjogIzIyOThGQzsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXZvbHVtZS1wYW5lbC13cmFwIHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICBsZWZ0OiA1MCU7XG4gIGJvdHRvbTogMTAwJTtcbiAgdmlzaWJpbGl0eTogaGlkZGVuO1xuICBvcGFjaXR5OiAwO1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZVgoLTUwJSkgdHJhbnNsYXRlWSgyMiUpO1xuICB0cmFuc2l0aW9uOiB2aXNpYmlsaXR5IDMwMG1zLCBvcGFjaXR5IDMwMG1zO1xuICBiYWNrZ3JvdW5kLWNvbG9yOiByZ2JhKDAsIDAsIDAsIDAuNSk7XG4gIGJvcmRlci1yYWRpdXM6IDRweDtcbiAgaGVpZ2h0OiAxMjBweDtcbiAgd2lkdGg6IDUwcHg7XG4gIG92ZXJmbG93OiBoaWRkZW47IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS12b2x1bWUtcGFuZWwtd3JhcC5qZXNzaWJ1Y2Etdm9sdW1lLXBhbmVsLXdyYXAtc2hvdyB7XG4gICAgdmlzaWJpbGl0eTogdmlzaWJsZTtcbiAgICBvcGFjaXR5OiAxOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2Etdm9sdW1lLXBhbmVsIHtcbiAgY3Vyc29yOiBwb2ludGVyO1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIHRvcDogMjFweDtcbiAgaGVpZ2h0OiA2MHB4O1xuICB3aWR0aDogNTBweDtcbiAgb3ZlcmZsb3c6IGhpZGRlbjsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXZvbHVtZS1wYW5lbC10ZXh0IHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICBsZWZ0OiAwO1xuICB0b3A6IDA7XG4gIHdpZHRoOiA1MHB4O1xuICBoZWlnaHQ6IDIwcHg7XG4gIGxpbmUtaGVpZ2h0OiAyMHB4O1xuICB0ZXh0LWFsaWduOiBjZW50ZXI7XG4gIGNvbG9yOiAjZmZmO1xuICBmb250LXNpemU6IDEycHg7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS12b2x1bWUtcGFuZWwtaGFuZGxlIHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICB0b3A6IDQ4cHg7XG4gIGxlZnQ6IDUwJTtcbiAgd2lkdGg6IDEycHg7XG4gIGhlaWdodDogMTJweDtcbiAgYm9yZGVyLXJhZGl1czogMTJweDtcbiAgbWFyZ2luLWxlZnQ6IC02cHg7XG4gIGJhY2tncm91bmQ6ICNmZmY7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS12b2x1bWUtcGFuZWwtaGFuZGxlOjpiZWZvcmUge1xuICAgIGJvdHRvbTogLTU0cHg7XG4gICAgYmFja2dyb3VuZDogI2ZmZjsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXZvbHVtZS1wYW5lbC1oYW5kbGU6OmFmdGVyIHtcbiAgICBib3R0b206IDZweDtcbiAgICBiYWNrZ3JvdW5kOiByZ2JhKDI1NSwgMjU1LCAyNTUsIDAuMik7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS12b2x1bWUtcGFuZWwtaGFuZGxlOjpiZWZvcmUsIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2Etdm9sdW1lLXBhbmVsLWhhbmRsZTo6YWZ0ZXIge1xuICAgIGNvbnRlbnQ6ICcnO1xuICAgIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgICBkaXNwbGF5OiBibG9jaztcbiAgICBsZWZ0OiA1MCU7XG4gICAgd2lkdGg6IDNweDtcbiAgICBtYXJnaW4tbGVmdDogLTFweDtcbiAgICBoZWlnaHQ6IDYwcHg7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIuamVzc2lidWNhLWZ1bGxzY3JlZW4td2ViIC5qZXNzaWJ1Y2EtY29udHJvbHMge1xuICB3aWR0aDogMTAwdmg7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIuamVzc2lidWNhLWZ1bGxzY3JlZW4td2ViIC5qZXNzaWJ1Y2EtcGxheS1iaWc6YWZ0ZXIge1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZSgtNTAlLCAtNTAlKSByb3RhdGUoMjcwZGVnKTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lci5qZXNzaWJ1Y2EtZnVsbHNjcmVlbi13ZWIgLmplc3NpYnVjYS1sb2FkaW5nIHtcbiAgZmxleC1kaXJlY3Rpb246IHJvdzsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lci5qZXNzaWJ1Y2EtZnVsbHNjcmVlbi13ZWIgLmplc3NpYnVjYS1sb2FkaW5nLXRleHQge1xuICB0cmFuc2Zvcm06IHJvdGF0ZSgyNzBkZWcpOyB9XG4iXX0= */";
|
|
styleInject(css_248z$1);
|
|
|
|
// todo: 待定
|
|
var hotkey = ((player, control) => {
|
|
const {
|
|
events: {
|
|
proxy
|
|
}
|
|
} = player;
|
|
const keys = {};
|
|
|
|
function addHotkey(key, event) {
|
|
if (keys[key]) {
|
|
keys[key].push(event);
|
|
} else {
|
|
keys[key] = [event];
|
|
}
|
|
} //
|
|
|
|
|
|
addHotkey(HOT_KEY.esc, () => {
|
|
if (player.fullscreen) {
|
|
player.fullscreen = false;
|
|
}
|
|
}); //
|
|
|
|
addHotkey(HOT_KEY.arrowUp, () => {
|
|
player.volume += 0.05;
|
|
}); //
|
|
|
|
addHotkey(HOT_KEY.arrowDown, () => {
|
|
player.volume -= 0.05;
|
|
});
|
|
proxy(window, 'keydown', event => {
|
|
if (control.isFocus) {
|
|
const tag = document.activeElement.tagName.toUpperCase();
|
|
const editable = document.activeElement.getAttribute('contenteditable');
|
|
|
|
if (tag !== 'INPUT' && tag !== 'TEXTAREA' && editable !== '' && editable !== 'true') {
|
|
const events = keys[event.keyCode];
|
|
|
|
if (events) {
|
|
event.preventDefault();
|
|
events.forEach(fn => fn());
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
class Control {
|
|
constructor(player) {
|
|
this.player = player;
|
|
template(player, this);
|
|
property(player, this);
|
|
observer$1(player, this);
|
|
events(player, this);
|
|
|
|
if (player._opt.hotKey) {
|
|
hotkey(player, this);
|
|
}
|
|
|
|
this.player.debug.log('Control', 'init');
|
|
}
|
|
|
|
destroy() {
|
|
if (this.$poster) {
|
|
this.player.$container.removeChild(this.$poster);
|
|
}
|
|
|
|
if (this.$loading) {
|
|
this.player.$container.removeChild(this.$loading);
|
|
}
|
|
|
|
if (this.$controls) {
|
|
this.player.$container.removeChild(this.$controls);
|
|
}
|
|
|
|
if (this.$recording) {
|
|
this.player.$container.removeChild(this.$recording);
|
|
}
|
|
|
|
if (this.$playBig) {
|
|
this.player.$container.removeChild(this.$playBig);
|
|
}
|
|
|
|
this.player.debug.log('control', 'destroy');
|
|
}
|
|
|
|
autoSize() {
|
|
const player = this.player;
|
|
player.$container.style.padding = '0 0';
|
|
const playerWidth = player.width;
|
|
const playerHeight = player.height;
|
|
const playerRatio = playerWidth / playerHeight;
|
|
const canvasWidth = player.video.$videoElement.width;
|
|
const canvasHeight = player.video.$videoElement.height;
|
|
const canvasRatio = canvasWidth / canvasHeight;
|
|
|
|
if (playerRatio > canvasRatio) {
|
|
const padding = (playerWidth - playerHeight * canvasRatio) / 2;
|
|
player.$container.style.padding = `0 ${padding}px`;
|
|
} else {
|
|
const padding = (playerHeight - playerWidth / canvasRatio) / 2;
|
|
player.$container.style.padding = `${padding}px 0`;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
var css_248z = ".jessibuca-container{position:relative;display:block;width:100%;height:100%;overflow:hidden}.jessibuca-container.jessibuca-fullscreen-web{position:fixed;z-index:9999;left:0;top:0;right:0;bottom:0;width:100vw!important;height:100vh!important;background:#000}\n/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInN0eWxlLnNjc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEscUJBQ0UsaUJBQWtCLENBQ2xCLGFBQWMsQ0FDZCxVQUFXLENBQ1gsV0FBWSxDQUNaLGVBQWtCLENBQ2xCLDhDQUNFLGNBQWUsQ0FDZixZQUFhLENBQ2IsTUFBTyxDQUNQLEtBQU0sQ0FDTixPQUFRLENBQ1IsUUFBUyxDQUNULHFCQUF1QixDQUN2QixzQkFBd0IsQ0FDeEIsZUFBa0IiLCJmaWxlIjoic3R5bGUuc2NzcyIsInNvdXJjZXNDb250ZW50IjpbIi5qZXNzaWJ1Y2EtY29udGFpbmVyIHtcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xuICBkaXNwbGF5OiBibG9jaztcbiAgd2lkdGg6IDEwMCU7XG4gIGhlaWdodDogMTAwJTtcbiAgb3ZlcmZsb3c6IGhpZGRlbjsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lci5qZXNzaWJ1Y2EtZnVsbHNjcmVlbi13ZWIge1xuICAgIHBvc2l0aW9uOiBmaXhlZDtcbiAgICB6LWluZGV4OiA5OTk5O1xuICAgIGxlZnQ6IDA7XG4gICAgdG9wOiAwO1xuICAgIHJpZ2h0OiAwO1xuICAgIGJvdHRvbTogMDtcbiAgICB3aWR0aDogMTAwdncgIWltcG9ydGFudDtcbiAgICBoZWlnaHQ6IDEwMHZoICFpbXBvcnRhbnQ7XG4gICAgYmFja2dyb3VuZDogIzAwMDsgfVxuIl19 */";
|
|
styleInject(css_248z);
|
|
|
|
var observer = (player => {
|
|
const {
|
|
_opt,
|
|
debug,
|
|
events: {
|
|
proxy
|
|
}
|
|
} = player;
|
|
|
|
if (_opt.supportDblclickFullscreen) {
|
|
proxy(player.$container, 'dblclick', e => {
|
|
const target = getTarget(e);
|
|
const nodeName = target.nodeName.toLowerCase();
|
|
|
|
if (nodeName === 'canvas' || nodeName === 'video') {
|
|
player.fullscreen = !player.fullscreen;
|
|
}
|
|
});
|
|
} //
|
|
|
|
|
|
proxy(document, 'visibilitychange', () => {
|
|
if (_opt.hiddenAutoPause) {
|
|
debug.log('visibilitychange', document.visibilityState, player._isPlayingBeforePageHidden);
|
|
|
|
if ("visible" === document.visibilityState) {
|
|
if (player._isPlayingBeforePageHidden) {
|
|
player.play();
|
|
}
|
|
} else {
|
|
player._isPlayingBeforePageHidden = player.playing; // hidden
|
|
|
|
if (player.playing) {
|
|
player.pause();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
proxy(window, 'fullscreenchange', () => {
|
|
//
|
|
if (player.keepScreenOn !== null && "visible" === document.visibilityState) {
|
|
player.enableWakeLock();
|
|
}
|
|
});
|
|
});
|
|
|
|
class MP4$1 {
|
|
static init() {
|
|
MP4$1.types = {
|
|
avc1: [],
|
|
avcC: [],
|
|
hvc1: [],
|
|
hvcC: [],
|
|
btrt: [],
|
|
dinf: [],
|
|
dref: [],
|
|
esds: [],
|
|
ftyp: [],
|
|
hdlr: [],
|
|
mdat: [],
|
|
mdhd: [],
|
|
mdia: [],
|
|
mfhd: [],
|
|
minf: [],
|
|
moof: [],
|
|
moov: [],
|
|
mp4a: [],
|
|
mvex: [],
|
|
mvhd: [],
|
|
sdtp: [],
|
|
stbl: [],
|
|
stco: [],
|
|
stsc: [],
|
|
stsd: [],
|
|
stsz: [],
|
|
stts: [],
|
|
tfdt: [],
|
|
tfhd: [],
|
|
traf: [],
|
|
trak: [],
|
|
trun: [],
|
|
trex: [],
|
|
tkhd: [],
|
|
vmhd: [],
|
|
smhd: []
|
|
};
|
|
|
|
for (let name in MP4$1.types) {
|
|
if (MP4$1.types.hasOwnProperty(name)) {
|
|
MP4$1.types[name] = [name.charCodeAt(0), name.charCodeAt(1), name.charCodeAt(2), name.charCodeAt(3)];
|
|
}
|
|
}
|
|
|
|
let constants = MP4$1.constants = {};
|
|
constants.FTYP = new Uint8Array([0x69, 0x73, 0x6F, 0x6D, // major_brand: isom
|
|
0x0, 0x0, 0x0, 0x1, // minor_version: 0x01
|
|
0x69, 0x73, 0x6F, 0x6D, // isom
|
|
0x61, 0x76, 0x63, 0x31 // avc1
|
|
]);
|
|
constants.STSD_PREFIX = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags
|
|
0x00, 0x00, 0x00, 0x01 // entry_count
|
|
]);
|
|
constants.STTS = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags
|
|
0x00, 0x00, 0x00, 0x00 // entry_count
|
|
]);
|
|
constants.STSC = constants.STCO = constants.STTS;
|
|
constants.STSZ = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags
|
|
0x00, 0x00, 0x00, 0x00, // sample_size
|
|
0x00, 0x00, 0x00, 0x00 // sample_count
|
|
]);
|
|
constants.HDLR_VIDEO = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags
|
|
0x00, 0x00, 0x00, 0x00, // pre_defined
|
|
0x76, 0x69, 0x64, 0x65, // handler_type: 'vide'
|
|
0x00, 0x00, 0x00, 0x00, // reserved: 3 * 4 bytes
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x56, 0x69, 0x64, 0x65, 0x6F, 0x48, 0x61, 0x6E, 0x64, 0x6C, 0x65, 0x72, 0x00 // name: VideoHandler
|
|
]);
|
|
constants.HDLR_AUDIO = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags
|
|
0x00, 0x00, 0x00, 0x00, // pre_defined
|
|
0x73, 0x6F, 0x75, 0x6E, // handler_type: 'soun'
|
|
0x00, 0x00, 0x00, 0x00, // reserved: 3 * 4 bytes
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0x6F, 0x75, 0x6E, 0x64, 0x48, 0x61, 0x6E, 0x64, 0x6C, 0x65, 0x72, 0x00 // name: SoundHandler
|
|
]);
|
|
constants.DREF = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags
|
|
0x00, 0x00, 0x00, 0x01, // entry_count
|
|
0x00, 0x00, 0x00, 0x0C, // entry_size
|
|
0x75, 0x72, 0x6C, 0x20, // type 'url '
|
|
0x00, 0x00, 0x00, 0x01 // version(0) + flags
|
|
]); // Sound media header
|
|
|
|
constants.SMHD = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags
|
|
0x00, 0x00, 0x00, 0x00 // balance(2) + reserved(2)
|
|
]); // video media header
|
|
|
|
constants.VMHD = new Uint8Array([0x00, 0x00, 0x00, 0x01, // version(0) + flags
|
|
0x00, 0x00, // graphicsmode: 2 bytes
|
|
0x00, 0x00, 0x00, 0x00, // opcolor: 3 * 2 bytes
|
|
0x00, 0x00]);
|
|
} // Generate a box
|
|
|
|
|
|
static box(type) {
|
|
let size = 8;
|
|
let result = null;
|
|
let datas = Array.prototype.slice.call(arguments, 1);
|
|
let arrayCount = datas.length;
|
|
|
|
for (let i = 0; i < arrayCount; i++) {
|
|
size += datas[i].byteLength;
|
|
}
|
|
|
|
result = new Uint8Array(size);
|
|
result[0] = size >>> 24 & 0xFF; // size
|
|
|
|
result[1] = size >>> 16 & 0xFF;
|
|
result[2] = size >>> 8 & 0xFF;
|
|
result[3] = size & 0xFF;
|
|
result.set(type, 4); // type
|
|
|
|
let offset = 8;
|
|
|
|
for (let i = 0; i < arrayCount; i++) {
|
|
// data body
|
|
result.set(datas[i], offset);
|
|
offset += datas[i].byteLength;
|
|
}
|
|
|
|
return result;
|
|
} // emit ftyp & moov
|
|
|
|
|
|
static generateInitSegment(meta) {
|
|
let ftyp = MP4$1.box(MP4$1.types.ftyp, MP4$1.constants.FTYP);
|
|
let moov = MP4$1.moov(meta);
|
|
let result = new Uint8Array(ftyp.byteLength + moov.byteLength);
|
|
result.set(ftyp, 0);
|
|
result.set(moov, ftyp.byteLength);
|
|
return result;
|
|
} // Movie metadata box
|
|
|
|
|
|
static moov(meta) {
|
|
let mvhd = MP4$1.mvhd(meta.timescale, meta.duration);
|
|
let trak = MP4$1.trak(meta);
|
|
let mvex = MP4$1.mvex(meta);
|
|
return MP4$1.box(MP4$1.types.moov, mvhd, trak, mvex);
|
|
} // Movie header box
|
|
|
|
|
|
static mvhd(timescale, duration) {
|
|
return MP4$1.box(MP4$1.types.mvhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags
|
|
0x00, 0x00, 0x00, 0x00, // creation_time
|
|
0x00, 0x00, 0x00, 0x00, // modification_time
|
|
timescale >>> 24 & 0xFF, // timescale: 4 bytes
|
|
timescale >>> 16 & 0xFF, timescale >>> 8 & 0xFF, timescale & 0xFF, duration >>> 24 & 0xFF, // duration: 4 bytes
|
|
duration >>> 16 & 0xFF, duration >>> 8 & 0xFF, duration & 0xFF, 0x00, 0x01, 0x00, 0x00, // Preferred rate: 1.0
|
|
0x01, 0x00, 0x00, 0x00, // PreferredVolume(1.0, 2bytes) + reserved(2bytes)
|
|
0x00, 0x00, 0x00, 0x00, // reserved: 4 + 4 bytes
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, // ----begin composition matrix----
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, // ----end composition matrix----
|
|
0x00, 0x00, 0x00, 0x00, // ----begin pre_defined 6 * 4 bytes----
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ----end pre_defined 6 * 4 bytes----
|
|
0xFF, 0xFF, 0xFF, 0xFF // next_track_ID
|
|
]));
|
|
} // Track box
|
|
|
|
|
|
static trak(meta) {
|
|
return MP4$1.box(MP4$1.types.trak, MP4$1.tkhd(meta), MP4$1.mdia(meta));
|
|
} // Track header box
|
|
|
|
|
|
static tkhd(meta) {
|
|
let trackId = meta.id,
|
|
duration = meta.duration;
|
|
let width = meta.presentWidth,
|
|
height = meta.presentHeight;
|
|
return MP4$1.box(MP4$1.types.tkhd, new Uint8Array([0x00, 0x00, 0x00, 0x07, // version(0) + flags
|
|
0x00, 0x00, 0x00, 0x00, // creation_time
|
|
0x00, 0x00, 0x00, 0x00, // modification_time
|
|
trackId >>> 24 & 0xFF, // track_ID: 4 bytes
|
|
trackId >>> 16 & 0xFF, trackId >>> 8 & 0xFF, trackId & 0xFF, 0x00, 0x00, 0x00, 0x00, // reserved: 4 bytes
|
|
duration >>> 24 & 0xFF, // duration: 4 bytes
|
|
duration >>> 16 & 0xFF, duration >>> 8 & 0xFF, duration & 0xFF, 0x00, 0x00, 0x00, 0x00, // reserved: 2 * 4 bytes
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // layer(2bytes) + alternate_group(2bytes)
|
|
0x00, 0x00, 0x00, 0x00, // volume(2bytes) + reserved(2bytes)
|
|
0x00, 0x01, 0x00, 0x00, // ----begin composition matrix----
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, // ----end composition matrix----
|
|
width >>> 8 & 0xFF, // width and height
|
|
width & 0xFF, 0x00, 0x00, height >>> 8 & 0xFF, height & 0xFF, 0x00, 0x00]));
|
|
}
|
|
|
|
static mdia(meta) {
|
|
return MP4$1.box(MP4$1.types.mdia, MP4$1.mdhd(meta), MP4$1.hdlr(meta), MP4$1.minf(meta));
|
|
} // Media header box
|
|
|
|
|
|
static mdhd(meta) {
|
|
let timescale = meta.timescale;
|
|
let duration = meta.duration;
|
|
return MP4$1.box(MP4$1.types.mdhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags
|
|
0x00, 0x00, 0x00, 0x00, // creation_time
|
|
0x00, 0x00, 0x00, 0x00, // modification_time
|
|
timescale >>> 24 & 0xFF, // timescale: 4 bytes
|
|
timescale >>> 16 & 0xFF, timescale >>> 8 & 0xFF, timescale & 0xFF, duration >>> 24 & 0xFF, // duration: 4 bytes
|
|
duration >>> 16 & 0xFF, duration >>> 8 & 0xFF, duration & 0xFF, 0x55, 0xC4, // language: und (undetermined)
|
|
0x00, 0x00 // pre_defined = 0
|
|
]));
|
|
} // Media handler reference box
|
|
|
|
|
|
static hdlr(meta) {
|
|
let data = null;
|
|
|
|
if (meta.type === 'audio') {
|
|
data = MP4$1.constants.HDLR_AUDIO;
|
|
} else {
|
|
data = MP4$1.constants.HDLR_VIDEO;
|
|
}
|
|
|
|
return MP4$1.box(MP4$1.types.hdlr, data);
|
|
} // Media infomation box
|
|
|
|
|
|
static minf(meta) {
|
|
let xmhd = null;
|
|
|
|
if (meta.type === 'audio') {
|
|
xmhd = MP4$1.box(MP4$1.types.smhd, MP4$1.constants.SMHD);
|
|
} else {
|
|
xmhd = MP4$1.box(MP4$1.types.vmhd, MP4$1.constants.VMHD);
|
|
}
|
|
|
|
return MP4$1.box(MP4$1.types.minf, xmhd, MP4$1.dinf(), MP4$1.stbl(meta));
|
|
} // Data infomation box
|
|
|
|
|
|
static dinf() {
|
|
let result = MP4$1.box(MP4$1.types.dinf, MP4$1.box(MP4$1.types.dref, MP4$1.constants.DREF));
|
|
return result;
|
|
} // Sample table box
|
|
|
|
|
|
static stbl(meta) {
|
|
let result = MP4$1.box(MP4$1.types.stbl, // type: stbl
|
|
MP4$1.stsd(meta), // Sample Description Table
|
|
MP4$1.box(MP4$1.types.stts, MP4$1.constants.STTS), // Time-To-Sample
|
|
MP4$1.box(MP4$1.types.stsc, MP4$1.constants.STSC), // Sample-To-Chunk
|
|
MP4$1.box(MP4$1.types.stsz, MP4$1.constants.STSZ), // Sample size
|
|
MP4$1.box(MP4$1.types.stco, MP4$1.constants.STCO) // Chunk offset
|
|
);
|
|
return result;
|
|
} // Sample description box
|
|
|
|
|
|
static stsd(meta) {
|
|
if (meta.type === 'audio') {
|
|
// else: aac -> mp4a
|
|
return MP4$1.box(MP4$1.types.stsd, MP4$1.constants.STSD_PREFIX, MP4$1.mp4a(meta));
|
|
} else {
|
|
if (meta.videoType === 'avc') {
|
|
//
|
|
return MP4$1.box(MP4$1.types.stsd, MP4$1.constants.STSD_PREFIX, MP4$1.avc1(meta));
|
|
} else {
|
|
//
|
|
return MP4$1.box(MP4$1.types.stsd, MP4$1.constants.STSD_PREFIX, MP4$1.hvc1(meta));
|
|
}
|
|
}
|
|
}
|
|
|
|
static mp4a(meta) {
|
|
let channelCount = meta.channelCount;
|
|
let sampleRate = meta.audioSampleRate;
|
|
let data = new Uint8Array([0x00, 0x00, 0x00, 0x00, // reserved(4)
|
|
0x00, 0x00, 0x00, 0x01, // reserved(2) + data_reference_index(2)
|
|
0x00, 0x00, 0x00, 0x00, // reserved: 2 * 4 bytes
|
|
0x00, 0x00, 0x00, 0x00, 0x00, channelCount, // channelCount(2)
|
|
0x00, 0x10, // sampleSize(2)
|
|
0x00, 0x00, 0x00, 0x00, // reserved(4)
|
|
sampleRate >>> 8 & 0xFF, // Audio sample rate
|
|
sampleRate & 0xFF, 0x00, 0x00]);
|
|
return MP4$1.box(MP4$1.types.mp4a, data, MP4$1.esds(meta));
|
|
}
|
|
|
|
static esds(meta) {
|
|
let config = meta.config || [];
|
|
let configSize = config.length;
|
|
let data = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version 0 + flags
|
|
0x03, // descriptor_type
|
|
0x17 + configSize, // length3
|
|
0x00, 0x01, // es_id
|
|
0x00, // stream_priority
|
|
0x04, // descriptor_type
|
|
0x0F + configSize, // length
|
|
0x40, // codec: mpeg4_audio
|
|
0x15, // stream_type: Audio
|
|
0x00, 0x00, 0x00, // buffer_size
|
|
0x00, 0x00, 0x00, 0x00, // maxBitrate
|
|
0x00, 0x00, 0x00, 0x00, // avgBitrate
|
|
0x05 // descriptor_type
|
|
].concat([configSize]).concat(config).concat([0x06, 0x01, 0x02 // GASpecificConfig
|
|
]));
|
|
return MP4$1.box(MP4$1.types.esds, data);
|
|
} // avc
|
|
|
|
|
|
static avc1(meta) {
|
|
let avcc = meta.avcc;
|
|
const width = meta.codecWidth;
|
|
const height = meta.codecHeight;
|
|
let data = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, width >>> 8 & 255, width & 255, height >>> 8 & 255, height & 255, 0, 72, 0, 0, 0, 72, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24, 255, 255]);
|
|
return MP4$1.box(MP4$1.types.avc1, data, MP4$1.box(MP4$1.types.avcC, avcc));
|
|
} // hvc
|
|
|
|
|
|
static hvc1(meta) {
|
|
let avcc = meta.avcc;
|
|
const width = meta.codecWidth;
|
|
const height = meta.codecHeight;
|
|
let data = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, width >>> 8 & 255, width & 255, height >>> 8 & 255, height & 255, 0, 72, 0, 0, 0, 72, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24, 255, 255]);
|
|
return MP4$1.box(MP4$1.types.hvc1, data, MP4$1.box(MP4$1.types.hvcC, avcc));
|
|
} // Movie Extends box
|
|
|
|
|
|
static mvex(meta) {
|
|
return MP4$1.box(MP4$1.types.mvex, MP4$1.trex(meta));
|
|
} // Track Extends box
|
|
|
|
|
|
static trex(meta) {
|
|
let trackId = meta.id;
|
|
let data = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags
|
|
trackId >>> 24 & 0xFF, // track_ID
|
|
trackId >>> 16 & 0xFF, trackId >>> 8 & 0xFF, trackId & 0xFF, 0x00, 0x00, 0x00, 0x01, // default_sample_description_index
|
|
0x00, 0x00, 0x00, 0x00, // default_sample_duration
|
|
0x00, 0x00, 0x00, 0x00, // default_sample_size
|
|
0x00, 0x01, 0x00, 0x01 // default_sample_flags
|
|
]);
|
|
return MP4$1.box(MP4$1.types.trex, data);
|
|
} // Movie fragment box
|
|
|
|
|
|
static moof(track, baseMediaDecodeTime) {
|
|
return MP4$1.box(MP4$1.types.moof, MP4$1.mfhd(track.sequenceNumber), MP4$1.traf(track, baseMediaDecodeTime));
|
|
} //
|
|
|
|
|
|
static mfhd(sequenceNumber) {
|
|
let data = new Uint8Array([0x00, 0x00, 0x00, 0x00, sequenceNumber >>> 24 & 0xFF, // sequence_number: int32
|
|
sequenceNumber >>> 16 & 0xFF, sequenceNumber >>> 8 & 0xFF, sequenceNumber & 0xFF]);
|
|
return MP4$1.box(MP4$1.types.mfhd, data);
|
|
} // Track fragment box
|
|
|
|
|
|
static traf(track, baseMediaDecodeTime) {
|
|
let trackId = track.id; // Track fragment header box
|
|
|
|
let tfhd = MP4$1.box(MP4$1.types.tfhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) & flags
|
|
trackId >>> 24 & 0xFF, // track_ID
|
|
trackId >>> 16 & 0xFF, trackId >>> 8 & 0xFF, trackId & 0xFF])); // Track Fragment Decode Time
|
|
|
|
let tfdt = MP4$1.box(MP4$1.types.tfdt, new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) & flags
|
|
baseMediaDecodeTime >>> 24 & 0xFF, // baseMediaDecodeTime: int32
|
|
baseMediaDecodeTime >>> 16 & 0xFF, baseMediaDecodeTime >>> 8 & 0xFF, baseMediaDecodeTime & 0xFF]));
|
|
let sdtp = MP4$1.sdtp(track);
|
|
let trun = MP4$1.trun(track, sdtp.byteLength + 16 + 16 + 8 + 16 + 8 + 8);
|
|
return MP4$1.box(MP4$1.types.traf, tfhd, tfdt, trun, sdtp);
|
|
} // Sample Dependency Type box
|
|
|
|
|
|
static sdtp(track) {
|
|
let data = new Uint8Array(4 + 1);
|
|
let flags = track.flags;
|
|
data[4] = flags.isLeading << 6 | flags.dependsOn << 4 | flags.isDependedOn << 2 | flags.hasRedundancy;
|
|
return MP4$1.box(MP4$1.types.sdtp, data);
|
|
} // trun
|
|
|
|
|
|
static trun(track, offset) {
|
|
let dataSize = 12 + 16;
|
|
let data = new Uint8Array(dataSize);
|
|
offset += 8 + dataSize;
|
|
data.set([0x00, 0x00, 0x0F, 0x01, // version(0) & flags
|
|
0x00, 0x00, 0x00, 0x01, // sample_count
|
|
offset >>> 24 & 0xFF, // data_offset
|
|
offset >>> 16 & 0xFF, offset >>> 8 & 0xFF, offset & 0xFF], 0);
|
|
let duration = track.duration;
|
|
let size = track.size;
|
|
let flags = track.flags;
|
|
let cts = track.cts;
|
|
data.set([duration >>> 24 & 0xFF, // sample_duration
|
|
duration >>> 16 & 0xFF, duration >>> 8 & 0xFF, duration & 0xFF, size >>> 24 & 0xFF, // sample_size
|
|
size >>> 16 & 0xFF, size >>> 8 & 0xFF, size & 0xFF, flags.isLeading << 2 | flags.dependsOn, // sample_flags
|
|
flags.isDependedOn << 6 | flags.hasRedundancy << 4 | flags.isNonSync, 0x00, 0x00, // sample_degradation_priority
|
|
cts >>> 24 & 0xFF, // sample_composition_time_offset
|
|
cts >>> 16 & 0xFF, cts >>> 8 & 0xFF, cts & 0xFF], 12);
|
|
return MP4$1.box(MP4$1.types.trun, data);
|
|
} // mdat
|
|
|
|
|
|
static mdat(data) {
|
|
return MP4$1.box(MP4$1.types.mdat, data);
|
|
}
|
|
|
|
}
|
|
|
|
MP4$1.init();
|
|
|
|
class MseDecoder extends Emitter {
|
|
constructor(player) {
|
|
super();
|
|
this.player = player;
|
|
this.isAvc = true;
|
|
this.mediaSource = new window.MediaSource();
|
|
this.sourceBuffer = null;
|
|
this.hasInit = false;
|
|
this.isInitInfo = false;
|
|
this.cacheTrack = {};
|
|
this.timeInit = false;
|
|
this.sequenceNumber = 0;
|
|
this.mediaSourceOpen = false;
|
|
this.dropping = false;
|
|
this.firstRenderTime = null;
|
|
this.mediaSourceAppendBufferError = false;
|
|
this.mediaSourceAppendBufferFull = false;
|
|
this.isDecodeFirstIIframe = false;
|
|
this.player.video.$videoElement.src = window.URL.createObjectURL(this.mediaSource);
|
|
const {
|
|
debug,
|
|
events: {
|
|
proxy
|
|
}
|
|
} = player;
|
|
proxy(this.mediaSource, 'sourceopen', () => {
|
|
this.mediaSourceOpen = true;
|
|
this.player.emit(EVENTS.mseSourceOpen);
|
|
});
|
|
proxy(this.mediaSource, 'sourceclose', () => {
|
|
this.player.emit(EVENTS.mseSourceClose);
|
|
});
|
|
player.debug.log('MediaSource', 'init');
|
|
}
|
|
|
|
destroy() {
|
|
this.stop();
|
|
this.mediaSource = null;
|
|
this.mediaSourceOpen = false;
|
|
this.sourceBuffer = null;
|
|
this.hasInit = false;
|
|
this.isInitInfo = false;
|
|
this.sequenceNumber = 0;
|
|
this.cacheTrack = null;
|
|
this.timeInit = false;
|
|
this.mediaSourceAppendBufferError = false;
|
|
this.mediaSourceAppendBufferFull = false;
|
|
this.isDecodeFirstIIframe = false;
|
|
this.off();
|
|
this.player.debug.log('MediaSource', 'destroy');
|
|
}
|
|
|
|
get state() {
|
|
return this.mediaSource && this.mediaSource.readyState;
|
|
}
|
|
|
|
get isStateOpen() {
|
|
return this.state === MEDIA_SOURCE_STATE.open;
|
|
}
|
|
|
|
get isStateClosed() {
|
|
return this.state === MEDIA_SOURCE_STATE.closed;
|
|
}
|
|
|
|
get isStateEnded() {
|
|
return this.state === MEDIA_SOURCE_STATE.ended;
|
|
}
|
|
|
|
get duration() {
|
|
return this.mediaSource && this.mediaSource.duration;
|
|
}
|
|
|
|
set duration(duration) {
|
|
this.mediaSource.duration = duration;
|
|
}
|
|
|
|
decodeVideo(payload, ts, isIframe, cts) {
|
|
const player = this.player;
|
|
|
|
if (!player) {
|
|
return;
|
|
}
|
|
|
|
if (!this.hasInit) {
|
|
if (isIframe && payload[1] === 0) {
|
|
const videoCodec = payload[0] & 0x0F;
|
|
player.video.updateVideoInfo({
|
|
encTypeCode: videoCodec
|
|
}); // 如果解码出来的是
|
|
|
|
if (videoCodec === VIDEO_ENC_CODE.h265) {
|
|
this.emit(EVENTS_ERROR.mediaSourceH265NotSupport);
|
|
return;
|
|
}
|
|
|
|
if (!player._times.decodeStart) {
|
|
player._times.decodeStart = now();
|
|
}
|
|
|
|
this._decodeConfigurationRecord(payload, ts, isIframe, videoCodec);
|
|
|
|
this.hasInit = true;
|
|
}
|
|
} else {
|
|
if (isIframe && payload[1] === 0) {
|
|
let config = parseAVCDecoderConfigurationRecord(payload.slice(5));
|
|
const videoInfo = this.player.video.videoInfo;
|
|
|
|
if (videoInfo && videoInfo.width && videoInfo.height && config && config.codecWidth && config.codecHeight && (config.codecWidth !== videoInfo.width || config.codecHeight !== videoInfo.height)) {
|
|
this.player.debug.warn('MediaSource', `width or height is update, width ${videoInfo.width}-> ${config.codecWidth}, height ${videoInfo.height}-> ${config.codecHeight}`);
|
|
this.isInitInfo = false;
|
|
this.player.video.init = false;
|
|
}
|
|
}
|
|
|
|
if (!this.isDecodeFirstIIframe && isIframe) {
|
|
this.isDecodeFirstIIframe = true;
|
|
}
|
|
|
|
if (this.isDecodeFirstIIframe) {
|
|
if (this.firstRenderTime === null) {
|
|
this.firstRenderTime = ts;
|
|
}
|
|
|
|
const dts = ts - this.firstRenderTime;
|
|
|
|
this._decodeVideo(payload, dts, isIframe, cts);
|
|
} else {
|
|
this.player.debug.warn('MediaSource', 'decodeVideo isDecodeFirstIIframe false');
|
|
}
|
|
}
|
|
}
|
|
|
|
_decodeConfigurationRecord(payload, ts, isIframe, videoCodec) {
|
|
let data = payload.slice(5);
|
|
let config = {};
|
|
config = parseAVCDecoderConfigurationRecord(data);
|
|
const metaData = {
|
|
id: 1,
|
|
// video tag data
|
|
type: 'video',
|
|
timescale: 1000,
|
|
duration: 0,
|
|
avcc: data,
|
|
codecWidth: config.codecWidth,
|
|
codecHeight: config.codecHeight,
|
|
videoType: config.videoType
|
|
}; // ftyp
|
|
|
|
const metaBox = MP4$1.generateInitSegment(metaData);
|
|
this.isAvc = true;
|
|
this.appendBuffer(metaBox.buffer);
|
|
this.sequenceNumber = 0;
|
|
this.cacheTrack = null;
|
|
this.timeInit = false;
|
|
} //
|
|
|
|
|
|
_decodeVideo(payload, dts, isIframe, cts) {
|
|
const player = this.player;
|
|
let arrayBuffer = payload.slice(5);
|
|
let bytes = arrayBuffer.byteLength; // player.debug.log('MediaSource', '_decodeVideo', ts);
|
|
|
|
const $video = player.video.$videoElement;
|
|
const videoBufferDelay = player._opt.videoBufferDelay;
|
|
|
|
if ($video.buffered.length > 1) {
|
|
this.removeBuffer($video.buffered.start(0), $video.buffered.end(0));
|
|
this.timeInit = false;
|
|
}
|
|
|
|
if (this.dropping && dts - this.cacheTrack.dts > videoBufferDelay) {
|
|
this.dropping = false;
|
|
this.cacheTrack = {};
|
|
} else if (this.cacheTrack && dts >= this.cacheTrack.dts) {
|
|
// 需要额外加8个size
|
|
let mdatBytes = 8 + this.cacheTrack.size;
|
|
let mdatbox = new Uint8Array(mdatBytes);
|
|
mdatbox[0] = mdatBytes >>> 24 & 255;
|
|
mdatbox[1] = mdatBytes >>> 16 & 255;
|
|
mdatbox[2] = mdatBytes >>> 8 & 255;
|
|
mdatbox[3] = mdatBytes & 255;
|
|
mdatbox.set(MP4$1.types.mdat, 4);
|
|
mdatbox.set(this.cacheTrack.data, 8);
|
|
this.cacheTrack.duration = dts - this.cacheTrack.dts; // moof
|
|
|
|
let moofbox = MP4$1.moof(this.cacheTrack, this.cacheTrack.dts);
|
|
let result = new Uint8Array(moofbox.byteLength + mdatbox.byteLength);
|
|
result.set(moofbox, 0);
|
|
result.set(mdatbox, moofbox.byteLength); // appendBuffer
|
|
|
|
this.appendBuffer(result.buffer);
|
|
player.handleRender();
|
|
player.updateStats({
|
|
fps: true,
|
|
ts: dts,
|
|
buf: player.demux && player.demux.delay || 0
|
|
});
|
|
|
|
if (!player._times.videoStart) {
|
|
player._times.videoStart = now();
|
|
player.handlePlayToRenderTimes();
|
|
}
|
|
} else {
|
|
player.debug.log('MediaSource', 'timeInit set false , cacheTrack = {}');
|
|
this.timeInit = false;
|
|
this.cacheTrack = {};
|
|
}
|
|
|
|
if (!this.cacheTrack) {
|
|
this.cacheTrack = {};
|
|
}
|
|
|
|
this.cacheTrack.id = 1;
|
|
this.cacheTrack.sequenceNumber = ++this.sequenceNumber;
|
|
this.cacheTrack.size = bytes;
|
|
this.cacheTrack.dts = dts;
|
|
this.cacheTrack.cts = cts;
|
|
this.cacheTrack.isKeyframe = isIframe;
|
|
this.cacheTrack.data = arrayBuffer; //
|
|
|
|
this.cacheTrack.flags = {
|
|
isLeading: 0,
|
|
dependsOn: isIframe ? 2 : 1,
|
|
isDependedOn: isIframe ? 1 : 0,
|
|
hasRedundancy: 0,
|
|
isNonSync: isIframe ? 0 : 1
|
|
}; //
|
|
|
|
if (!this.timeInit && $video.buffered.length === 1) {
|
|
player.debug.log('MediaSource', 'timeInit set true');
|
|
this.timeInit = true;
|
|
$video.currentTime = $video.buffered.end(0);
|
|
}
|
|
|
|
if (!this.isInitInfo && $video.videoWidth > 0 && $video.videoHeight > 0) {
|
|
player.debug.log('MediaSource', `updateVideoInfo: ${$video.videoWidth},${$video.videoHeight}`);
|
|
player.video.updateVideoInfo({
|
|
width: $video.videoWidth,
|
|
height: $video.videoHeight
|
|
});
|
|
player.video.initCanvasViewSize();
|
|
this.isInitInfo = true;
|
|
}
|
|
}
|
|
|
|
appendBuffer(buffer) {
|
|
const {
|
|
debug,
|
|
events: {
|
|
proxy
|
|
}
|
|
} = this.player;
|
|
|
|
if (this.sourceBuffer === null) {
|
|
this.sourceBuffer = this.mediaSource.addSourceBuffer(MP4_CODECS.avc);
|
|
proxy(this.sourceBuffer, 'error', error => {
|
|
this.player.emit(EVENTS.mseSourceBufferError, error); // this.dropSourceBuffer(false)
|
|
});
|
|
}
|
|
|
|
if (this.mediaSourceAppendBufferError) {
|
|
debug.error('MediaSource', `this.mediaSourceAppendBufferError is true`);
|
|
return;
|
|
}
|
|
|
|
if (this.mediaSourceAppendBufferFull) {
|
|
debug.error('MediaSource', `this.mediaSourceAppendBufferFull is true`);
|
|
return;
|
|
}
|
|
|
|
if (this.sourceBuffer.updating === false && this.isStateOpen) {
|
|
try {
|
|
this.sourceBuffer.appendBuffer(buffer);
|
|
} catch (e) {
|
|
debug.warn('MediaSource', 'this.sourceBuffer.appendBuffer()', e.code, e);
|
|
|
|
if (e.code === 22) {
|
|
// QuotaExceededError
|
|
// The SourceBuffer is full, and cannot free space to append additional buffers
|
|
this.stop();
|
|
this.mediaSourceAppendBufferFull = true;
|
|
this.emit(EVENTS_ERROR.mediaSourceFull);
|
|
} else if (e.code === 11) {
|
|
// Failed to execute 'appendBuffer' on 'SourceBuffer': The HTMLMediaElement.error attribute is not null.
|
|
this.stop();
|
|
this.mediaSourceAppendBufferError = true;
|
|
this.emit(EVENTS_ERROR.mediaSourceAppendBufferError);
|
|
} else {
|
|
debug.error('MediaSource', 'appendBuffer error', e);
|
|
this.player.emit(EVENTS.mseSourceBufferError, e);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (this.isStateClosed) {
|
|
this.player.emitError(EVENTS_ERROR.mseSourceBufferError, 'mediaSource is not attached to video or mediaSource is closed');
|
|
} else if (this.isStateEnded) {
|
|
this.player.emitError(EVENTS_ERROR.mseSourceBufferError, 'mediaSource is closed');
|
|
} else {
|
|
if (this.sourceBuffer.updating === true) {
|
|
this.player.emit(EVENTS.mseSourceBufferBusy); // this.dropSourceBuffer(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
stop() {
|
|
this.abortSourceBuffer();
|
|
this.removeSourceBuffer();
|
|
this.endOfStream();
|
|
}
|
|
|
|
dropSourceBuffer(isDropping) {
|
|
const $video = this.player.video.$videoElement;
|
|
this.dropping = isDropping;
|
|
|
|
if ($video.buffered.length > 0) {
|
|
if ($video.buffered.end(0) - $video.currentTime > 1) {
|
|
this.player.debug.warn('MediaSource', 'dropSourceBuffer', `$video.buffered.end(0) is ${$video.buffered.end(0)} - $video.currentTime ${$video.currentTime}`);
|
|
$video.currentTime = $video.buffered.end(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
removeBuffer(start, end) {
|
|
if (this.isStateOpen && this.sourceBuffer.updating === false) {
|
|
try {
|
|
this.sourceBuffer.remove(start, end);
|
|
} catch (e) {
|
|
this.player.debug.warn('MediaSource', 'removeBuffer() error', e);
|
|
}
|
|
} else {
|
|
this.player.debug.warn('MediaSource', 'removeBuffer() this.isStateOpen is', this.isStateOpen, 'this.sourceBuffer.updating', this.sourceBuffer.updating);
|
|
}
|
|
}
|
|
|
|
endOfStream() {
|
|
// fix: MediaSource endOfStream before demuxer initialization completes (before HAVE_METADATA) is treated as an error
|
|
const $videoElement = this.player.video && this.player.video.$videoElement;
|
|
|
|
if (this.isStateOpen && $videoElement && $videoElement.readyState >= 1) {
|
|
try {
|
|
this.mediaSource.endOfStream();
|
|
} catch (e) {
|
|
this.player.debug.warn('MediaSource', 'endOfStream() error', e);
|
|
}
|
|
}
|
|
}
|
|
|
|
abortSourceBuffer() {
|
|
if (this.isStateOpen) {
|
|
if (this.sourceBuffer) {
|
|
this.sourceBuffer.abort();
|
|
this.sourceBuffer = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
removeSourceBuffer() {
|
|
if (!this.isStateClosed) {
|
|
if (this.mediaSource && this.sourceBuffer) {
|
|
try {
|
|
this.mediaSource.removeSourceBuffer(this.sourceBuffer);
|
|
} catch (e) {
|
|
this.player.debug.warn('MediaSource', 'removeSourceBuffer() error', e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// tks: https://github.com/richtr/NoSleep.js
|
|
const WEBM = "data:video/webm;base64,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";
|
|
const MP4 = "data:video/mp4;base64,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"; // Detect iOS browsers < version 10
|
|
|
|
const oldIOS = () => typeof navigator !== "undefined" && parseFloat(("" + (/CPU.*OS ([0-9_]{3,4})[0-9_]{0,1}|(CPU like).*AppleWebKit.*Mobile/i.exec(navigator.userAgent) || [0, ""])[1]).replace("undefined", "3_2").replace("_", ".").replace("_", "")) < 10 && !window.MSStream; // Detect native Wake Lock API support
|
|
|
|
|
|
const nativeWakeLock = () => "wakeLock" in navigator;
|
|
|
|
class NoSleep {
|
|
constructor(player) {
|
|
this.player = player;
|
|
this.enabled = false;
|
|
|
|
if (nativeWakeLock()) {
|
|
this._wakeLock = null;
|
|
|
|
const handleVisibilityChange = () => {
|
|
if (this._wakeLock !== null && document.visibilityState === "visible") {
|
|
this.enable();
|
|
}
|
|
};
|
|
|
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
document.addEventListener("fullscreenchange", handleVisibilityChange);
|
|
} else if (oldIOS()) {
|
|
this.noSleepTimer = null;
|
|
} else {
|
|
// Set up no sleep video element
|
|
this.noSleepVideo = document.createElement("video");
|
|
this.noSleepVideo.setAttribute("title", "No Sleep");
|
|
this.noSleepVideo.setAttribute("playsinline", "");
|
|
|
|
this._addSourceToVideo(this.noSleepVideo, "webm", WEBM);
|
|
|
|
this._addSourceToVideo(this.noSleepVideo, "mp4", MP4);
|
|
|
|
this.noSleepVideo.addEventListener("loadedmetadata", () => {
|
|
if (this.noSleepVideo.duration <= 1) {
|
|
// webm source
|
|
this.noSleepVideo.setAttribute("loop", "");
|
|
} else {
|
|
// mp4 source
|
|
this.noSleepVideo.addEventListener("timeupdate", () => {
|
|
if (this.noSleepVideo.currentTime > 0.5) {
|
|
this.noSleepVideo.currentTime = Math.random();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
_addSourceToVideo(element, type, dataURI) {
|
|
var source = document.createElement("source");
|
|
source.src = dataURI;
|
|
source.type = `video/${type}`;
|
|
element.appendChild(source);
|
|
}
|
|
|
|
get isEnabled() {
|
|
return this.enabled;
|
|
}
|
|
|
|
enable() {
|
|
const debug = this.player.debug;
|
|
|
|
if (nativeWakeLock()) {
|
|
return navigator.wakeLock.request("screen").then(wakeLock => {
|
|
this._wakeLock = wakeLock;
|
|
this.enabled = true;
|
|
debug.log('wakeLock', 'Wake Lock active.');
|
|
|
|
this._wakeLock.addEventListener("release", () => {
|
|
// ToDo: Potentially emit an event for the page to observe since
|
|
// Wake Lock releases happen when page visibility changes.
|
|
// (https://web.dev/wakelock/#wake-lock-lifecycle)
|
|
debug.log('wakeLock', 'Wake Lock released.');
|
|
});
|
|
}).catch(err => {
|
|
this.enabled = false;
|
|
debug.error('wakeLock', `${err.name}, ${err.message}`);
|
|
throw err;
|
|
});
|
|
} else if (oldIOS()) {
|
|
this.disable();
|
|
this.noSleepTimer = window.setInterval(() => {
|
|
if (!document.hidden) {
|
|
window.location.href = window.location.href.split("#")[0];
|
|
window.setTimeout(window.stop, 0);
|
|
}
|
|
}, 15000);
|
|
this.enabled = true;
|
|
return Promise.resolve();
|
|
} else {
|
|
let playPromise = this.noSleepVideo.play();
|
|
return playPromise.then(res => {
|
|
this.enabled = true;
|
|
return res;
|
|
}).catch(err => {
|
|
this.enabled = false;
|
|
throw err;
|
|
});
|
|
}
|
|
}
|
|
|
|
disable() {
|
|
const debug = this.player.debug;
|
|
|
|
if (nativeWakeLock()) {
|
|
if (this._wakeLock) {
|
|
this._wakeLock.release();
|
|
}
|
|
|
|
this._wakeLock = null;
|
|
} else if (oldIOS()) {
|
|
if (this.noSleepTimer) {
|
|
debug.warn('wakeLock', 'NoSleep now disabled for older iOS devices.');
|
|
window.clearInterval(this.noSleepTimer);
|
|
this.noSleepTimer = null;
|
|
}
|
|
} else {
|
|
this.noSleepVideo.pause();
|
|
}
|
|
|
|
this.enabled = false;
|
|
}
|
|
|
|
}
|
|
|
|
class Player extends Emitter {
|
|
constructor(container, options) {
|
|
super();
|
|
this.$container = container;
|
|
this._opt = Object.assign({}, DEFAULT_PLAYER_OPTIONS, options);
|
|
this.debug = new Debug(this); //
|
|
|
|
if (this._opt.useWCS) {
|
|
this._opt.useWCS = supportWCS();
|
|
} //
|
|
|
|
|
|
if (this._opt.useMSE) {
|
|
this._opt.useMSE = supportMSE();
|
|
} //
|
|
|
|
|
|
if (this._opt.wcsUseVideoRender) {
|
|
this._opt.wcsUseVideoRender = supportMediaStreamTrack();
|
|
} // 如果使用mse则强制不允许 webcodecs
|
|
|
|
|
|
if (this._opt.useMSE) {
|
|
if (this._opt.useWCS) {
|
|
this.debug.log('Player', 'useWCS set true->false');
|
|
}
|
|
|
|
if (!this._opt.forceNoOffscreen) {
|
|
this.debug.log('Player', 'forceNoOffscreen set false->true');
|
|
}
|
|
|
|
this._opt.useWCS = false;
|
|
this._opt.forceNoOffscreen = true;
|
|
}
|
|
|
|
if (!this._opt.forceNoOffscreen) {
|
|
if (!supportOffscreenV2()) {
|
|
this._opt.forceNoOffscreen = true;
|
|
this._opt.useOffscreen = false;
|
|
} else {
|
|
this._opt.useOffscreen = true;
|
|
}
|
|
}
|
|
|
|
if (!this._opt.hasAudio) {
|
|
this._opt.operateBtns.audio = false;
|
|
}
|
|
|
|
this._opt.hasControl = this._hasControl(); //
|
|
|
|
this._loading = false;
|
|
this._playing = false;
|
|
this._hasLoaded = false; //
|
|
|
|
this._checkHeartTimeout = null;
|
|
this._checkLoadingTimeout = null;
|
|
this._checkStatsInterval = null; //
|
|
|
|
this._startBpsTime = null;
|
|
this._isPlayingBeforePageHidden = false;
|
|
this._stats = {
|
|
buf: 0,
|
|
// 当前缓冲区时长,单位毫秒,
|
|
fps: 0,
|
|
// 当前视频帧率
|
|
abps: 0,
|
|
// 当前音频码率,单位bit
|
|
vbps: 0,
|
|
// 当前视频码率,单位bit
|
|
ts: 0 // 当前视频帧pts,单位毫秒
|
|
|
|
}; // 各个步骤的时间统计
|
|
|
|
this._times = initPlayTimes(); //
|
|
|
|
this._videoTimestamp = 0;
|
|
this._audioTimestamp = 0;
|
|
property$1(this);
|
|
this.events = new Events(this);
|
|
this.video = new Video(this);
|
|
|
|
if (this._opt.hasAudio) {
|
|
this.audio = new Audio(this);
|
|
}
|
|
|
|
this.recorder = new Recorder(this);
|
|
|
|
if (!this._onlyMseOrWcsVideo()) {
|
|
this.decoderWorker = new DecoderWorker(this);
|
|
} else {
|
|
this.loaded = true;
|
|
}
|
|
|
|
this.stream = null;
|
|
this.demux = null;
|
|
this._lastVolume = null;
|
|
|
|
if (this._opt.useWCS) {
|
|
this.webcodecsDecoder = new WebcodecsDecoder(this);
|
|
this.loaded = true;
|
|
}
|
|
|
|
if (this._opt.useMSE) {
|
|
this.mseDecoder = new MseDecoder(this);
|
|
this.loaded = true;
|
|
} //
|
|
|
|
|
|
this.control = new Control(this);
|
|
|
|
if (isMobile()) {
|
|
this.keepScreenOn = new NoSleep(this);
|
|
}
|
|
|
|
events$1(this);
|
|
observer(this);
|
|
|
|
if (this._opt.useWCS) {
|
|
this.debug.log('Player', 'use WCS');
|
|
}
|
|
|
|
if (this._opt.useMSE) {
|
|
this.debug.log('Player', 'use MSE');
|
|
}
|
|
|
|
if (this._opt.useOffscreen) {
|
|
this.debug.log('Player', 'use offscreen');
|
|
}
|
|
|
|
this.debug.log('Player options', this._opt);
|
|
}
|
|
|
|
destroy() {
|
|
this._loading = false;
|
|
this._playing = false;
|
|
this._hasLoaded = false;
|
|
this._lastVolume = null;
|
|
this._times = initPlayTimes();
|
|
|
|
if (this.decoderWorker) {
|
|
this.decoderWorker.destroy();
|
|
this.decoderWorker = null;
|
|
}
|
|
|
|
if (this.video) {
|
|
this.video.destroy();
|
|
this.video = null;
|
|
}
|
|
|
|
if (this.audio) {
|
|
this.audio.destroy();
|
|
this.audio = null;
|
|
}
|
|
|
|
if (this.stream) {
|
|
this.stream.destroy();
|
|
this.stream = null;
|
|
}
|
|
|
|
if (this.recorder) {
|
|
this.recorder.destroy();
|
|
this.recorder = null;
|
|
}
|
|
|
|
if (this.control) {
|
|
this.control.destroy();
|
|
this.control = null;
|
|
}
|
|
|
|
if (this.webcodecsDecoder) {
|
|
this.webcodecsDecoder.destroy();
|
|
this.webcodecsDecoder = null;
|
|
}
|
|
|
|
if (this.mseDecoder) {
|
|
this.mseDecoder.destroy();
|
|
this.mseDecoder = null;
|
|
}
|
|
|
|
if (this.demux) {
|
|
this.demux.destroy();
|
|
this.demux = null;
|
|
}
|
|
|
|
if (this.events) {
|
|
this.events.destroy();
|
|
this.events = null;
|
|
}
|
|
|
|
this.clearCheckHeartTimeout();
|
|
this.clearCheckLoadingTimeout();
|
|
this.clearStatsInterval(); //
|
|
|
|
this.releaseWakeLock();
|
|
this.keepScreenOn = null; // reset stats
|
|
|
|
this.resetStats();
|
|
this._audioTimestamp = 0;
|
|
this._videoTimestamp = 0; // 其他没法解耦的,通过 destroy 方式
|
|
|
|
this.emit('destroy'); // 接触所有绑定事件
|
|
|
|
this.off();
|
|
this.debug.log('play', 'destroy end');
|
|
}
|
|
|
|
set fullscreen(value) {
|
|
if (isMobile() && this._opt.useWebFullScreen) {
|
|
this.emit(EVENTS.webFullscreen, value);
|
|
setTimeout(() => {
|
|
this.updateOption({
|
|
rotate: value ? 270 : 0
|
|
});
|
|
this.resize();
|
|
}, 10);
|
|
} else {
|
|
this.emit(EVENTS.fullscreen, value);
|
|
}
|
|
}
|
|
|
|
get fullscreen() {
|
|
return isFullScreen() || this.webFullscreen;
|
|
}
|
|
|
|
set webFullscreen(value) {
|
|
this.emit(EVENTS.webFullscreen, value);
|
|
}
|
|
|
|
get webFullscreen() {
|
|
return this.$container.classList.contains('jessibuca-fullscreen-web');
|
|
}
|
|
|
|
set loaded(value) {
|
|
this._hasLoaded = value;
|
|
}
|
|
|
|
get loaded() {
|
|
return this._hasLoaded;
|
|
} //
|
|
|
|
|
|
set playing(value) {
|
|
if (value) {
|
|
// 将loading 设置为 false
|
|
this.loading = false;
|
|
}
|
|
|
|
if (this.playing !== value) {
|
|
this._playing = value;
|
|
this.emit(EVENTS.playing, value);
|
|
this.emit(EVENTS.volumechange, this.volume);
|
|
|
|
if (value) {
|
|
this.emit(EVENTS.play);
|
|
} else {
|
|
this.emit(EVENTS.pause);
|
|
}
|
|
}
|
|
}
|
|
|
|
get playing() {
|
|
return this._playing;
|
|
}
|
|
|
|
get volume() {
|
|
return this.audio && this.audio.volume || 0;
|
|
}
|
|
|
|
set volume(value) {
|
|
if (value !== this.volume) {
|
|
this.audio && this.audio.setVolume(value);
|
|
this._lastVolume = value;
|
|
}
|
|
}
|
|
|
|
get lastVolume() {
|
|
return this._lastVolume;
|
|
}
|
|
|
|
set loading(value) {
|
|
if (this.loading !== value) {
|
|
this._loading = value;
|
|
this.emit(EVENTS.loading, this._loading);
|
|
}
|
|
}
|
|
|
|
get loading() {
|
|
return this._loading;
|
|
}
|
|
|
|
set recording(value) {
|
|
if (value) {
|
|
if (this.playing) {
|
|
this.recorder && this.recorder.startRecord();
|
|
}
|
|
} else {
|
|
this.recorder && this.recorder.stopRecordAndSave();
|
|
}
|
|
}
|
|
|
|
get recording() {
|
|
return this.recorder ? this.recorder.recording : false;
|
|
}
|
|
|
|
set audioTimestamp(value) {
|
|
if (value === null) {
|
|
return;
|
|
}
|
|
|
|
this._audioTimestamp = value;
|
|
} //
|
|
|
|
|
|
get audioTimestamp() {
|
|
return this._audioTimestamp;
|
|
} //
|
|
|
|
|
|
set videoTimestamp(value) {
|
|
if (value === null) {
|
|
return;
|
|
}
|
|
|
|
this._videoTimestamp = value; // just for wasm
|
|
|
|
if (!this._opt.useWCS && !this._opt.useMSE) {
|
|
if (this.audioTimestamp && this.videoTimestamp) {
|
|
this.audio && this.audio.emit(EVENTS.videoSyncAudio, {
|
|
audioTimestamp: this.audioTimestamp,
|
|
videoTimestamp: this.videoTimestamp,
|
|
diff: this.audioTimestamp - this.videoTimestamp
|
|
});
|
|
}
|
|
}
|
|
} //
|
|
|
|
|
|
get videoTimestamp() {
|
|
return this._videoTimestamp;
|
|
}
|
|
|
|
get isDebug() {
|
|
return this._opt.debug === true;
|
|
}
|
|
/**
|
|
*
|
|
* @param options
|
|
*/
|
|
|
|
|
|
updateOption(options) {
|
|
this._opt = Object.assign({}, this._opt, options);
|
|
}
|
|
/**
|
|
*
|
|
* @returns {Promise<unknown>}
|
|
*/
|
|
|
|
|
|
init() {
|
|
return new Promise((resolve, reject) => {
|
|
if (!this.stream) {
|
|
this.stream = new Stream(this);
|
|
}
|
|
|
|
if (!this.audio) {
|
|
if (this._opt.hasAudio) {
|
|
this.audio = new Audio(this);
|
|
}
|
|
}
|
|
|
|
if (!this.demux) {
|
|
this.demux = new Demux(this);
|
|
}
|
|
|
|
if (this._opt.useWCS) {
|
|
if (!this.webcodecsDecoder) {
|
|
this.webcodecsDecoder = new WebcodecsDecoder(this);
|
|
}
|
|
}
|
|
|
|
if (this._opt.useMSE) {
|
|
if (!this.mseDecoder) {
|
|
this.mseDecoder = new MseDecoder(this);
|
|
}
|
|
}
|
|
|
|
if (!this.decoderWorker && !this._onlyMseOrWcsVideo()) {
|
|
this.decoderWorker = new DecoderWorker(this);
|
|
this.once(EVENTS.decoderWorkerInit, () => {
|
|
resolve();
|
|
});
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
*
|
|
* @param url
|
|
* @returns {Promise<unknown>}
|
|
*/
|
|
|
|
|
|
play(url, options) {
|
|
return new Promise((resolve, reject) => {
|
|
if (!url && !this._opt.url) {
|
|
return reject();
|
|
}
|
|
|
|
this.loading = true;
|
|
this.playing = false;
|
|
this._times.playInitStart = now();
|
|
|
|
if (!url) {
|
|
url = this._opt.url;
|
|
}
|
|
|
|
this._opt.url = url;
|
|
this.clearCheckHeartTimeout();
|
|
this.init().then(() => {
|
|
this._times.playStart = now(); //
|
|
|
|
if (this._opt.isNotMute) {
|
|
this.mute(false);
|
|
}
|
|
|
|
if (this.webcodecsDecoder) {
|
|
this.webcodecsDecoder.once(EVENTS_ERROR.webcodecsH265NotSupport, () => {
|
|
this.emit(EVENTS_ERROR.webcodecsH265NotSupport);
|
|
|
|
if (!this._opt.autoWasm) {
|
|
this.emit(EVENTS.error, EVENTS_ERROR.webcodecsH265NotSupport);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (this.mseDecoder) {
|
|
this.mseDecoder.once(EVENTS_ERROR.mediaSourceH265NotSupport, () => {
|
|
this.emit(EVENTS_ERROR.mediaSourceH265NotSupport);
|
|
|
|
if (!this._opt.autoWasm) {
|
|
this.emit(EVENTS.error, EVENTS_ERROR.mediaSourceH265NotSupport);
|
|
}
|
|
});
|
|
this.mseDecoder.once(EVENTS_ERROR.mediaSourceFull, () => {
|
|
this.emitError(EVENTS_ERROR.mediaSourceFull);
|
|
});
|
|
this.mseDecoder.once(EVENTS_ERROR.mediaSourceAppendBufferError, () => {
|
|
this.emitError(EVENTS_ERROR.mediaSourceAppendBufferError);
|
|
});
|
|
this.mseDecoder.once(EVENTS_ERROR.mediaSourceBufferListLarge, () => {
|
|
this.emitError(EVENTS_ERROR.mediaSourceBufferListLarge);
|
|
});
|
|
this.mseDecoder.once(EVENTS_ERROR.mediaSourceAppendBufferEndTimeout, () => {
|
|
this.emitError(EVENTS_ERROR.mediaSourceAppendBufferEndTimeout);
|
|
});
|
|
}
|
|
|
|
this.enableWakeLock();
|
|
this.stream.fetchStream(url, options); //
|
|
|
|
this.checkLoadingTimeout(); // fetch error
|
|
|
|
this.stream.once(EVENTS_ERROR.fetchError, error => {
|
|
reject(error);
|
|
}); // ws
|
|
|
|
this.stream.once(EVENTS_ERROR.websocketError, error => {
|
|
reject(error);
|
|
}); // stream end
|
|
|
|
this.stream.once(EVENTS.streamEnd, () => {
|
|
reject();
|
|
}); // success
|
|
|
|
this.stream.once(EVENTS.streamSuccess, () => {
|
|
resolve();
|
|
this._times.streamResponse = now(); //
|
|
|
|
this.video.play();
|
|
this.checkStatsInterval();
|
|
});
|
|
}).catch(e => {
|
|
reject(e);
|
|
});
|
|
});
|
|
}
|
|
/**
|
|
*
|
|
*/
|
|
|
|
|
|
close() {
|
|
return new Promise((resolve, reject) => {
|
|
this._close().then(() => {
|
|
this.video && this.video.clearView();
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
resumeAudioAfterPause() {
|
|
if (this.lastVolume) {
|
|
this.volume = this.lastVolume;
|
|
}
|
|
}
|
|
|
|
_close() {
|
|
return new Promise((resolve, reject) => {
|
|
//
|
|
if (this.stream) {
|
|
this.stream.destroy();
|
|
this.stream = null;
|
|
}
|
|
|
|
if (this.demux) {
|
|
this.demux.destroy();
|
|
this.demux = null;
|
|
} //
|
|
|
|
|
|
if (this.decoderWorker) {
|
|
this.decoderWorker.destroy();
|
|
this.decoderWorker = null;
|
|
}
|
|
|
|
if (this.webcodecsDecoder) {
|
|
this.webcodecsDecoder.destroy();
|
|
this.webcodecsDecoder = null;
|
|
}
|
|
|
|
if (this.mseDecoder) {
|
|
this.mseDecoder.destroy();
|
|
this.mseDecoder = null;
|
|
}
|
|
|
|
if (this.audio) {
|
|
this.audio.destroy();
|
|
this.audio = null;
|
|
}
|
|
|
|
this.clearCheckHeartTimeout();
|
|
this.clearCheckLoadingTimeout();
|
|
this.clearStatsInterval();
|
|
this.playing = false;
|
|
this.loading = false;
|
|
this.recording = false;
|
|
|
|
if (this.video) {
|
|
this.video.resetInit();
|
|
this.video.pause(true);
|
|
} // release lock
|
|
|
|
|
|
this.releaseWakeLock(); // reset stats
|
|
|
|
this.resetStats(); //
|
|
|
|
this._audioTimestamp = 0;
|
|
this._videoTimestamp = 0; //
|
|
|
|
this._times = initPlayTimes(); //
|
|
|
|
setTimeout(() => {
|
|
resolve();
|
|
}, 0);
|
|
});
|
|
}
|
|
/**
|
|
*
|
|
* @param flag {boolean} 是否清除画面
|
|
* @returns {Promise<unknown>}
|
|
*/
|
|
|
|
|
|
pause() {
|
|
let flag = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
|
|
|
|
if (flag) {
|
|
return this.close();
|
|
} else {
|
|
return this._close();
|
|
}
|
|
}
|
|
/**
|
|
*
|
|
* @param flag
|
|
*/
|
|
|
|
|
|
mute(flag) {
|
|
this.audio && this.audio.mute(flag);
|
|
}
|
|
/**
|
|
*
|
|
*/
|
|
|
|
|
|
resize() {
|
|
this.video.resize();
|
|
}
|
|
/**
|
|
*
|
|
* @param fileName
|
|
* @param fileType
|
|
*/
|
|
|
|
|
|
startRecord(fileName, fileType) {
|
|
if (this.recording) {
|
|
return;
|
|
}
|
|
|
|
this.recorder.setFileName(fileName, fileType);
|
|
this.recording = true;
|
|
}
|
|
/**
|
|
*
|
|
*/
|
|
|
|
|
|
stopRecordAndSave() {
|
|
if (this.recording) {
|
|
this.recording = false;
|
|
}
|
|
}
|
|
|
|
_hasControl() {
|
|
let result = false;
|
|
let hasBtnShow = false;
|
|
Object.keys(this._opt.operateBtns).forEach(key => {
|
|
if (this._opt.operateBtns[key]) {
|
|
hasBtnShow = true;
|
|
}
|
|
});
|
|
|
|
if (this._opt.showBandwidth || this._opt.text || hasBtnShow) {
|
|
result = true;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
_onlyMseOrWcsVideo() {
|
|
return this._opt.hasAudio === false && (this._opt.useMSE || this._opt.useWCS && !this._opt.useOffscreen);
|
|
}
|
|
|
|
checkHeart() {
|
|
this.clearCheckHeartTimeout();
|
|
this.checkHeartTimeout();
|
|
} // 心跳检查,如果渲染间隔暂停了多少时间之后,就会抛出异常
|
|
|
|
|
|
checkHeartTimeout() {
|
|
this._checkHeartTimeout = setTimeout(() => {
|
|
if (this.playing) {
|
|
// check again
|
|
if (this._stats.fps !== 0) {
|
|
return;
|
|
}
|
|
|
|
this.pause().then(() => {
|
|
this.emit(EVENTS.timeout, EVENTS.delayTimeout);
|
|
this.emit(EVENTS.delayTimeout);
|
|
});
|
|
}
|
|
}, this._opt.heartTimeout * 1000);
|
|
}
|
|
|
|
checkStatsInterval() {
|
|
this._checkStatsInterval = setInterval(() => {
|
|
this.updateStats();
|
|
}, 1000);
|
|
} //
|
|
|
|
|
|
clearCheckHeartTimeout() {
|
|
if (this._checkHeartTimeout) {
|
|
clearTimeout(this._checkHeartTimeout);
|
|
this._checkHeartTimeout = null;
|
|
}
|
|
} // loading 等待时间
|
|
|
|
|
|
checkLoadingTimeout() {
|
|
this._checkLoadingTimeout = setTimeout(() => {
|
|
// check again
|
|
if (this.playing) {
|
|
return;
|
|
}
|
|
|
|
this.pause().then(() => {
|
|
this.emit(EVENTS.timeout, EVENTS.loadingTimeout);
|
|
this.emit(EVENTS.loadingTimeout);
|
|
});
|
|
}, this._opt.loadingTimeout * 1000);
|
|
}
|
|
|
|
clearCheckLoadingTimeout() {
|
|
if (this._checkLoadingTimeout) {
|
|
clearTimeout(this._checkLoadingTimeout);
|
|
this._checkLoadingTimeout = null;
|
|
}
|
|
}
|
|
|
|
clearStatsInterval() {
|
|
if (this._checkStatsInterval) {
|
|
clearInterval(this._checkStatsInterval);
|
|
this._checkStatsInterval = null;
|
|
}
|
|
}
|
|
|
|
handleRender() {
|
|
if (this.loading) {
|
|
this.emit(EVENTS.start);
|
|
this.loading = false;
|
|
this.clearCheckLoadingTimeout();
|
|
}
|
|
|
|
if (!this.playing) {
|
|
this.playing = true;
|
|
}
|
|
|
|
this.checkHeart();
|
|
} //
|
|
|
|
|
|
updateStats(options) {
|
|
options = options || {};
|
|
|
|
if (!this._startBpsTime) {
|
|
this._startBpsTime = now();
|
|
}
|
|
|
|
if (isNotEmpty(options.ts)) {
|
|
this._stats.ts = options.ts;
|
|
}
|
|
|
|
if (isNotEmpty(options.buf)) {
|
|
this._stats.buf = options.buf;
|
|
}
|
|
|
|
if (options.fps) {
|
|
this._stats.fps += 1;
|
|
}
|
|
|
|
if (options.abps) {
|
|
this._stats.abps += options.abps;
|
|
}
|
|
|
|
if (options.vbps) {
|
|
this._stats.vbps += options.vbps;
|
|
}
|
|
|
|
const _nowTime = now();
|
|
|
|
const timestamp = _nowTime - this._startBpsTime;
|
|
|
|
if (timestamp < 1 * 1000) {
|
|
return;
|
|
}
|
|
|
|
this.emit(EVENTS.stats, this._stats);
|
|
this.emit(EVENTS.performance, fpsStatus(this._stats.fps));
|
|
this._stats.fps = 0;
|
|
this._stats.abps = 0;
|
|
this._stats.vbps = 0;
|
|
this._startBpsTime = _nowTime;
|
|
}
|
|
|
|
resetStats() {
|
|
this._startBpsTime = null;
|
|
this._stats = {
|
|
buf: 0,
|
|
//ms
|
|
fps: 0,
|
|
abps: 0,
|
|
vbps: 0,
|
|
ts: 0
|
|
};
|
|
}
|
|
|
|
enableWakeLock() {
|
|
if (this._opt.keepScreenOn) {
|
|
this.keepScreenOn && this.keepScreenOn.enable();
|
|
}
|
|
}
|
|
|
|
releaseWakeLock() {
|
|
if (this._opt.keepScreenOn) {
|
|
this.keepScreenOn && this.keepScreenOn.disable();
|
|
}
|
|
}
|
|
|
|
handlePlayToRenderTimes() {
|
|
const _times = this._times;
|
|
_times.playTimestamp = _times.playStart - _times.playInitStart;
|
|
_times.streamTimestamp = _times.streamStart - _times.playStart;
|
|
_times.streamResponseTimestamp = _times.streamResponse - _times.streamStart;
|
|
_times.demuxTimestamp = _times.demuxStart - _times.streamResponse;
|
|
_times.decodeTimestamp = _times.decodeStart - _times.demuxStart;
|
|
_times.videoTimestamp = _times.videoStart - _times.decodeStart;
|
|
_times.allTimestamp = _times.videoStart - _times.playInitStart;
|
|
this.emit(EVENTS.playToRenderTimes, _times);
|
|
}
|
|
|
|
getOption() {
|
|
return this._opt;
|
|
}
|
|
|
|
emitError(errorType) {
|
|
let message = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
|
|
this.emit(EVENTS.error, errorType, message);
|
|
this.emit(errorType, message);
|
|
}
|
|
|
|
}
|
|
|
|
class Jessibuca extends Emitter {
|
|
constructor(options) {
|
|
super();
|
|
let _opt = options;
|
|
let $container = options.container;
|
|
|
|
if (typeof options.container === 'string') {
|
|
$container = document.querySelector(options.container);
|
|
}
|
|
|
|
if (!$container) {
|
|
throw new Error('Jessibuca need container option');
|
|
} // check container node name
|
|
|
|
|
|
if ($container.nodeName === 'CANVAS' || $container.nodeName === 'VIDEO') {
|
|
throw new Error(`Jessibuca container type can not be ${$container.nodeName} type`);
|
|
}
|
|
|
|
if (_opt.videoBuffer >= _opt.heartTimeout) {
|
|
throw new Error(`Jessibuca videoBuffer ${_opt.videoBuffer}s must be less than heartTimeout ${_opt.heartTimeout}s`);
|
|
}
|
|
|
|
$container.classList.add('jessibuca-container');
|
|
delete _opt.container; // 禁用离屏渲染
|
|
|
|
_opt.forceNoOffscreen = true; // 移动端不支持自动关闭控制栏
|
|
|
|
if (isMobile()) {
|
|
_opt.controlAutoHide = false;
|
|
} // s -> ms
|
|
|
|
|
|
if (isNotEmpty(_opt.videoBuffer)) {
|
|
_opt.videoBuffer = Number(_opt.videoBuffer) * 1000;
|
|
} // setting
|
|
|
|
|
|
if (isNotEmpty(_opt.timeout)) {
|
|
if (isEmpty(_opt.loadingTimeout)) {
|
|
_opt.loadingTimeout = _opt.timeout;
|
|
}
|
|
|
|
if (isEmpty(_opt.heartTimeout)) {
|
|
_opt.heartTimeout = _opt.timeout;
|
|
}
|
|
}
|
|
|
|
this._opt = _opt;
|
|
this.$container = $container;
|
|
this._loadingTimeoutReplayTimes = 0;
|
|
this._heartTimeoutReplayTimes = 0;
|
|
this.events = new Events(this);
|
|
|
|
this._initPlayer($container, _opt);
|
|
}
|
|
/**
|
|
*
|
|
*/
|
|
|
|
|
|
destroy() {
|
|
if (this.events) {
|
|
this.events.destroy();
|
|
this.events = null;
|
|
}
|
|
|
|
if (this.player) {
|
|
this.player.destroy();
|
|
this.player = null;
|
|
}
|
|
|
|
this.$container = null;
|
|
this._opt = null;
|
|
this._loadingTimeoutReplayTimes = 0;
|
|
this._heartTimeoutReplayTimes = 0;
|
|
this.off();
|
|
}
|
|
|
|
_initPlayer($container, options) {
|
|
this.player = new Player($container, options);
|
|
this.player.debug.log('jessibuca', '_initPlayer', this.player.getOption());
|
|
|
|
this._bindEvents();
|
|
}
|
|
|
|
_resetPlayer() {
|
|
let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
this.player.destroy();
|
|
this.player = null;
|
|
this._opt = Object.assign(this._opt, options);
|
|
this._opt.url = ''; // reset url
|
|
|
|
this._initPlayer(this.$container, this._opt);
|
|
}
|
|
|
|
_bindEvents() {
|
|
// 对外的事件
|
|
Object.keys(JESSIBUCA_EVENTS).forEach(key => {
|
|
this.player.on(JESSIBUCA_EVENTS[key], value => {
|
|
this.emit(key, value);
|
|
});
|
|
});
|
|
}
|
|
/**
|
|
* 是否开启控制台调试打印
|
|
* @param value {Boolean}
|
|
*/
|
|
|
|
|
|
setDebug(value) {
|
|
this.player.updateOption({
|
|
debug: !!value
|
|
});
|
|
}
|
|
/**
|
|
*
|
|
*/
|
|
|
|
|
|
mute() {
|
|
this.player.mute(true);
|
|
}
|
|
/**
|
|
*
|
|
*/
|
|
|
|
|
|
cancelMute() {
|
|
this.player.mute(false);
|
|
}
|
|
/**
|
|
*
|
|
* @param value {number}
|
|
*/
|
|
|
|
|
|
setVolume(value) {
|
|
this.player.volume = value;
|
|
}
|
|
/**
|
|
*
|
|
*/
|
|
|
|
|
|
audioResume() {
|
|
this.player.audio && this.player.audio.audioEnabled(true);
|
|
}
|
|
/**
|
|
* 设置超时时长, 单位秒 在连接成功之前和播放中途,如果超过设定时长无数据返回,则回调timeout事件
|
|
* @param value {number}
|
|
*/
|
|
|
|
|
|
setTimeout(time) {
|
|
time = Number(time);
|
|
this.player.updateOption({
|
|
timeout: time,
|
|
loadingTimeout: time,
|
|
heartTimeout: time
|
|
});
|
|
}
|
|
/**
|
|
*
|
|
* @param type {number}: 0,1,2
|
|
*/
|
|
|
|
|
|
setScaleMode(type) {
|
|
type = Number(type);
|
|
let options = {
|
|
isFullResize: false,
|
|
isResize: false
|
|
};
|
|
|
|
switch (type) {
|
|
case SCALE_MODE_TYPE.full:
|
|
options.isFullResize = false;
|
|
options.isResize = false;
|
|
break;
|
|
|
|
case SCALE_MODE_TYPE.auto:
|
|
options.isFullResize = false;
|
|
options.isResize = true;
|
|
break;
|
|
|
|
case SCALE_MODE_TYPE.fullAuto:
|
|
options.isFullResize = true;
|
|
options.isResize = true;
|
|
break;
|
|
}
|
|
|
|
this.player.updateOption(options);
|
|
this.resize();
|
|
}
|
|
/**
|
|
*
|
|
* @returns {Promise<commander.ParseOptionsResult.unknown>}
|
|
*/
|
|
|
|
|
|
pause() {
|
|
return this.player.pause();
|
|
}
|
|
/**
|
|
*
|
|
*/
|
|
|
|
|
|
close() {
|
|
// clear url
|
|
this._opt.url = '';
|
|
this._opt.playOptions = {};
|
|
return this.player.close();
|
|
}
|
|
/**
|
|
*
|
|
*/
|
|
|
|
|
|
clearView() {
|
|
this.player.video.clearView();
|
|
}
|
|
/**
|
|
*
|
|
* @param url {string}
|
|
* @param options {object}
|
|
* @returns {Promise<unknown>}
|
|
*/
|
|
|
|
|
|
play(url) {
|
|
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
return new Promise((resolve, reject) => {
|
|
if (!url && !this._opt.url) {
|
|
this.emit(EVENTS.error, EVENTS_ERROR.playError);
|
|
reject('play url is empty');
|
|
return;
|
|
}
|
|
|
|
if (url) {
|
|
// url 相等的时候。
|
|
if (this._opt.url) {
|
|
// 存在相同的 url
|
|
if (url === this._opt.url) {
|
|
// 正在播放
|
|
if (this.player.playing) {
|
|
resolve();
|
|
} else {
|
|
// pause -> play
|
|
this.clearView();
|
|
this.player.play(this._opt.url, this._opt.playOptions).then(() => {
|
|
resolve(); // 恢复下之前的音量
|
|
|
|
this.player.resumeAudioAfterPause();
|
|
}).catch(e => {
|
|
this.player.debug.warn('jessibuca', 'pause -> play and play error', e);
|
|
this.player.pause().then(() => {
|
|
reject(e);
|
|
});
|
|
});
|
|
}
|
|
} else {
|
|
// url 发生改变了
|
|
this.player.pause().then(() => {
|
|
// 清除 画面
|
|
this.clearView();
|
|
|
|
this._play(url, options).then(() => {
|
|
resolve();
|
|
}).catch(e => {
|
|
this.player.debug.warn('jessibuca', 'this._play error', e);
|
|
reject(e);
|
|
});
|
|
}).catch(e => {
|
|
this.player.debug.warn('jessibuca', 'this._opt.url is null and pause error', e);
|
|
reject(e);
|
|
});
|
|
}
|
|
} else {
|
|
this._play(url, options).then(() => {
|
|
resolve();
|
|
}).catch(e => {
|
|
this.player.debug.warn('jessibuca', 'this._play error', e);
|
|
reject(e);
|
|
});
|
|
}
|
|
} else {
|
|
// url 不存在的时候
|
|
// 就是从 play -> pause -> play
|
|
this.player.play(this._opt.url, this._opt.playOptions).then(() => {
|
|
resolve(); // 恢复下之前的音量
|
|
|
|
this.player.resumeAudioAfterPause();
|
|
}).catch(e => {
|
|
this.player.debug.warn('jessibuca', 'url is null and play error', e);
|
|
this.player.pause().then(() => {
|
|
reject(e);
|
|
});
|
|
});
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
*
|
|
* @param url {string}
|
|
* @param options {object}
|
|
* @returns {Promise<unknown>}
|
|
* @private
|
|
*/
|
|
|
|
|
|
_play(url) {
|
|
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
return new Promise((resolve, reject) => {
|
|
this._opt.url = url;
|
|
this._opt.playOptions = options; // 新的url
|
|
|
|
const isHttp = url.indexOf("http") === 0; //
|
|
|
|
const protocol = isHttp ? PLAYER_PLAY_PROTOCOL.fetch : PLAYER_PLAY_PROTOCOL.websocket; //
|
|
|
|
const demuxType = isHttp || url.indexOf(".flv") !== -1 || this._opt.isFlv ? DEMUX_TYPE.flv : DEMUX_TYPE.m7s;
|
|
this.player.updateOption({
|
|
protocol,
|
|
demuxType
|
|
});
|
|
this.player.once(EVENTS_ERROR.webglAlignmentError, () => {
|
|
this.pause().then(() => {
|
|
this.player.debug.log('Jessibuca', 'webglAlignmentError');
|
|
|
|
this._resetPlayer({
|
|
openWebglAlignment: true
|
|
});
|
|
|
|
this.play(url).then(() => {
|
|
// resolve();
|
|
this.player.debug.log('Jessibuca', 'webglAlignmentError and play success');
|
|
}).catch(() => {
|
|
// reject();
|
|
this.player.debug.log('Jessibuca', 'webglAlignmentError and play error');
|
|
});
|
|
});
|
|
});
|
|
this.player.once(EVENTS_ERROR.mediaSourceH265NotSupport, () => {
|
|
this.pause().then(() => {
|
|
if (this.player._opt.autoWasm) {
|
|
this.player.debug.log('Jessibuca', 'auto wasm [mse-> wasm] reset player and play');
|
|
|
|
this._resetPlayer({
|
|
useMSE: false
|
|
});
|
|
|
|
this.play(url, options).then(() => {
|
|
// resolve();
|
|
this.player.debug.log('Jessibuca', 'auto wasm [mse-> wasm] reset player and play success');
|
|
}).catch(() => {
|
|
// reject();
|
|
this.player.debug.log('Jessibuca', 'auto wasm [mse-> wasm] reset player and play error');
|
|
});
|
|
}
|
|
});
|
|
}); // media source full error
|
|
|
|
this.player.once(EVENTS_ERROR.mediaSourceFull, () => {
|
|
this.pause().then(() => {
|
|
this.player.debug.log('Jessibuca', 'media source full');
|
|
|
|
this._resetPlayer();
|
|
|
|
this.play(url).then(() => {
|
|
// resolve();
|
|
this.player.debug.log('Jessibuca', 'media source full and reset player and play success');
|
|
}).catch(() => {
|
|
// reject();
|
|
this.player.debug.warn('Jessibuca', 'media source full and reset player and play error');
|
|
});
|
|
});
|
|
}); // media source append buffer error
|
|
|
|
this.player.once(EVENTS_ERROR.mediaSourceAppendBufferError, () => {
|
|
this.pause().then(() => {
|
|
this.player.debug.log('Jessibuca', 'media source append buffer error');
|
|
|
|
this._resetPlayer();
|
|
|
|
this.play(url).then(() => {
|
|
// resolve();
|
|
this.player.debug.log('Jessibuca', 'media source append buffer error and reset player and play success');
|
|
}).catch(() => {
|
|
// reject();
|
|
this.player.debug.warn('Jessibuca', 'media source append buffer error and reset player and play error');
|
|
});
|
|
});
|
|
});
|
|
this.player.once(EVENTS_ERROR.mediaSourceBufferListLarge, () => {
|
|
this.pause().then(() => {
|
|
this.player.debug.log('Jessibuca', 'media source buffer list large');
|
|
|
|
this._resetPlayer();
|
|
|
|
this.play(url).then(() => {
|
|
// resolve();
|
|
this.player.debug.log('Jessibuca', 'media source buffer list large and reset player and play success');
|
|
}).catch(() => {
|
|
// reject();
|
|
this.player.debug.warn('Jessibuca', 'media source buffer list large and reset player and play error');
|
|
});
|
|
});
|
|
});
|
|
this.player.once(EVENTS_ERROR.mediaSourceAppendBufferEndTimeout, () => {
|
|
this.pause().then(() => {
|
|
this.player.debug.log('Jessibuca', 'media source append buffer end timeout');
|
|
|
|
this._resetPlayer();
|
|
|
|
this.play(url).then(() => {
|
|
// resolve();
|
|
this.player.debug.log('Jessibuca', 'media source append buffer end timeout and reset player and play success');
|
|
}).catch(() => {
|
|
// reject();
|
|
this.player.debug.warn('Jessibuca', 'media source append buffer end timeout and reset player and play error');
|
|
});
|
|
});
|
|
});
|
|
this.player.once(EVENTS_ERROR.mseSourceBufferError, () => {
|
|
this.pause().then(() => {
|
|
this.player.debug.log('Jessibuca', 'mseSourceBufferError close success');
|
|
});
|
|
}); //
|
|
|
|
this.player.once(EVENTS_ERROR.webcodecsH265NotSupport, () => {
|
|
this.pause().then(() => {
|
|
if (this.player._opt.autoWasm) {
|
|
this.player.debug.log('Jessibuca', 'auto wasm [wcs-> wasm] reset player and play');
|
|
|
|
this._resetPlayer({
|
|
useWCS: false
|
|
});
|
|
|
|
this.play(url).then(() => {
|
|
// resolve();
|
|
this.player.debug.log('Jessibuca', 'auto wasm [wcs-> wasm] reset player and play success');
|
|
}).catch(() => {
|
|
// reject();
|
|
this.player.debug.warn('Jessibuca', 'auto wasm [wcs-> wasm] reset player and play error');
|
|
});
|
|
}
|
|
});
|
|
}); // webcodecs
|
|
|
|
this.player.once(EVENTS_ERROR.webcodecsWidthOrHeightChange, () => {
|
|
this.pause().then(() => {
|
|
this.player.debug.log('Jessibuca', 'webcodecs Width Or Height Change reset player and play');
|
|
|
|
this._resetPlayer({
|
|
useWCS: true
|
|
});
|
|
|
|
this.play(url).then(() => {
|
|
// resolve();
|
|
this.player.debug.log('Jessibuca', 'webcodecs Width Or Height Change reset player and play success');
|
|
}).catch(() => {
|
|
// reject();
|
|
this.player.debug.warn('Jessibuca', 'webcodecs Width Or Height Change reset player and play error');
|
|
});
|
|
});
|
|
}); // webcodecs
|
|
|
|
this.player.once(EVENTS_ERROR.webcodecsDecodeError, () => {
|
|
this.pause().then(() => {
|
|
if (this.player._opt.autoWasm) {
|
|
this.player.debug.log('Jessibuca', 'webcodecs decode error reset player and play');
|
|
|
|
this._resetPlayer({
|
|
useWCS: false
|
|
});
|
|
|
|
this.play(url).then(() => {
|
|
// resolve();
|
|
this.player.debug.log('Jessibuca', 'webcodecs decode error reset player and play success');
|
|
}).catch(() => {
|
|
// reject();
|
|
this.player.debug.warn('Jessibuca', 'webcodecs decode error reset player and play error');
|
|
});
|
|
}
|
|
});
|
|
}); // wasm。
|
|
|
|
this.player.once(EVENTS_ERROR.wasmDecodeError, () => {
|
|
if (this.player._opt.wasmDecodeErrorReplay) {
|
|
this.pause().then(() => {
|
|
this.player.debug.log('Jessibuca', 'wasm decode error and reset player and play');
|
|
|
|
this._resetPlayer({
|
|
useWCS: false
|
|
});
|
|
|
|
this.play(url, options).then(() => {
|
|
// resolve();
|
|
this.player.debug.log('Jessibuca', 'wasm decode error and reset player and play success');
|
|
}).catch(() => {
|
|
// reject();
|
|
this.player.debug.warn('Jessibuca', 'wasm decode error and reset player and play error');
|
|
});
|
|
});
|
|
}
|
|
}); // 监听 delay timeout
|
|
|
|
this.player.on(EVENTS.delayTimeout, () => {
|
|
if (this.player._opt.heartTimeoutReplay && (this._heartTimeoutReplayTimes < this.player._opt.heartTimeoutReplayTimes || this.player._opt.heartTimeoutReplayTimes === -1)) {
|
|
this.player.debug.log('Jessibuca', `delay timeout replay time is ${this._heartTimeoutReplayTimes}`);
|
|
this._heartTimeoutReplayTimes += 1;
|
|
this.play(url, options).then(() => {
|
|
// resolve();
|
|
this._heartTimeoutReplayTimes = 0;
|
|
}).catch(() => {// reject();
|
|
});
|
|
}
|
|
}); // 监听 loading timeout
|
|
|
|
this.player.on(EVENTS.loadingTimeout, () => {
|
|
if (this.player._opt.loadingTimeoutReplay && (this._loadingTimeoutReplayTimes < this.player._opt.loadingTimeoutReplayTimes || this.player._opt.loadingTimeoutReplayTimes === -1)) {
|
|
this.player.debug.log('Jessibuca', `loading timeout replay time is ${this._loadingTimeoutReplayTimes}`);
|
|
this._loadingTimeoutReplayTimes += 1;
|
|
this.play(url, options).then(() => {
|
|
// resolve();
|
|
this._loadingTimeoutReplayTimes = 0;
|
|
}).catch(() => {// reject();
|
|
});
|
|
}
|
|
});
|
|
|
|
if (this.hasLoaded()) {
|
|
this.player.play(url, options).then(() => {
|
|
resolve();
|
|
}).catch(e => {
|
|
this.player.debug.warn('Jessibuca', 'hasLoaded and play error', e);
|
|
this.player.pause().then(() => {
|
|
reject(e);
|
|
});
|
|
});
|
|
} else {
|
|
this.player.once(EVENTS.decoderWorkerInit, () => {
|
|
this.player.play(url, options).then(() => {
|
|
resolve();
|
|
}).catch(e => {
|
|
this.player.debug.warn('Jessibuca', 'decoderWorkerInit and play error', e);
|
|
this.player.pause().then(() => {
|
|
reject(e);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
*
|
|
*/
|
|
|
|
|
|
resize() {
|
|
this.player.resize();
|
|
}
|
|
/**
|
|
*
|
|
* @param time {number} s
|
|
*/
|
|
|
|
|
|
setBufferTime(time) {
|
|
time = Number(time); // s -> ms
|
|
|
|
this.player.updateOption({
|
|
videoBuffer: time * 1000
|
|
}); // update worker config
|
|
|
|
this.player.decoderWorker && this.player.decoderWorker.updateWorkConfig({
|
|
key: 'videoBuffer',
|
|
value: time * 1000
|
|
});
|
|
}
|
|
/**
|
|
*
|
|
* @param deg {number}
|
|
*/
|
|
|
|
|
|
setRotate(deg) {
|
|
deg = parseInt(deg, 10);
|
|
const list = [0, 90, 180, 270];
|
|
|
|
if (this._opt.rotate === deg || list.indexOf(deg) === -1) {
|
|
return;
|
|
}
|
|
|
|
this.player.updateOption({
|
|
rotate: deg
|
|
});
|
|
this.resize();
|
|
}
|
|
/**
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
|
|
|
|
hasLoaded() {
|
|
return this.player.loaded;
|
|
}
|
|
/**
|
|
*
|
|
*/
|
|
|
|
|
|
setKeepScreenOn() {
|
|
this.player.updateOption({
|
|
keepScreenOn: true
|
|
});
|
|
}
|
|
/**
|
|
*
|
|
* @param flag {Boolean}
|
|
*/
|
|
|
|
|
|
setFullscreen(flag) {
|
|
const fullscreen = !!flag;
|
|
|
|
if (this.player.fullscreen !== fullscreen) {
|
|
this.player.fullscreen = fullscreen;
|
|
}
|
|
}
|
|
/**
|
|
*
|
|
* @param filename {string}
|
|
* @param format {string}
|
|
* @param quality {number}
|
|
* @param type {string} download,base64,blob
|
|
*/
|
|
|
|
|
|
screenshot(filename, format, quality, type) {
|
|
if (!this.player.video) {
|
|
return '';
|
|
}
|
|
|
|
return this.player.video.screenshot(filename, format, quality, type);
|
|
}
|
|
/**
|
|
*
|
|
* @param fileName {string}
|
|
* @param fileType {string}
|
|
* @returns {Promise<unknown>}
|
|
*/
|
|
|
|
|
|
startRecord(fileName, fileType) {
|
|
return new Promise((resolve, reject) => {
|
|
if (this.player.playing) {
|
|
this.player.startRecord(fileName, fileType);
|
|
resolve();
|
|
} else {
|
|
reject();
|
|
}
|
|
});
|
|
}
|
|
|
|
stopRecordAndSave() {
|
|
if (this.player.recording) {
|
|
this.player.stopRecordAndSave();
|
|
}
|
|
}
|
|
/**
|
|
*
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
|
|
isPlaying() {
|
|
return this.player ? this.player.playing : false;
|
|
}
|
|
/**
|
|
* 是否静音状态
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
|
|
isMute() {
|
|
return this.player.audio ? this.player.audio.isMute : true;
|
|
}
|
|
/**
|
|
* 是否在录制视频
|
|
* @returns {*}
|
|
*/
|
|
|
|
|
|
isRecording() {
|
|
return this.player.recorder.recording;
|
|
}
|
|
|
|
}
|
|
|
|
_defineProperty(Jessibuca, "ERROR", EVENTS_ERROR);
|
|
|
|
_defineProperty(Jessibuca, "TIMEOUT", {
|
|
loadingTimeout: EVENTS.loadingTimeout,
|
|
delayTimeout: EVENTS.delayTimeout
|
|
});
|
|
|
|
window.Jessibuca = Jessibuca;
|
|
|
|
return Jessibuca;
|
|
|
|
}));
|
|
//# sourceMappingURL=jessibuca.js.map
|
|
|