diff --git a/openvidu-browser/package.json b/openvidu-browser/package.json index 406c8a90..4ab4d4cb 100644 --- a/openvidu-browser/package.json +++ b/openvidu-browser/package.json @@ -1,37 +1,37 @@ { - "author": "OpenVidu", - "dependencies": { - "freeice": "2.2.0", - "hark": "1.1.6", - "inherits": "2.0.3", - "merge": "1.2.0", - "sdp-translator": "0.1.24", - "ua-parser-js": "0.7.17", - "uuid": "3.1.0", - "webrtc-adapter": "6.1.1", - "wolfy87-eventemitter": "5.2.4" - }, - "description": "OpenVidu Browser", - "devDependencies": { - "browserify": "15.1.0", - "tsify": "3.0.4", - "typescript": "2.6.2", - "uglify-js": "3.3.5" - }, - "license": "Apache-2.0", - "main": "lib/OpenVidu/index.js", - "name": "openvidu-browser", - "repository": { - "type": "git", - "url": "git://github.com/OpenVidu/openvidu" - }, - "scripts": { - "browserify": "VERSION=${VERSION:-}; cd ts/OpenVidu && browserify Main.ts -p [ tsify ] --exclude kurento-browser-extensions --debug -o ../../static/js/openvidu-browser-$VERSION.js -v", - "browserify-prod": "VERSION=${VERSION:-}; cd ts/OpenVidu && browserify --debug Main.ts -p [ tsify ] --exclude kurento-browser-extensions | uglifyjs --source-map content=inline --output ../../static/js/openvidu-browser-$VERSION.min.js", - "prepublish": "cd ts/OpenViduInternal && tsc && cd ../OpenVidu && tsc && cd ../.. && tsc --declaration ts/OpenVidu/index.ts --outDir lib --sourceMap && tsc --declaration ts/OpenVidu/Main.ts --outDir lib --sourceMap", - "test": "echo \"Error: no test specified\" && exit 1", - "updatetsc": "cd ts/OpenViduInternal && tsc && cd ../OpenVidu && tsc && cd ../.. && tsc --declaration ts/OpenVidu/index.ts --outDir lib --sourceMap && tsc --declaration ts/OpenVidu/Main.ts --outDir lib --sourceMap" - }, - "types": "lib/OpenVidu/index.d.ts", - "version": "1.7.0" + "author": "OpenVidu", + "dependencies": { + "freeice": "2.2.0", + "hark": "1.1.6", + "inherits": "2.0.3", + "merge": "1.2.0", + "sdp-translator": "0.1.24", + "ua-parser-js": "0.7.17", + "uuid": "3.1.0", + "webrtc-adapter": "6.1.1", + "wolfy87-eventemitter": "5.2.4" + }, + "description": "OpenVidu Browser", + "devDependencies": { + "browserify": "15.1.0", + "tsify": "3.0.4", + "typescript": "2.6.2", + "uglify-js": "3.3.5" + }, + "license": "Apache-2.0", + "main": "lib/OpenVidu/index.js", + "name": "openvidu-browser", + "repository": { + "type": "git", + "url": "git://github.com/OpenVidu/openvidu" + }, + "scripts": { + "browserify": "VERSION=${VERSION:-}; cd ts/OpenVidu && browserify Main.ts -p [ tsify ] --exclude kurento-browser-extensions --debug -o ../../static/js/openvidu-browser-$VERSION.js -v", + "browserify-prod": "VERSION=${VERSION:-}; cd ts/OpenVidu && browserify --debug Main.ts -p [ tsify ] --exclude kurento-browser-extensions | uglifyjs --source-map content=inline --output ../../static/js/openvidu-browser-$VERSION.min.js", + "prepublish": "cd ts/OpenViduInternal && tsc && cd ../OpenVidu && tsc && cd ../.. && tsc --declaration ts/OpenVidu/index.ts --outDir lib --sourceMap && tsc --declaration ts/OpenVidu/Main.ts --outDir lib --sourceMap", + "test": "echo \"Error: no test specified\" && exit 1", + "updatetsc": "cd ts/OpenViduInternal && tsc && cd ../OpenVidu && tsc && cd ../.. && tsc --declaration ts/OpenVidu/index.ts --outDir lib --sourceMap --lib dom,es5,es2015.promise,scripthost && tsc --declaration ts/OpenVidu/Main.ts --outDir lib --sourceMap --lib dom,es5,es2015.promise,scripthost" + }, + "types": "lib/OpenVidu/index.d.ts", + "version": "1.7.0" } \ No newline at end of file diff --git a/openvidu-browser/ts/OpenVidu/OpenVidu.ts b/openvidu-browser/ts/OpenVidu/OpenVidu.ts index 54c188f5..b8cb72fa 100644 --- a/openvidu-browser/ts/OpenVidu/OpenVidu.ts +++ b/openvidu-browser/ts/OpenVidu/OpenVidu.ts @@ -20,6 +20,8 @@ import { Session } from './Session'; import { Publisher } from './Publisher'; import { OpenViduError, OpenViduErrorName } from '../OpenViduInternal/OpenViduError'; import { OutboundStreamOptions } from '../OpenViduInternal/index'; +import { Stream } from '../OpenViduInternal/Stream'; +import { LocalRecorder } from '../OpenViduInternal/LocalRecorder'; import * as adapter from 'webrtc-adapter'; import * as screenSharing from '../ScreenSharing/Screen-Capturing.js'; @@ -269,4 +271,8 @@ export class OpenVidu { console.warn = function () { }; } + initLocalRecorder(stream: Stream): LocalRecorder { + return new LocalRecorder(stream); + } + } diff --git a/openvidu-browser/ts/OpenVidu/index.ts b/openvidu-browser/ts/OpenVidu/index.ts index 7aea0f50..1e3682cb 100644 --- a/openvidu-browser/ts/OpenVidu/index.ts +++ b/openvidu-browser/ts/OpenVidu/index.ts @@ -4,3 +4,4 @@ export * from './Publisher'; export * from './Subscriber'; export * from '../OpenViduInternal/Stream'; export * from '../OpenViduInternal/Connection'; +export * from '../OpenViduInternal/LocalRecorder'; \ No newline at end of file diff --git a/openvidu-browser/ts/OpenVidu/tsconfig.json b/openvidu-browser/ts/OpenVidu/tsconfig.json index adeb71b8..7a648cf4 100644 --- a/openvidu-browser/ts/OpenVidu/tsconfig.json +++ b/openvidu-browser/ts/OpenVidu/tsconfig.json @@ -21,7 +21,8 @@ "outDir": "../../lib", "emitBOM": false, "preserveConstEnums": true, - "sourceMap": true + "sourceMap": true, + "lib": ["dom","es5","es2015.promise","scripthost"] }, //"buildOnSave": true, "compileOnSave": true diff --git a/openvidu-browser/ts/OpenViduInternal/LocalRecorder.ts b/openvidu-browser/ts/OpenViduInternal/LocalRecorder.ts new file mode 100644 index 00000000..2d0d0f22 --- /dev/null +++ b/openvidu-browser/ts/OpenViduInternal/LocalRecorder.ts @@ -0,0 +1,258 @@ +import { Stream } from "./Stream"; + +declare var MediaRecorder: any; + +export const enum LocalRecoderState { + READY = 'READY', + RECORDING = 'RECORDING', + PAUSED = 'PAUSED', + FINISHED = 'FINISHED' +} + +export class LocalRecorder { + + state: LocalRecoderState; + + private stream: Stream; + + private mediaRecorder: any; + private chunks: any[] = []; + private blob: Blob; + private count: number = 0; + private id: string; + + private videoPreviewSrc: string; + private htmlParentElementId: string; + private videoPreview: HTMLVideoElement; + + constructor(stream: Stream) { + this.stream = stream; + this.id = this.stream.streamId + '_' + this.stream.connection.connectionId + '_localrecord'; + this.state = LocalRecoderState.READY; + } + + record() { + if (typeof MediaRecorder === 'undefined') { + console.error('MediaRecorder not supported on your browser. See compatibility in https://caniuse.com/#search=MediaRecorder'); + throw (Error('MediaRecorder not supported on your browser. See compatibility in https://caniuse.com/#search=MediaRecorder')); + } + if (this.state !== LocalRecoderState.READY) { + throw (Error('\'LocalRecord.record()\' needs \'LocalRecord.state\' to be \'READY\' (current value: \'' + this.state + '\'). Call \'LocalRecorder.clean()\' or init a new LocalRecorder before')); + } + console.log("Starting local recording of stream '" + this.stream.streamId + "' of connection '" + this.stream.connection.connectionId + "'"); + if (typeof MediaRecorder.isTypeSupported == 'function') { + let options; + if (MediaRecorder.isTypeSupported('video/webm;codecs=vp9')) { + options = { mimeType: 'video/webm;codecs=vp9' }; + } else if (MediaRecorder.isTypeSupported('video/webm;codecs=h264')) { + options = { mimeType: 'video/webm;codecs=h264' }; + } else if (MediaRecorder.isTypeSupported('video/webm;codecs=vp8')) { + options = { mimeType: 'video/webm;codecs=vp8' }; + } + console.log('Using mimeType ' + options.mimeType); + this.mediaRecorder = new MediaRecorder(this.stream.getMediaStream(), options); + } else { + console.warn('isTypeSupported is not supported, using default codecs for browser'); + this.mediaRecorder = new MediaRecorder(this.stream.getMediaStream()); + } + + this.mediaRecorder.start(10); + + this.mediaRecorder.ondataavailable = (e) => { + this.chunks.push(e.data); + }; + + this.mediaRecorder.onerror = (e) => { + console.error('MediaRecorder error: ', e); + }; + + + this.mediaRecorder.onstart = () => { + console.log('MediaRecorder started (state=' + this.mediaRecorder.state + ")"); + }; + + this.mediaRecorder.onstop = () => { + this.onStopDefault(); + }; + + this.mediaRecorder.onpause = () => { + console.log('MediaRecorder paused (state=' + this.mediaRecorder.state + ")"); + } + + this.mediaRecorder.onresume = () => { + console.log('MediaRecorder resumed (state=' + this.mediaRecorder.state + ")"); + } + + this.mediaRecorder.onwarning = (e) => { + console.log('MediaRecorder warning: ' + e); + }; + + this.state = LocalRecoderState.RECORDING; + } + + stop(): Promise { + return new Promise((resolve, reject) => { + try { + if (this.state === LocalRecoderState.READY || this.state === LocalRecoderState.FINISHED) { + throw (Error('\'LocalRecord.stop()\' needs \'LocalRecord.state\' to be \'RECORDING\' or \'PAUSED\' (current value: \'' + this.state + '\'). Call \'LocalRecorder.start()\' before')); + } + this.mediaRecorder.onstop = () => { + this.onStopDefault(); + resolve(); + } + } catch (e) { + reject(e); + } + try { + this.mediaRecorder.stop(); + } catch (e) { + reject(e); + } + }); + } + + pause() { + if (this.state !== LocalRecoderState.RECORDING) { + throw (Error('\'LocalRecord.pause()\' needs \'LocalRecord.state\' to be \'RECORDING\' (current value: \'' + this.state + '\'). Call \'LocalRecorder.start()\' or \'LocalRecorder.resume()\' before')); + } + this.mediaRecorder.pause(); + this.state = LocalRecoderState.PAUSED; + } + + resume() { + if (this.state !== LocalRecoderState.PAUSED) { + throw (Error('\'LocalRecord.resume()\' needs \'LocalRecord.state\' to be \'PAUSED\' (current value: \'' + this.state + '\'). Call \'LocalRecorder.pause()\' before')); + } + this.mediaRecorder.resume(); + this.state = LocalRecoderState.RECORDING; + } + + preview(parentElement): HTMLVideoElement { + + if (this.state !== LocalRecoderState.FINISHED) { + throw (Error('\'LocalRecord.preview()\' needs \'LocalRecord.state\' to be \'FINISHED\' (current value: \'' + this.state + '\'). Call \'LocalRecorder.stop()\' before')); + } + + this.videoPreview = document.createElement('video'); + + this.videoPreview.id = this.id; + this.videoPreview.autoplay = true; + + if (typeof parentElement === "string") { + this.htmlParentElementId = parentElement; + + let parentElementDom = document.getElementById(parentElement); + if (parentElementDom) { + this.videoPreview = parentElementDom.appendChild(this.videoPreview); + } + } else { + this.htmlParentElementId = parentElement.id; + this.videoPreview = parentElement.appendChild(this.videoPreview); + } + + this.videoPreview.src = this.videoPreviewSrc; + + return this.videoPreview; + } + + clean() { + let f = () => { + delete this.blob; + this.chunks = []; + this.count = 0; + delete this.mediaRecorder; + this.state = LocalRecoderState.READY; + } + if (this.state === LocalRecoderState.RECORDING || this.state === LocalRecoderState.PAUSED) { + this.stop().then(() => f()).catch(() => f()); + } else { + f(); + } + } + + download() { + if (this.state !== LocalRecoderState.FINISHED) { + throw (Error('\'LocalRecord.download()\' needs \'LocalRecord.state\' to be \'FINISHED\' (current value: \'' + this.state + '\'). Call \'LocalRecorder.stop()\' before')); + } else { + let a: HTMLAnchorElement = document.createElement("a"); + a.style.display = 'none'; + document.body.appendChild(a); + + let url = window.URL.createObjectURL(this.blob); + a.href = url; + a.download = this.id + '.webm'; + a.click(); + window.URL.revokeObjectURL(url); + + document.body.removeChild(a); + } + } + + getBlob(): Blob { + if (this.state !== LocalRecoderState.FINISHED) { + throw (Error('Call \'LocalRecord.stop()\' before getting Blob file')); + } else { + return this.blob; + } + } + + uploadAsBinary(endpoint: string): Promise { + return new Promise((resolve, reject) => { + if (this.state !== LocalRecoderState.FINISHED) { + reject(Error('\'LocalRecord.uploadAsBinary()\' needs \'LocalRecord.state\' to be \'FINISHED\' (current value: \'' + this.state + '\'). Call \'LocalRecorder.stop()\' before')); + } else { + let http = new XMLHttpRequest(); + http.open("POST", endpoint, true); + http.onreadystatechange = () => { + if (http.readyState === 4) { + if (http.status === 200) { + resolve("File uploaded"); + } else { + reject(Error("Upload error: " + http.status)); + } + } + } + http.send(this.blob); + } + }); + } + + uploadAsMultipartfile(endpoint: string): Promise { + return new Promise((resolve, reject) => { + if (this.state !== LocalRecoderState.FINISHED) { + reject(Error('\'LocalRecord.uploadAsMultipartfile()\' needs \'LocalRecord.state\' to be \'FINISHED\' (current value: \'' + this.state + '\'). Call \'LocalRecorder.stop()\' before')); + } else { + let http = new XMLHttpRequest(); + http.open("POST", endpoint, true); + + let sendable = new FormData(); + sendable.append("name", this.id); + sendable.append("file", this.blob); + + http.onreadystatechange = () => { + if (http.readyState === 4) { + if (http.status === 200) { + resolve("File uploaded"); + } else { + reject(Error("Upload error: " + http.status)); + } + } + } + + http.send(sendable); + } + }); + } + + private onStopDefault() { + console.log('MediaRecorder stopped (state=' + this.mediaRecorder.state + ")"); + + this.blob = new Blob(this.chunks, { type: "video/webm" }); + this.chunks = []; + + this.videoPreviewSrc = window.URL.createObjectURL(this.blob); + + this.state = LocalRecoderState.FINISHED; + } + +} diff --git a/openvidu-browser/ts/OpenViduInternal/Stream.ts b/openvidu-browser/ts/OpenViduInternal/Stream.ts index b4c86219..8ef8a1b5 100644 --- a/openvidu-browser/ts/OpenViduInternal/Stream.ts +++ b/openvidu-browser/ts/OpenViduInternal/Stream.ts @@ -693,7 +693,7 @@ export class Stream { } private stopWebRtcStats() { - if (this.webRtcStats.isEnabled()) { + if (this.webRtcStats != null && this.webRtcStats.isEnabled()) { this.webRtcStats.stopWebRtcStats(); } } diff --git a/openvidu-browser/ts/OpenViduInternal/WebRtcStats.ts b/openvidu-browser/ts/OpenViduInternal/WebRtcStats.ts index e3744241..22f45fad 100644 --- a/openvidu-browser/ts/OpenViduInternal/WebRtcStats.ts +++ b/openvidu-browser/ts/OpenViduInternal/WebRtcStats.ts @@ -82,7 +82,7 @@ export class WebRtcStats { http.open("POST", url, true); http.setRequestHeader("Content-type", "application/json"); - + http.onreadystatechange = () => { // Call a function when the state changes. if (http.readyState == 4 && http.status == 200) { console.log("WebRtc stats succesfully sent to " + url + " for stream " + this.stream.streamId + " of connection " + this.stream.connection.connectionId); diff --git a/openvidu-browser/ts/OpenViduInternal/index.ts b/openvidu-browser/ts/OpenViduInternal/index.ts index 1ef3a87f..1f028f2d 100644 --- a/openvidu-browser/ts/OpenViduInternal/index.ts +++ b/openvidu-browser/ts/OpenViduInternal/index.ts @@ -2,4 +2,5 @@ export * from './OpenViduInternal'; export * from './Connection'; export * from './SessionInternal'; export * from './Stream'; +export * from './LocalRecorder'; export * from './OpenViduError'; diff --git a/openvidu-browser/ts/OpenViduInternal/tsconfig.json b/openvidu-browser/ts/OpenViduInternal/tsconfig.json index 272c1d31..7fa55219 100644 --- a/openvidu-browser/ts/OpenViduInternal/tsconfig.json +++ b/openvidu-browser/ts/OpenViduInternal/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "allowJs": true, - "target": "es6", + "target": "es5", "module": "commonjs", //"noImplicitAny": true, "noImplicitThis": true, @@ -21,8 +21,9 @@ "outDir": "../../lib/OpenViduInternal", "emitBOM": false, "preserveConstEnums": true, - "sourceMap": true + "sourceMap": true, + "lib": ["dom","es5","es2015.promise","scripthost"] }, //"buildOnSave": true, - "compileOnSave":true + "compileOnSave": true } \ No newline at end of file