2018-04-26 15:33:47 +02:00
/ *
2022-01-13 11:18:47 +01:00
* ( C ) Copyright 2017 - 2022 OpenVidu ( https : //openvidu.io)
2018-04-26 15:33:47 +02:00
*
* 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 .
*
* /
2018-05-08 13:01:34 +02:00
import { Stream } from './Stream' ;
2018-04-27 11:08:03 +02:00
import { LocalRecorderState } from '../OpenViduInternal/Enums/LocalRecorderState' ;
2020-05-04 20:01:56 +02:00
import { OpenViduLogger } from '../OpenViduInternal/Logger/OpenViduLogger' ;
2020-10-13 16:13:37 +02:00
import { PlatformUtils } from '../OpenViduInternal/Utils/Platform' ;
2022-01-26 18:34:50 +01:00
import Mime = require ( 'mime/lite' ) ;
2018-12-04 09:55:00 +01:00
2020-05-04 20:01:56 +02:00
/ * *
* @hidden
* /
const logger : OpenViduLogger = OpenViduLogger . getInstance ( ) ;
2018-04-26 15:33:47 +02:00
2020-10-13 16:13:37 +02:00
/ * *
* @hidden
* /
2020-11-26 13:17:55 +01:00
let platform : PlatformUtils ;
2020-10-13 16:13:37 +02:00
2018-04-26 15:33:47 +02:00
/ * *
2018-07-23 00:36:45 +02:00
* Easy recording of [ [ Stream ] ] objects straightaway from the browser . Initialized with [ [ OpenVidu . initLocalRecorder ] ] method
2018-04-26 15:33:47 +02:00
* /
export class LocalRecorder {
2018-04-27 11:08:03 +02:00
state : LocalRecorderState ;
2018-04-26 15:33:47 +02:00
private connectionId : string ;
2022-01-26 17:23:16 +01:00
private mediaRecorder : MediaRecorder ;
2018-04-26 15:33:47 +02:00
private chunks : any [ ] = [ ] ;
2020-11-10 18:22:14 +01:00
private blob? : Blob ;
2018-04-26 15:33:47 +02:00
private id : string ;
private videoPreviewSrc : string ;
private videoPreview : HTMLVideoElement ;
/ * *
* @hidden
* /
constructor ( private stream : Stream ) {
2020-11-26 13:17:55 +01:00
platform = PlatformUtils . getInstance ( ) ;
2022-08-17 18:04:05 +02:00
this . connectionId = ! ! this . stream . connection ? this . stream . connection . connectionId : 'default-connection' ;
2018-04-26 15:33:47 +02:00
this . id = this . stream . streamId + '_' + this . connectionId + '_localrecord' ;
2018-04-27 11:08:03 +02:00
this . state = LocalRecorderState . READY ;
2018-04-26 15:33:47 +02:00
}
/ * *
* Starts the recording of the Stream . [ [ state ] ] property must be ` READY ` . After method succeeds is set to ` RECORDING `
2020-05-04 20:01:56 +02:00
*
2022-01-26 17:23:16 +01:00
* @param options The [ MediaRecorder . options ] ( https : //developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/MediaRecorder#parameters) to be used to record this Stream.
2022-08-17 18:04:05 +02:00
* For example :
*
2022-01-26 17:23:16 +01:00
* ` ` ` javascript
* var OV = new OpenVidu ( ) ;
* var publisher = await OV . initPublisherAsync ( ) ;
* var localRecorder = OV . initLocalRecorder ( publisher . stream ) ;
* var options = {
* mimeType : 'video/webm;codecs=vp8' ,
* audioBitsPerSecond :128000 ,
* videoBitsPerSecond :2500000
* } ;
* localRecorder . record ( options ) ;
* ` ` `
2022-08-17 18:04:05 +02:00
*
2022-01-26 17:23:16 +01:00
* If not specified , the default options preferred by the platform will be used .
2020-05-04 20:01:56 +02:00
*
2018-04-26 15:33:47 +02:00
* @returns A Promise ( to which you can optionally subscribe to ) that is resolved if the recording successfully started and rejected with an Error object if not
* /
2022-01-26 17:23:16 +01:00
record ( options? : any ) : Promise < void > {
2018-04-26 15:33:47 +02:00
return new Promise ( ( resolve , reject ) = > {
try {
2022-01-26 18:51:20 +01:00
if ( typeof options === 'string' || options instanceof String ) {
2022-08-17 18:04:05 +02:00
return reject (
` When calling LocalRecorder.record(options) parameter 'options' cannot be a string. Must be an object like { mimeType: " ${ options } " } `
) ;
2022-01-26 18:51:20 +01:00
}
2018-04-26 15:33:47 +02:00
if ( typeof MediaRecorder === 'undefined' ) {
2022-08-17 18:04:05 +02:00
logger . error (
'MediaRecorder not supported on your device. See compatibility in https://caniuse.com/#search=MediaRecorder'
) ;
throw Error (
'MediaRecorder not supported on your device. See compatibility in https://caniuse.com/#search=MediaRecorder'
) ;
2018-04-26 15:33:47 +02:00
}
2018-04-27 11:08:03 +02:00
if ( this . state !== LocalRecorderState . READY ) {
2022-08-17 18:04:05 +02:00
throw Error (
"'LocalRecord.record()' needs 'LocalRecord.state' to be 'READY' (current value: '" +
this . state +
"'). Call 'LocalRecorder.clean()' or init a new LocalRecorder before"
) ;
2018-04-26 15:33:47 +02:00
}
2020-05-04 20:01:56 +02:00
logger . log ( "Starting local recording of stream '" + this . stream . streamId + "' of connection '" + this . connectionId + "'" ) ;
2018-04-26 15:33:47 +02:00
2022-01-26 17:23:16 +01:00
if ( ! options ) {
options = { mimeType : 'video/webm' } ;
} else if ( ! options . mimeType ) {
options . mimeType = 'video/webm' ;
2018-04-26 15:33:47 +02:00
}
2020-01-24 18:45:32 +01:00
this . mediaRecorder = new MediaRecorder ( this . stream . getMediaStream ( ) , options ) ;
2022-01-26 17:23:16 +01:00
this . mediaRecorder . start ( ) ;
2018-04-26 15:33:47 +02:00
} catch ( err ) {
2022-01-26 12:17:31 +01:00
return reject ( err ) ;
2018-04-26 15:33:47 +02:00
}
this . mediaRecorder . ondataavailable = ( e ) = > {
2022-01-26 17:23:16 +01:00
if ( e . data . size > 0 ) {
this . chunks . push ( e . data ) ;
}
2018-04-26 15:33:47 +02:00
} ;
this . mediaRecorder . onerror = ( e ) = > {
2020-05-04 20:01:56 +02:00
logger . error ( 'MediaRecorder error: ' , e ) ;
2018-04-26 15:33:47 +02:00
} ;
this . mediaRecorder . onstart = ( ) = > {
2020-05-04 20:01:56 +02:00
logger . log ( 'MediaRecorder started (state=' + this . mediaRecorder . state + ')' ) ;
2018-04-26 15:33:47 +02:00
} ;
this . mediaRecorder . onstop = ( ) = > {
this . onStopDefault ( ) ;
} ;
this . mediaRecorder . onpause = ( ) = > {
2020-05-04 20:01:56 +02:00
logger . log ( 'MediaRecorder paused (state=' + this . mediaRecorder . state + ')' ) ;
2018-04-26 15:33:47 +02:00
} ;
this . mediaRecorder . onresume = ( ) = > {
2020-05-04 20:01:56 +02:00
logger . log ( 'MediaRecorder resumed (state=' + this . mediaRecorder . state + ')' ) ;
2018-04-26 15:33:47 +02:00
} ;
2018-04-27 11:08:03 +02:00
this . state = LocalRecorderState . RECORDING ;
2022-01-26 12:17:31 +01:00
return resolve ( ) ;
2018-04-26 15:33:47 +02:00
} ) ;
}
/ * *
* Ends the recording of the Stream . [ [ state ] ] property must be ` RECORDING ` or ` PAUSED ` . After method succeeds is set to ` FINISHED `
* @returns A Promise ( to which you can optionally subscribe to ) that is resolved if the recording successfully stopped and rejected with an Error object if not
* /
2021-03-09 15:07:45 +01:00
stop ( ) : Promise < void > {
2018-04-26 15:33:47 +02:00
return new Promise ( ( resolve , reject ) = > {
try {
2018-04-27 11:08:03 +02:00
if ( this . state === LocalRecorderState . READY || this . state === LocalRecorderState . FINISHED ) {
2022-08-17 18:04:05 +02:00
throw Error (
"'LocalRecord.stop()' needs 'LocalRecord.state' to be 'RECORDING' or 'PAUSED' (current value: '" +
this . state +
"'). Call 'LocalRecorder.start()' before"
) ;
2018-04-26 15:33:47 +02:00
}
this . mediaRecorder . onstop = ( ) = > {
this . onStopDefault ( ) ;
2022-01-26 12:17:31 +01:00
return resolve ( ) ;
2018-04-26 15:33:47 +02:00
} ;
this . mediaRecorder . stop ( ) ;
} catch ( e ) {
2022-01-26 12:17:31 +01:00
return reject ( e ) ;
2018-04-26 15:33:47 +02:00
}
} ) ;
}
/ * *
* Pauses the recording of the Stream . [ [ state ] ] property must be ` RECORDING ` . After method succeeds is set to ` PAUSED `
* @returns A Promise ( to which you can optionally subscribe to ) that is resolved if the recording was successfully paused and rejected with an Error object if not
* /
2021-03-09 15:07:45 +01:00
pause ( ) : Promise < void > {
2018-04-26 15:33:47 +02:00
return new Promise ( ( resolve , reject ) = > {
try {
2018-04-27 11:08:03 +02:00
if ( this . state !== LocalRecorderState . RECORDING ) {
2022-08-17 18:04:05 +02:00
return reject (
Error (
"'LocalRecord.pause()' needs 'LocalRecord.state' to be 'RECORDING' (current value: '" +
this . state +
"'). Call 'LocalRecorder.start()' or 'LocalRecorder.resume()' before"
)
) ;
2018-04-26 15:33:47 +02:00
}
this . mediaRecorder . pause ( ) ;
2018-04-27 11:08:03 +02:00
this . state = LocalRecorderState . PAUSED ;
2022-01-26 12:17:31 +01:00
return resolve ( ) ;
2018-04-26 15:33:47 +02:00
} catch ( error ) {
2022-01-26 12:17:31 +01:00
return reject ( error ) ;
2018-04-26 15:33:47 +02:00
}
} ) ;
}
/ * *
* Resumes the recording of the Stream . [ [ state ] ] property must be ` PAUSED ` . After method succeeds is set to ` RECORDING `
* @returns A Promise ( to which you can optionally subscribe to ) that is resolved if the recording was successfully resumed and rejected with an Error object if not
* /
2021-03-09 15:07:45 +01:00
resume ( ) : Promise < void > {
2018-04-26 15:33:47 +02:00
return new Promise ( ( resolve , reject ) = > {
try {
2018-04-27 11:08:03 +02:00
if ( this . state !== LocalRecorderState . PAUSED ) {
2022-08-17 18:04:05 +02:00
throw Error (
"'LocalRecord.resume()' needs 'LocalRecord.state' to be 'PAUSED' (current value: '" +
this . state +
"'). Call 'LocalRecorder.pause()' before"
) ;
2018-04-26 15:33:47 +02:00
}
this . mediaRecorder . resume ( ) ;
2018-04-27 11:08:03 +02:00
this . state = LocalRecorderState . RECORDING ;
2022-01-26 12:17:31 +01:00
return resolve ( ) ;
2018-04-26 15:33:47 +02:00
} catch ( error ) {
2022-01-26 12:17:31 +01:00
return reject ( error ) ;
2018-04-26 15:33:47 +02:00
}
} ) ;
}
/ * *
* Previews the recording , appending a new HTMLVideoElement to element with id ` parentId ` . [ [ state ] ] property must be ` FINISHED `
* /
preview ( parentElement ) : HTMLVideoElement {
2018-04-27 11:08:03 +02:00
if ( this . state !== LocalRecorderState . FINISHED ) {
2022-08-17 18:04:05 +02:00
throw Error (
"'LocalRecord.preview()' needs 'LocalRecord.state' to be 'FINISHED' (current value: '" +
this . state +
"'). Call 'LocalRecorder.stop()' before"
) ;
2018-04-26 15:33:47 +02:00
}
this . videoPreview = document . createElement ( 'video' ) ;
this . videoPreview . id = this . id ;
this . videoPreview . autoplay = true ;
2020-10-13 16:13:37 +02:00
if ( platform . isSafariBrowser ( ) ) {
2022-06-23 13:28:00 +02:00
this . videoPreview . playsInline = true ;
2018-11-21 12:03:14 +01:00
}
2018-04-26 15:33:47 +02:00
if ( typeof parentElement === 'string' ) {
const parentElementDom = document . getElementById ( parentElement ) ;
if ( parentElementDom ) {
this . videoPreview = parentElementDom . appendChild ( this . videoPreview ) ;
}
} else {
this . videoPreview = parentElement . appendChild ( this . videoPreview ) ;
}
this . videoPreview . src = this . videoPreviewSrc ;
return this . videoPreview ;
}
/ * *
* Gracefully stops and cleans the current recording ( WARNING : it is completely dismissed ) . Sets [ [ state ] ] to ` READY ` so the recording can start again
* /
clean ( ) : void {
const f = ( ) = > {
delete this . blob ;
this . chunks = [ ] ;
2018-04-27 11:08:03 +02:00
this . state = LocalRecorderState . READY ;
2018-04-26 15:33:47 +02:00
} ;
2018-04-27 11:08:03 +02:00
if ( this . state === LocalRecorderState . RECORDING || this . state === LocalRecorderState . PAUSED ) {
2022-08-17 18:04:05 +02:00
this . stop ( )
. then ( ( ) = > f ( ) )
. catch ( ( ) = > f ( ) ) ;
2018-04-26 15:33:47 +02:00
} else {
f ( ) ;
}
}
/ * *
* Downloads the recorded video through the browser . [ [ state ] ] property must be ` FINISHED `
* /
download ( ) : void {
2018-04-27 11:08:03 +02:00
if ( this . state !== LocalRecorderState . FINISHED ) {
2022-08-17 18:04:05 +02:00
throw Error (
"'LocalRecord.download()' needs 'LocalRecord.state' to be 'FINISHED' (current value: '" +
this . state +
"'). Call 'LocalRecorder.stop()' before"
) ;
2018-04-26 15:33:47 +02:00
} else {
const a : HTMLAnchorElement = document . createElement ( 'a' ) ;
a . style . display = 'none' ;
document . body . appendChild ( a ) ;
2022-07-27 16:36:08 +02:00
const url = globalThis . URL . createObjectURL ( < any > this . blob ) ;
2018-04-26 15:33:47 +02:00
a . href = url ;
2022-01-26 18:34:50 +01:00
a . download = this . id + '.' + Mime . getExtension ( this . blob ! . type ) ;
2018-04-26 15:33:47 +02:00
a . click ( ) ;
2022-07-27 16:36:08 +02:00
globalThis . URL . revokeObjectURL ( url ) ;
2018-04-26 15:33:47 +02:00
document . body . removeChild ( a ) ;
}
}
/ * *
* Gets the raw Blob file . Methods preview , download , uploadAsBinary and uploadAsMultipartfile use this same file to perform their specific actions . [ [ state ] ] property must be ` FINISHED `
* /
getBlob ( ) : Blob {
2018-04-27 11:08:03 +02:00
if ( this . state !== LocalRecorderState . FINISHED ) {
2022-08-17 18:04:05 +02:00
throw Error ( "Call 'LocalRecord.stop()' before getting Blob file" ) ;
2018-04-26 15:33:47 +02:00
} else {
2020-11-10 18:22:14 +01:00
return this . blob ! ;
2018-04-26 15:33:47 +02:00
}
}
/ * *
* Uploads the recorded video as a binary file performing an HTTP / POST operation to URL ` endpoint ` . [ [ state ] ] property must be ` FINISHED ` . Optional HTTP headers can be passed as second parameter . For example :
* ` ` `
* var headers = {
* "Cookie" : "$Version=1; Skin=new;" ,
* "Authorization" : "Basic QWxhZGpbjpuIHNlctZQ=="
* }
* ` ` `
* @returns A Promise ( to which you can optionally subscribe to ) that is resolved with the ` http.responseText ` from server if the operation was successful and rejected with the failed ` http.status ` if not
* /
uploadAsBinary ( endpoint : string , headers? : any ) : Promise < any > {
return new Promise ( ( resolve , reject ) = > {
2018-04-27 11:08:03 +02:00
if ( this . state !== LocalRecorderState . FINISHED ) {
2022-08-17 18:04:05 +02:00
return reject (
Error (
"'LocalRecord.uploadAsBinary()' needs 'LocalRecord.state' to be 'FINISHED' (current value: '" +
this . state +
"'). Call 'LocalRecorder.stop()' before"
)
) ;
2018-04-26 15:33:47 +02:00
} else {
const http = new XMLHttpRequest ( ) ;
http . open ( 'POST' , endpoint , true ) ;
if ( typeof headers === 'object' ) {
for ( const key of Object . keys ( headers ) ) {
http . setRequestHeader ( key , headers [ key ] ) ;
}
}
http . onreadystatechange = ( ) = > {
if ( http . readyState === 4 ) {
if ( http . status . toString ( ) . charAt ( 0 ) === '2' ) {
// Success response from server (HTTP status standard: 2XX is success)
2022-01-26 12:17:31 +01:00
return resolve ( http . responseText ) ;
2018-04-26 15:33:47 +02:00
} else {
2022-01-26 12:17:31 +01:00
return reject ( http . status ) ;
2018-04-26 15:33:47 +02:00
}
}
} ;
http . send ( this . blob ) ;
}
} ) ;
}
/ * *
* Uploads the recorded video as a multipart file performing an HTTP / POST operation to URL ` endpoint ` . [ [ state ] ] property must be ` FINISHED ` . Optional HTTP headers can be passed as second parameter . For example :
* ` ` `
* var headers = {
* "Cookie" : "$Version=1; Skin=new;" ,
* "Authorization" : "Basic QWxhZGpbjpuIHNlctZQ=="
* }
* ` ` `
* @returns A Promise ( to which you can optionally subscribe to ) that is resolved with the ` http.responseText ` from server if the operation was successful and rejected with the failed ` http.status ` if not :
* /
uploadAsMultipartfile ( endpoint : string , headers? : any ) : Promise < any > {
return new Promise ( ( resolve , reject ) = > {
2018-04-27 11:08:03 +02:00
if ( this . state !== LocalRecorderState . FINISHED ) {
2022-08-17 18:04:05 +02:00
return reject (
Error (
"'LocalRecord.uploadAsMultipartfile()' needs 'LocalRecord.state' to be 'FINISHED' (current value: '" +
this . state +
"'). Call 'LocalRecorder.stop()' before"
)
) ;
2018-04-26 15:33:47 +02:00
} else {
const http = new XMLHttpRequest ( ) ;
http . open ( 'POST' , endpoint , true ) ;
if ( typeof headers === 'object' ) {
for ( const key of Object . keys ( headers ) ) {
http . setRequestHeader ( key , headers [ key ] ) ;
}
}
const sendable = new FormData ( ) ;
2022-01-26 18:34:50 +01:00
sendable . append ( 'file' , this . blob ! , this . id + '.' + Mime . getExtension ( this . blob ! . type ) ) ;
2018-04-26 15:33:47 +02:00
http . onreadystatechange = ( ) = > {
if ( http . readyState === 4 ) {
if ( http . status . toString ( ) . charAt ( 0 ) === '2' ) {
// Success response from server (HTTP status standard: 2XX is success)
2022-01-26 12:17:31 +01:00
return resolve ( http . responseText ) ;
2018-04-26 15:33:47 +02:00
} else {
2022-01-26 12:17:31 +01:00
return reject ( http . status ) ;
2018-04-26 15:33:47 +02:00
}
}
} ;
http . send ( sendable ) ;
}
} ) ;
}
/* Private methods */
private onStopDefault ( ) : void {
2020-05-04 20:01:56 +02:00
logger . log ( 'MediaRecorder stopped (state=' + this . mediaRecorder . state + ')' ) ;
2018-04-26 15:33:47 +02:00
2022-01-26 17:23:16 +01:00
this . blob = new Blob ( this . chunks , { type : this . mediaRecorder . mimeType } ) ;
2018-04-26 15:33:47 +02:00
this . chunks = [ ] ;
2022-07-27 16:36:08 +02:00
this . videoPreviewSrc = globalThis . URL . createObjectURL ( this . blob ) ;
2018-04-26 15:33:47 +02:00
2018-04-27 11:08:03 +02:00
this . state = LocalRecorderState . FINISHED ;
2018-04-26 15:33:47 +02:00
}
}