declare let LibAV , MediaRecorder ; import * as audio from "./audio" ; import * as avloader from "./avloader" ; import * as config from "./config" ; import * as downloadStream from "./download-stream" ; import * as fileStorage from "./file-storage" ; import * as jitsi from "./jitsi" ; import * as log from "./log" ; import * as net from "./net" ; import { prot } from "./protocol" ; import * as ui from "./ui" ; import * as util from "./util" ; import * as video from "./video" ; import * as wsp from "web-streams-polyfill/ponyfill" ; export const supported = ( typeof MediaRecorder !== "undefined" ) ; let recordVideoStop = null ; export let recordVideoRemoteOK = null ; let recordVideoRemoteOKTimeout = null ; interface RecordVideoOptions { local ? : boolean ; remote ? : boolean ; remotePeer ? : number ; bitrate ? : number ; localWriter ? : WritableStreamDefaultWriter ; } const fixedTimeBase = { num : 1 , den : 1000 } ; async function recordVideo ( opts ) { recordVideoUI ( true ) ; const formats = [ [ "webm" , "vp9" , false ] , [ "webm" , "vp8" , false ] , [ "mp4" , "avc1" , true ] ] ; let format ; let mimeType ; let fi ; for ( fi = 0 ; fi < formats . length ; fi ++ ) { format = formats [ fi ] ; mimeType = "video/" + format [ 0 ] + "; codecs=" + format [ 1 ] ; if ( MediaRecorder . isTypeSupported ( mimeType ) ) break ; } if ( fi === formats . length ) { log . pushStatus ( "mediaRecorder" , "No supported video encoder found!" ) ; setTimeout ( function ( ) { log . popStatus ( "mediaRecorder" ) ; } , 10000 ) ; return ; } const outFormat = ( format [ 0 ] === "webm" ) ? "webm" : "mkv" ; let filename = "" ; if ( net . recName ) filename = net . recName + "-" ; filename += config . username + "-video." + outFormat ; const videoSettings = video . userMediaVideo . getVideoTracks ( ) [ 0 ] . getSettings ( ) ; let bitrate ; if ( opts . bitrate ) bitrate = Math . min ( opts . bitrate * 1000000 , videoSettings . height * 100000 ) ; else bitrate = videoSettings . height * 10000 ; const globalFrameTime = 1 / videoSettings . frameRate * 1000 ; const inExt = ( format [ 0 ] === "x-matroska" ) ? "mkv" : format [ 0 ] ; const inF = "in." + inExt ; const outF = "out." + outFormat ; if ( opts . remote ) { if ( typeof opts . remotePeer !== "number" ) { jitsi . videoRecSend ( jitsi . videoRecHost , prot . videoRec . startVideoRecReq , { ext : outFormat } ) ; await new Promise < void > ( res => { recordVideoRemoteOK = ( peer ) => { if ( recordVideoRemoteOKTimeout ) { clearTimeout ( recordVideoRemoteOKTimeout ) ; recordVideoRemoteOKTimeout = null ; opts . remotePeer = peer ; res ( ) ; } } ; recordVideoRemoteOKTimeout = setTimeout ( ( ) => { res ( ) ; } , 5000 ) ; } ) ; recordVideoRemoteOK = null ; if ( recordVideoRemoteOKTimeout ) { clearTimeout ( recordVideoRemoteOKTimeout ) ; recordVideoRemoteOKTimeout = null ; } } if ( typeof opts . remotePeer !== "number" ) { opts . remote = false ; } } if ( ! opts . remote && ! opts . local ) opts . local = true ; await avloader . loadLibAV ( ) ; const libav = await LibAV . LibAV ( ) ; await libav . mkreaderdev ( inF ) ; const libavBuf = [ ] ; let libavPos = 0 ; let libavNotify = null ; libav . onwrite = function ( name , pos , chunk ) { if ( chunk ) { if ( libavPos !== pos ) return ; libavPos = pos + chunk . length ; } libavBuf . push ( ( new Uint8Array ( chunk . buffer ) ) . slice ( 0 ) ) ; if ( libavNotify ) libavNotify ( ) ; } ; const libavStream = new wsp . ReadableStream < Uint8Array > ( { async pull ( controller ) { if ( ! libavBuf . length ) { await new Promise < void > ( res => libavNotify = res ) ; libavNotify = null ; } const chunk = libavBuf . shift ( ) ; if ( chunk ) controller . enqueue ( chunk ) ; else controller . close ( ) ; } } ) ; let out_oc = - 1 ; if ( format [ 2 ] ) { let data ; const mrs = getMediaRecorderStream ( video . userMediaVideo , { mimeType : mimeType , videoBitsPerSecond : bitrate } ) ; const rdr = mrs . stream . getReader ( ) ; { const blob = await rdr . read ( ) ; const ab = await blobToArrayBuffer ( blob . value ) ; data = new Uint8Array ( ab ) ; } try { mrs . mediaRecorder . stop ( ) ; } catch ( ex ) { } while ( true ) { const blob = await rdr . read ( ) ; if ( blob . done ) break ; const u8 = new Uint8Array ( await blobToArrayBuffer ( blob . value ) ) ; const newData = new Uint8Array ( data . length + u8 . length ) ; newData . set ( data ) ; newData . set ( u8 , data . length ) ; data = newData ; } const tmpFile = inF + ".tmp." + inExt ; await libav . writeFile ( tmpFile , data ) ; const [ in_fmt_ctx , [ in_stream ] ] = await libav . ff_init_demuxer_file ( tmpFile ) ; await libav . avformat_find_stream_info ( in_fmt_ctx ) ; const c = await libav . avcodec_alloc_context3 ( 0 ) ; await libav . avcodec_parameters_to_context ( c , in_stream . codecpar ) ; [ out_oc ] = await libav . ff_init_muxer ( { filename : outF , open : true , device : true } , [ [ c , fixedTimeBase . num , fixedTimeBase . den ] ] ) ; await libav . avformat_write_header ( out_oc , 0 ) ; await libav . avcodec_free_context_js ( c ) ; await libav . avformat_close_input_js ( in_fmt_ctx ) ; await libav . unlink ( tmpFile ) ; } const mrs = getMediaRecorderStream ( video . userMediaVideo , { mimeType : mimeType , videoBitsPerSecond : bitrate } ) ; const inputRdr = mrs . stream . getReader ( ) ; const recordPromise = ( async function ( ) { ( async function ( ) { while ( true ) { const rdres = await inputRdr . read ( ) ; const chunk = rdres . done ? null : new Uint8Array ( await blobToArrayBuffer ( rdres . value ) ) ; await libav . ff_reader_dev_send ( inF , chunk ) ; if ( rdres . done ) break ; } } ) ( ) ; ( async function ( ) { const [ in_fmt_ctx , [ in_stream ] ] = await libav . ff_init_demuxer_file ( inF ) ; const c = await libav . avcodec_alloc_context3 ( 0 ) ; const pkt = await libav . av_packet_alloc ( ) ; await libav . avcodec_parameters_to_context ( c , in_stream . codecpar ) ; function timeFrom ( fromhi , from ) { from += fromhi * 0x100000000 ; return from * in_stream . time_base_num / in_stream . time_base_den * 1000 ; } function timeTo ( from ) { const to = from * fixedTimeBase . den / fixedTimeBase . num / 1000 ; return { hi : ~ ~ ( to / 0x100000000 ) , lo : ~ ~ ( to % 0x100000000 ) } ; } let lastDTS = 0 ; let dtsOffset = null ; let starterPackets = [ ] ; while ( true ) { const [ res , allPackets ] = await libav . ff_read_multi ( in_fmt_ctx , pkt , inF , { devLimit : 1 , limit : 1 } ) ; const endTimeReal = performance . now ( ) ; if ( res !== 0 && res !== - libav . EAGAIN && res !== libav . AVERROR_EOF ) { throw new Error ( res + "" ) ; } let packets = allPackets [ in_stream . index ] ; if ( ! packets || ! packets . length ) { if ( res === libav . AVERROR_EOF ) break ; else continue ; } if ( out_oc < 0 ) { [ out_oc ] = await libav . ff_init_muxer ( { filename : outF , open : true , device : true } , [ [ c , fixedTimeBase . num , fixedTimeBase . den ] ] ) ; await libav . avformat_write_header ( out_oc , 0 ) ; } if ( net . remoteBeginTime && audio . timeOffset ) { const lastPacket = packets [ packets . length - 1 ] ; let inEndTime = timeFrom ( lastPacket . dtshi , lastPacket . dts ) ; if ( inEndTime < 0 ) inEndTime = timeFrom ( lastPacket . ptshi , lastPacket . pts ) ; const outEndTime = endTimeReal - video . videoLatency + audio . timeOffset - net . remoteBeginTime ; if ( dtsOffset === null ) { dtsOffset = outEndTime - inEndTime ; } else { let portion = Math . min ( globalFrameTime / 2000 , 1 ) ; dtsOffset = ( outEndTime - inEndTime ) * portion + dtsOffset * ( 1 - portion ) ; } for ( const packet of packets ) { let pdts = timeFrom ( packet . dtshi , packet . dts ) ; let ppts = timeFrom ( packet . ptshi , packet . pts ) ; if ( pdts < 0 ) pdts = ppts ; ppts -= pdts ; pdts += dtsOffset ; if ( pdts < lastDTS ) pdts = lastDTS ; ppts += pdts ; if ( ppts < 0 ) ppts = 0 ; pdts = timeTo ( pdts ) ; ppts = timeTo ( ppts ) ; packet . dtshi = pdts . hi ; packet . dts = pdts . lo ; packet . ptshi = ppts . hi ; packet . pts = ppts . lo ; lastDTS = pdts ; } if ( starterPackets ) { if ( starterPackets . length ) { packets = starterPackets . concat ( packets ) ; } else { const packet = packets [ 0 ] ; packet . dtshi = packet . dts = packet . ptshi = packet . pts = 0 ; } starterPackets = null ; } await libav . ff_write_multi ( out_oc , pkt , packets ) ; } else { for ( const packet of packets ) { packet . dtshi = packet . dts = packet . ptshi = packet . pts = 0 ; if ( packet . flags & 1 ) starterPackets = [ packet ] ; else starterPackets . push ( packet ) ; } } if ( res === libav . AVERROR_EOF ) break ; } if ( out_oc >= 0 ) await libav . av_write_trailer ( out_oc ) ; libavBuf . push ( null ) ; if ( libavNotify ) libavNotify ( ) ; if ( libav . worker ) libav . terminate ( ) ; } ) ( ) ; let localStream = null ; let remoteStream = null ; let promises ; if ( opts . local && opts . remote ) { [ localStream , remoteStream ] = < any [ ] > libavStream . tee ( ) ; promises = [ doLocal ( ) , doRemote ( ) ] ; } else if ( opts . remote ) { remoteStream = < any > libavStream ; promises = [ doRemote ( ) ] ; } else { localStream = < any > libavStream ; promises = [ doLocal ( ) ] ; } async function doLocal ( ) { await saveVideo ( filename , localStream , mimeType ) ; } async function doRemote ( ) { const rdr = remoteStream . getReader ( ) ; let idx = 0 ; while ( true ) { const chunk = await rdr . read ( ) ; if ( chunk . done ) { recordVideoRemoteClose ( opts . remotePeer ) ; break ; } else { recordVideoRemoteWrite ( opts . remotePeer , idx , chunk . value ) ; idx += chunk . value . length ; } } } await promises ; } ) ( ) ; recordVideoStop = async function ( ) { try { mrs . mediaRecorder . stop ( ) ; } catch ( ex ) { } await recordPromise ; } ; recordVideoUI ( ) ; } function getMediaRecorderStream ( ms , opts ) { const mediaRecorder = new MediaRecorder ( ms , opts ) ; const buf = [ ] ; let notify = null ; mediaRecorder . addEventListener ( "dataavailable" , ( ev ) => { buf . push ( ev . data ) ; if ( notify ) notify ( ) ; } ) ; mediaRecorder . addEventListener ( "stop" , ( ) => { buf . push ( null ) ; if ( notify ) notify ( ) ; } ) ; const stream = new wsp . ReadableStream ( { async pull ( controller ) { if ( ! buf . length ) { await new Promise < void > ( res => notify = res ) ; notify = null ; } const chunk = buf . shift ( ) ; if ( chunk ) controller . enqueue ( chunk ) ; else controller . close ( ) ; } } ) ; mediaRecorder . start ( 200 ) ; return { mediaRecorder , stream } ; } export function recordVideoRemoteIncoming ( peer , stream , opts ? ) { let ext = "webm" ; let mimeType = "video/webm" ; if ( opts . ext === "mkv" ) { ext = "mkv" ; mimeType = "video/x-matroska" ; } let filename = "" ; if ( net . recName ) filename = net . recName + "-" ; const remoteUser = ui . ui . panels . userList . users [ peer ] ; const remoteName = remoteUser ? remoteUser . name . innerText : "" ; if ( remoteName ) filename += remoteName + "-" ; filename += "video." + ext ; saveVideo ( filename , stream , mimeType ) ; } let lastStorageCt = - 1 ; function storageReport ( ct , used , max ) { const s = ( ct === 1 ) ? "" : "s" ; const msg = `Saving ${ ct } video stream${ s }` ; ui . ui . panels . master . videoStatus . innerHTML = msg + ". Storage used: " + Math . round ( used / max * 100 ) + "%" ; if ( ct !== lastStorageCt ) { lastStorageCt = ct ; if ( ct ) log . pushStatus ( "videoStorage" , msg ) ; else log . popStatus ( "videoStorage" ) ; } } async function saveVideo ( filename , stream , mimeType ) { let doDownloadStream = true , doLocalStorage = false ; let dStream = null , lsStream = null ; if ( ( "master" in config . config ) && ui . ui . panels . master . saveVideoInBrowser . checked ) { doLocalStorage = true ; if ( ! ui . ui . panels . master . downloadVideoLive . checked ) doDownloadStream = false ; } if ( doDownloadStream ) { if ( doLocalStorage ) [ dStream , lsStream ] = stream . tee ( ) ; else dStream = stream ; } else { lsStream = stream ; } let promises = [ ] ; if ( dStream ) { promises . push ( downloadStream . stream ( filename , dStream , { "content-type" : mimeType } ) ) ; } if ( lsStream ) { promises . push ( fileStorage . storeFile ( filename , [ config . config . id , config . config . key , config . config . master ] , lsStream , { mimeType , report : storageReport } ) ) ; } await Promise . all ( promises ) ; } function blobToArrayBuffer ( blob ) { if ( blob . arrayBuffer ) { return blob . arrayBuffer ( ) ; } else { return new Promise ( res => { const fr = new FileReader ( ) ; fr . onloadend = function ( ) { res ( < ArrayBuffer > fr . result ) ; } ; fr . readAsArrayBuffer ( blob ) ; } ) ; } } function recordVideoRemoteWrite ( peer , idx , buf ) { jitsi . videoDataSend ( peer , idx , buf ) ; } function recordVideoRemoteClose ( peer ) { jitsi . videoRecSend ( peer , prot . videoRec . endVideoRec ) ; } export function recordVideoUI ( loading ? ) { const recording = ui . ui . panels . videoConfig . recording ; recording . record . disabled = ! ! loading ; recording . optHider . style . display = ( loading || ! recording . record . checked ) ? "none" : "" ; recording . bitrateHider . style . display = recording . manualBitrate . checked ? "" : "none" ; } let reconsidering = false ; async function maybeRecord ( ) { const recording = ui . ui . panels . videoConfig . recording ; if ( reconsidering ) { return ; } reconsidering = true ; if ( recordVideoStop ) await recordVideoStop ( ) ; if ( supported && video . userMediaVideo && recording . record . checked ) { const opts = { remote : ! ( "master" in config . config ) && recording . remote . checked && config . useRTC , local : ( "master" in config . config ) || recording . local . checked || ! config . useRTC } ; if ( recording . manualBitrate . checked ) { const br = + recording . bitrate . value ; if ( br ) opts . bitrate = br ; } await recordVideo ( opts ) ; } else if ( ! supported && video . userMediaVideo && recording . record . checked && config . useVideoRec ) { log . pushStatus ( "video-rec-unsupported" , "WARNING: Your browser does not support video recording!" ) ; setTimeout ( ( ) => { log . popStatus ( "video-rec-unsupported" ) ; } , 10000 ) ; } reconsidering = false ; } export function loadVideoRecordPanel ( ) { const recording = ui . ui . panels . videoConfig . recording ; recording . hider . style . display = supported ? "" : "none" ; recording . record . checked = config . useVideoRec ; ui . saveConfigCheckbox ( recording . record , "record-video-" + config . useVideoRec , function ( ) { recordVideoUI ( ) ; maybeRecord ( ) ; } ) ; ui . saveConfigCheckbox ( recording . remote , "record-video-remote-" + config . useVideoRec , function ( ev ) { if ( ! recording . remote . checked && ! recording . local . checked ) { recording . local . checked = true ; recording . local . onchange ( ev ) ; return ; } maybeRecord ( ) ; } ) ; ui . saveConfigCheckbox ( recording . local , "record-video-local-" + config . useVideoRec , function ( ev ) { if ( ! recording . remote . checked && ! recording . local . checked ) { recording . remote . checked = true ; recording . remote . onchange ( ev ) ; return ; } maybeRecord ( ) ; } ) ; if ( ! recording . remote . checked && ! recording . local . checked ) { if ( config . useVideoRec ) recording . remote . checked = true ; else recording . local . checked = true ; } ui . saveConfigCheckbox ( recording . manualBitrate , "record-video-bitrate-sel" , function ( ) { recordVideoUI ( ) ; if ( recording . bitrate . value !== "" ) maybeRecord ( ) ; } ) ; ui . saveConfigValue ( recording . bitrate , "record-video-bitrate" , maybeRecord ) ; recordVideoUI ( ) ; } util . events . addEventListener ( "usermediavideoready" , maybeRecord ) ; util . events . addEventListener ( "usermediavideostopped" , maybeRecord ) ;