( 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()" < / c o d e > m e t h o d . U s a g e o f " g e t B l o b " i s r e c o m m e n d e d , t h o u g h .
* @ 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 2 nd 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.6 e + 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 8 K or 16 K . 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> ` : '' }
< / d i v >
$ { options . hasControl && operateBtns . play ? ` <div class="jessibuca-play-big"></div> ` : '' }
$ { options . hasControl ? `
< div class = "jessibuca-recording" >
< div class = "jessibuca-recording-red-point" > < / d i v >
< div class = "jessibuca-recording-time" > 00 : 00 : 01 < / d i v >
< div class = "jessibuca-icon-recordStop jessibuca-recording-stop" > $ { icons . recordStop } < / d i v >
< / d i v >
` : ''}
$ { 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> ` : '' }
< / d i v >
< 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" > < / d i v >
< / d i v >
< div class = "jessibuca-volume-panel-text" > < / d i v >
< / d i v >
< / d i v >
` : ''}
$ { 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> ` : '' }
< / d i v >
< / d i v >
< / d i v >
` : ''}
` );
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 3 px ; transform : translateX ( - 50 % ) ; justify - content : space - around ; align - items : center ; width : 95 px ; height : 20 px ; background : # 000 ; opacity : 1 ; border - radius : 0 0 8 px 8 px ; z - index : 1 } . jessibuca - container . jessibuca - recording . jessibuca - recording - red - point { width : 8 px ; height : 8 px ; background : # ff1f1f ; border - radius : 50 % ; animation : magentaPulse 1 s linear infinite } . jessibuca - container . jessibuca - recording . jessibuca - recording - time { font - size : 14 px ; font - weight : 500 ; color : # ddd } . jessibuca - container . jessibuca - recording . jessibuca - icon - recordStop { width : 16 px ; height : 16 px ; 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 : 20 px ; font - size : 13 px ; color : # fff ; margin - top : 10 px } . 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 : 38 px ; width : 100 % ; padding - left : 13 px ; padding - right : 13 px ; font - size : 14 px ; color : # fff ; opacity :
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 , GkXfowEAAAAAAAAfQoaBAUL3gQFC8oEEQvOBCEKChHdlYm1Ch4EEQoWBAhhTgGcBAAAAAAAVkhFNm3RALE27i1OrhBVJqWZTrIHfTbuMU6uEFlSua1OsggEwTbuMU6uEHFO7a1OsghV17AEAAAAAAACkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVSalmAQAAAAAAAEUq17GDD0JATYCNTGF2ZjU1LjMzLjEwMFdBjUxhdmY1NS4zMy4xMDBzpJBlrrXf3DCDVB8KcgbMpcr + RImIQJBgAAAAAAAWVK5rAQAAAAAAD ++ uAQAAAAAAADLXgQFzxYEBnIEAIrWcg3VuZIaFVl9WUDiDgQEj44OEAmJaAOABAAAAAAAABrCBsLqBkK4BAAAAAAAPq9eBAnPFgQKcgQAitZyDdW5khohBX1ZPUkJJU4OBAuEBAAAAAAAAEZ + BArWIQOdwAAAAAABiZIEgY6JPbwIeVgF2b3JiaXMAAAAAAoC7AAAAAAAAgLUBAAAAAAC4AQN2b3JiaXMtAAAAWGlwaC5PcmcgbGliVm9yYmlzIEkgMjAxMDExMDEgKFNjaGF1ZmVudWdnZXQpAQAAABUAAABlbmNvZGVyPUxhdmM1NS41Mi4xMDIBBXZvcmJpcyVCQ1YBAEAAACRzGCpGpXMWhBAaQlAZ4xxCzmvsGUJMEYIcMkxbyyVzkCGkoEKIWyiB0JBVAABAAACHQXgUhIpBCCGEJT1YkoMnPQghhIg5eBSEaUEIIYQQQgghhBBCCCGERTlokoMnQQgdhOMwOAyD5Tj4HIRFOVgQgydB6CCED0K4moOsOQghhCQ1SFCDBjnoHITCLCiKgsQwuBaEBDUojILkMMjUgwtCiJqDSTX4GoRnQXgWhGlBCCGEJEFIkIMGQcgYhEZBWJKDBjm4FITLQagahCo5CB + EIDRkFQCQAACgoiiKoigKEBqyCgDIAAAQQFEUx3EcyZEcybEcCwgNWQUAAAEACAAAoEiKpEiO5EiSJFmSJVmSJVmS5omqLMuyLMuyLMsyEBqyCgBIAABQUQxFcRQHCA1ZBQBkAAAIoDiKpViKpWiK54iOCISGrAIAgAAABAAAEDRDUzxHlETPVFXXtm3btm3btm3btm3btm1blmUZCA1ZBQBAAAAQ0mlmqQaIMAMZBkJDVgEACAAAgBGKMMSA0JBVAABAAACAGEoOogmtOd + c46BZDppKsTkdnEi1eZKbirk555xzzsnmnDHOOeecopxZDJoJrTnnnMSgWQqaCa0555wnsXnQmiqtOeeccc7pYJwRxjnnnCateZCajbU555wFrWmOmkuxOeecSLl5UptLtTnnnHPOOeecc84555zqxekcnBPOOeecqL25lpvQxTnnnE / G6d6cEM4555xzzjnnnHPOOeecIDRkFQAABABAEIaNYdwpCNLnaCBGEWIaMulB9 + gwCRqDnELq0ehopJQ6CCWVcVJKJwgNWQUAAAIAQAghhRRSSCGFFFJIIYUUYoghhhhyyimnoIJKKqmooowyyyyzzDLLLLPMOuyssw47DDHEEEMrrcRSU2011lhr7jnnmoO0VlprrbVSSimllFIKQkNWAQAgAAAEQgYZZJBRSCGFFGKIKaeccgoqqIDQkFUAACAAgAAAAABP8hzRER3RER3RER3RER3R8RzPESVREiVREi3TMjXTU0VVdWXXlnVZt31b2IVd933d933d + HVhWJZlWZZlWZZlWZZlWZZlWZYgNGQVAAACAAAghBBCSCGFFFJIKcYYc8w56CSUEAgNWQUAAAIACAAAAHAUR3EcyZEcSbIkS9IkzdIsT / M0TxM9URRF0zRV0RVdUTdtUTZl0zVdUzZdVVZtV5ZtW7Z125dl2 / d93 / d93 / d93 / d93 / d9XQdCQ1YBABIAADqSIymSIimS4ziOJElAaMgqAEAGAEAAAIriKI7jOJIkSZIlaZJneZaomZrpmZ4qqkBoyCoAABAAQAAAAAAAAIqmeIqpeIqoeI7oiJJomZaoqZoryqbsuq7ruq7ruq7ruq7ruq7ruq7ruq7ruq7ruq7ruq7ruq7ruq4LhIasAgAkAAB0JEdyJEdSJEVSJEdygNCQVQCADACAAAAcwzEkRXIsy9I0T / M0TxM90RM901NFV3SB0JBVAAAgAIAAAAAAAAAMybAUy9EcTRIl1VItVVMt1VJF1VNVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVN0zRNEwgNWQkAkAEAkBBTLS3GmgmLJGLSaqugYwxS7KWxSCpntbfKMYUYtV4ah5RREHupJGOKQcwtpNApJq3WVEKFFKSYYyoVUg5SIDRkhQAQmgHgcBxAsixAsiwAAAAAAAAAkDQN0DwPsDQPAAAAAAAAACRNAyxPAzTPAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA0jRA8zxA8zwAAAAAAAAA0DwP8DwR8EQRAAAAAAAAACzPAzTRAzxRBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA0jRA8zxA8zwAAAAAAAAAsDwP8EQR0DwRAAAAAAAAACzPAzxRBDzRAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAEOAAABBgIRQasiIAiBMAcEgSJAmSBM0DSJYFTYOmwTQBkmVB06BpME0AAAAAAAAAAAAAJE2DpkHTIIoASdOgadA0iCIAAAAAAAAAAAAAkqZB06BpEEWApGnQNGgaRBEAAAAAAAAAAAAAzzQhihBFmCbAM02IIkQRpgkAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAGHAAAAgwoQwUGrIiAIgTAHA4imUBAIDjOJYFAACO41gWAABYliWKAABgWZooAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAYcAAAC
const MP4 = " data : video / mp4 ; base64 , AAAAHGZ0eXBNNFYgAAACAGlzb21pc28yYXZjMQAAAAhmcmVlAAAGF21kYXTeBAAAbGliZmFhYyAxLjI4AABCAJMgBDIARwAAArEGBf //rdxF6b3m2Ui3lizYINkj7u94MjY0IC0gY29yZSAxNDIgcjIgOTU2YzhkOCAtIEguMjY0L01QRUctNCBBVkMgY29kZWMgLSBDb3B5bGVmdCAyMDAzLTIwMTQgLSBodHRwOi8vd3d3LnZpZGVvbGFuLm9yZy94MjY0Lmh0bWwgLSBvcHRpb25zOiBjYWJhYz0wIHJlZj0zIGRlYmxvY2s9MTowOjAgYW5hbHlzZT0weDE6MHgxMTEgbWU9aGV4IHN1Ym1lPTcgcHN5PTEgcHN5X3JkPTEuMDA6MC4wMCBtaXhlZF9yZWY9MSBtZV9yYW5nZT0xNiBjaHJvbWFfbWU9MSB0cmVsbGlzPTEgOHg4ZGN0PTAgY3FtPTAgZGVhZHpvbmU9MjEsMTEgZmFzdF9wc2tpcD0xIGNocm9tYV9xcF9vZmZzZXQ9LTIgdGhyZWFkcz02IGxvb2thaGVhZF90aHJlYWRzPTEgc2xpY2VkX3RocmVhZHM9MCBucj0wIGRlY2ltYXRlPTEgaW50ZXJsYWNlZD0wIGJsdXJheV9jb21wYXQ9MCBjb25zdHJhaW5lZF9pbnRyYT0wIGJmcmFtZXM9MCB3ZWlnaHRwPTAga2V5aW50PTI1MCBrZXlpbnRfbWluPTI1IHNjZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NDAgcmM9Y3JmIG1idHJlZT0xIGNyZj0yMy4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCB2YnZfbWF4cmF0ZT03NjggdmJ2X2J1ZnNpemU9MzAwMCBjcmZfbWF4PTAuMCBuYWxfaHJkPW5vbmUgZmlsbGVyPTAgaXBfcmF0aW89MS40MCBhcT0xOjEuMDAAgAAAAFZliIQL8mKAAKvMnJycnJycnJycnXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXiEASZACGQAjgCEASZACGQAjgAAAAAdBmjgX4GSAIQBJkAIZACOAAAAAB0GaVAX4GSAhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZpgL8DJIQBJkAIZACOAIQBJkAIZACOAAAAABkGagC/AySEASZACGQAjgAAAAAZBmqAvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZrAL8DJIQBJkAIZACOAAAAABkGa4C/AySEASZACGQAjgCEASZACGQAjgAAAAAZBmwAvwMkhAEmQAhkAI4AAAAAGQZsgL8DJIQBJkAIZACOAIQBJkAIZACOAAAAABkGbQC/AySEASZACGQAjgCEASZACGQAjgAAAAAZBm2AvwMkhAEmQAhkAI4AAAAAGQZuAL8DJIQBJkAIZACOAIQBJkAIZACOAAAAABkGboC/AySEASZACGQAjgAAAAAZBm8AvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZvgL8DJIQBJkAIZACOAAAAABkGaAC/AySEASZACGQAjgCEASZACGQAjgAAAAAZBmiAvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZpAL8DJIQBJkAIZACOAAAAABkGaYC/AySEASZACGQAjgCEASZACGQAjgAAAAAZBmoAvwMkhAEmQAhkAI4AAAAAGQZqgL8DJIQBJkAIZACOAIQBJkAIZACOAAAAABkGawC/AySEASZACGQAjgAAAAAZBmuAvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZsAL8DJIQBJkAIZACOAAAAABkGbIC/AySEASZACGQAjgCEASZACGQAjgAAAAAZBm0AvwMkhAEmQAhkAI4AhAEmQAhkAI4AAAAAGQZtgL8DJIQBJkAIZACOAAAAABkGbgCvAySEASZACGQAjgCEASZACGQAjgAAAAAZBm6AnwMkhAEmQAhkAI4AhAEmQAhkAI4AhAEmQAhkAI4AhAEmQAhkAI4AAAAhubW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAAD6AAABDcAAQAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAzB0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAAAA+kAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAALAAAACQAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAAPpAAAAAAABAAAAAAKobWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAAB1MAAAdU5VxAAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAACU21pbmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAhNzdGJsAAAAr3N0c2QAAAAAAAAAAQAAAJ9hdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAALAAkABIAAAASAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGP//AAAALWF2Y0MBQsAN/+EAFWdCwA3ZAsTsBEAAAPpAADqYA8UKkgEABWjLg8sgAAAAHHV1aWRraEDyXyRPxbo5pRvPAyPzAAAAAAAAABhzdHRzAAAAAAAAAAEAAAAeAAAD6QAAABRzdHNzAAAAAAAAAAEAAAABAAAAHHN0c2MAAAAAAAAAAQAAAAEAAAABAAAAAQAAAIxzdHN6AAAAAAAAAAAAAAAeAAADDwAAAAsAAAALAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAAiHN0Y28AAAAAAAAAHgAAAEYAAANnAAADewAAA5gAAAO0AAADxwAAA+MAAAP2AAAEEgAABCUAAARBAAAEXQAABHAAAASMAAAEnwAABLsAAATOAAAE6gAABQYAAAUZAAAFNQAABUgAAAVkAAAFdwAABZMAAAWmAAAFwgAABd4AAAXxAAAGDQAABGh0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAACAAAAAAAABDcAAAAAAAAAAAAAAAEBAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAAQkAAADcAABAAAAAAPgbWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAAC7gAAAykBVxAAAAAAALWhkbHIAAAAAAAAAAHNvdW4AAAAAAAAAAAAAAABTb3VuZEhhbmRsZXIAAAADi21pbmYAAAAQc21oZAAAAAAAAAAAAAAAJGRpbmYAAAAcZHJlZgAAAAAAAAABAAAADHVybCAAAAABAAADT3N0YmwAAABnc3RzZAAAAAAAAAABAAAAV21wNGEAAAAAAAAAAQAAAAAAAAAAAAIAEAAAAAC7gAAAAAAAM2VzZHMAAAAAA4CAgCIAAgAEgICAFEAVBbjYAAu4AAAADcoFgICAAhGQBoCAgAECAAAAIHN0dHMAAAAAAAAAAgAAADIAAAQAAAAAAQAAAkAAAAFUc3RzYwAAAAAAAAAbAAAAAQAAAAEAAAABAAAAAgAAAAIAAAABAAAAAwAAAAEAAAABAAAABAAAAAIAAAABAAAABgAAAAEAAAABAAAABwAAAAIAAAABAAAACAA
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