diff --git a/README.md b/README.md index 7acbbdc4..9499ff51 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ OpenVidu is composed by several modules which require some interconnections in o Here's a simple summary about the structure of OpenVidu: -
![OpenVidu structure](https://drive.google.com/uc?export=view&id=0B61cQ4sbhmWSQ1AwaXlnRTR4djA)
+![OpenVidu structure](https://drive.google.com/uc?export=view&id=0B61cQ4sbhmWSQ1AwaXlnRTR4djA) - **Kurento Media Server**: External module which provides the low-level functionalities related to the media transmission. diff --git a/openvidu-browser/README.md b/openvidu-browser/README.md index b322aa8b..dab8aae7 100644 --- a/openvidu-browser/README.md +++ b/openvidu-browser/README.md @@ -15,7 +15,7 @@ It uses WebSockets and JSON-RPC to interact with the server-side of the Room API Typescript is currently used to develop openvidu-browser. The class diagram is shown below: -
![OpenVidu structure](https://drive.google.com/uc?export=view&id=0B61cQ4sbhmWSM1N3SmE5amt3TzA)
+![OpenVidu structure](https://drive.google.com/uc?export=view&id=0B61cQ4sbhmWSM1N3SmE5amt3TzA) What is Kurento --------------- diff --git a/openvidu-browser/src/main/resources/ts/Stream.ts b/openvidu-browser/src/main/resources/ts/Stream.ts index a05978d2..6edd2caa 100644 --- a/openvidu-browser/src/main/resources/ts/Stream.ts +++ b/openvidu-browser/src/main/resources/ts/Stream.ts @@ -15,15 +15,15 @@ import 'webrtc-adapter'; declare var navigator: any; declare var RTCSessionDescription: any; -function jq(id: string):string { +function jq(id: string): string { return id.replace(/(@|:|\.|\[|\]|,)/g, "\\$1"); } -function show(id: string){ +function show(id: string) { document.getElementById(jq(id))!.style.display = 'block'; } -function hide(id: string){ +function hide(id: string) { document.getElementById(jq(id))!.style.display = 'none'; } @@ -65,9 +65,9 @@ export class Stream { private dataChannel: boolean; private dataChannelOpened = false; - constructor( private openVidu: OpenVidu, private local: boolean, private room: Session, options: StreamOptions ) { + constructor(private openVidu: OpenVidu, private local: boolean, private room: Session, options: StreamOptions) { - if ( options.id ) { + if (options.id) { this.id = options.id; } else { this.id = "webcam"; @@ -99,10 +99,10 @@ export class Stream { return this.showMyRemote; } - mirrorLocalStream( wr ) { + mirrorLocalStream(wr) { this.showMyRemote = true; this.localMirrored = true; - if ( wr ) { + if (wr) { this.wrStream = wr; } } @@ -125,25 +125,25 @@ export class Stream { return this.dataChannelOpened; } - onDataChannelOpen( event ) { - console.log( 'Data channel is opened' ); + onDataChannelOpen(event) { + console.log('Data channel is opened'); this.dataChannelOpened = true; } - onDataChannelClosed( event ) { - console.log( 'Data channel is closed' ); + onDataChannelClosed(event) { + console.log('Data channel is closed'); this.dataChannelOpened = false; } - sendData( data ) { - if ( this.wp === undefined ) { - throw new Error( 'WebRTC peer has not been created yet' ); + sendData(data) { + if (this.wp === undefined) { + throw new Error('WebRTC peer has not been created yet'); } - if ( !this.dataChannelOpened ) { - throw new Error( 'Data channel is not opened' ); + if (!this.dataChannelOpened) { + throw new Error('Data channel is not opened'); } - console.log( "Sending through data channel: " + data ); - this.wp.send( data ); + console.log("Sending through data channel: " + data); + this.wp.send(data); } getWrStream() { @@ -154,86 +154,86 @@ export class Stream { return this.wp; } - addEventListener( eventName: string, listener: any ) { - this.ee.addListener( eventName, listener ); + addEventListener(eventName: string, listener: any) { + this.ee.addListener(eventName, listener); } - showSpinner( spinnerParentId: string ) { - let progress = document.createElement( 'div' ); + showSpinner(spinnerParentId: string) { + let progress = document.createElement('div'); progress.id = 'progress-' + this.getId(); progress.style.background = "center transparent url('img/spinner.gif') no-repeat"; - let spinnerParent = document.getElementById( spinnerParentId ); - if(spinnerParent){ - spinnerParent.appendChild( progress ); + let spinnerParent = document.getElementById(spinnerParentId); + if (spinnerParent) { + spinnerParent.appendChild(progress); } } - hideSpinner( spinnerId?: string ) { - spinnerId = ( spinnerId === undefined ) ? this.getId() : spinnerId; - hide( 'progress-' + spinnerId ); + hideSpinner(spinnerId?: string) { + spinnerId = (spinnerId === undefined) ? this.getId() : spinnerId; + hide('progress-' + spinnerId); } - playOnlyVideo( parentElement, thumbnailId ) { - this.video = document.createElement( 'video' ); + playOnlyVideo(parentElement, thumbnailId) { + this.video = document.createElement('video'); this.video.id = 'native-video-' + this.getId(); this.video.autoplay = true; this.video.controls = false; - if ( this.wrStream ) { - this.video.src = URL.createObjectURL( this.wrStream ); - show( thumbnailId ); + if (this.wrStream) { + this.video.src = URL.createObjectURL(this.wrStream); + show(thumbnailId); this.hideSpinner(); } else { - console.log( "No wrStream yet for", this.getId() ); + console.log("No wrStream yet for", this.getId()); } - this.videoElements.push( { + this.videoElements.push({ thumb: thumbnailId, video: this.video }); - if ( this.local ) { + if (this.local) { this.video.muted = true; } - if ( typeof parentElement === "string" ) { - let parentElementDom = document.getElementById( parentElement ); - if(parentElementDom){ - parentElementDom.appendChild( this.video ); + if (typeof parentElement === "string") { + let parentElementDom = document.getElementById(parentElement); + if (parentElementDom) { + parentElementDom.appendChild(this.video); } } else { - parentElement.appendChild( this.video ); + parentElement.appendChild(this.video); } return this.video; } - playThumbnail( thumbnailId ) { + playThumbnail(thumbnailId) { - let container = document.createElement( 'div' ); + let container = document.createElement('div'); container.className = "participant"; container.id = this.getId(); - let thumbnail = document.getElementById( thumbnailId ); - if(thumbnail){ - thumbnail.appendChild( container ); + let thumbnail = document.getElementById(thumbnailId); + if (thumbnail) { + thumbnail.appendChild(container); } - this.elements.push( container ); + this.elements.push(container); - let name = document.createElement( 'div' ); - container.appendChild( name ); - let userName = this.getId().replace( '_webcam', '' ); - if ( userName.length >= 16 ) { - userName = userName.substring( 0, 16 ) + "..."; + let name = document.createElement('div'); + container.appendChild(name); + let userName = this.getId().replace('_webcam', ''); + if (userName.length >= 16) { + userName = userName.substring(0, 16) + "..."; } - name.appendChild( document.createTextNode( userName ) ); + name.appendChild(document.createTextNode(userName)); name.id = "name-" + this.getId(); name.className = "name"; name.title = this.getId(); - this.showSpinner( thumbnailId ); + this.showSpinner(thumbnailId); - return this.playOnlyVideo( container, thumbnailId ); + return this.playOnlyVideo(container, thumbnailId); } getIdInParticipant() { @@ -245,7 +245,7 @@ export class Stream { } getId() { - if ( this.participant ) { + if (this.participant) { return this.participant.getId() + "_" + this.id; } else { return this.id + "_webcam"; @@ -254,9 +254,11 @@ export class Stream { requestCameraAccess(callback: Callback) { - this.participant.addStream( this ); + this.participant.addStream(this); - let constraints = { + let constraints = this.mediaConstraints; + + let constraints2 = { audio: true, video: { width: { @@ -268,78 +270,79 @@ export class Stream { } }; - navigator.mediaDevices.getUserMedia(constraints) - .then(userStream => { - userStream.getAudioTracks()[0].enabled = this.sendAudio; - userStream.getVideoTracks()[0].enabled = this.sendVideo; + navigator.mediaDevices.getUserMedia(constraints) + .then(userStream => { + userStream.getAudioTracks()[0].enabled = this.sendAudio; + userStream.getVideoTracks()[0].enabled = this.sendVideo; - this.wrStream = userStream; - callback(undefined, this);}) - .catch(function(e) { - console.error( "Access denied", e ); - callback(e, undefined); - }); + this.wrStream = userStream; + callback(undefined, this); + }) + .catch(function (e) { + console.error("Access denied", e); + callback(e, undefined); + }); } - publishVideoCallback( error, sdpOfferParam, wp ) { - - if ( error ) { - return console.error( "(publish) SDP offer error: " - + JSON.stringify( error ) ); + publishVideoCallback(error, sdpOfferParam, wp) { + + if (error) { + return console.error("(publish) SDP offer error: " + + JSON.stringify(error)); } - console.log( "Sending SDP offer to publish as " - + this.getId(), sdpOfferParam ); + console.log("Sending SDP offer to publish as " + + this.getId(), sdpOfferParam); - this.openVidu.sendRequest( "publishVideo", { + this.openVidu.sendRequest("publishVideo", { sdpOffer: sdpOfferParam, doLoopback: this.displayMyRemote() || false - }, ( error, response ) => { - if ( error ) { - console.error( "Error on publishVideo: " + JSON.stringify( error ) ); + }, (error, response) => { + if (error) { + console.error("Error on publishVideo: " + JSON.stringify(error)); } else { - this.room.emitEvent( 'stream-published', [{ + this.room.emitEvent('stream-published', [{ stream: this - }] ) - this.processSdpAnswer( response.sdpAnswer ); + }]) + this.processSdpAnswer(response.sdpAnswer); } }); } - startVideoCallback( error, sdpOfferParam, wp ) { - if ( error ) { - return console.error( "(subscribe) SDP offer error: " - + JSON.stringify( error ) ); + startVideoCallback(error, sdpOfferParam, wp) { + if (error) { + return console.error("(subscribe) SDP offer error: " + + JSON.stringify(error)); } - console.log( "Sending SDP offer to subscribe to " - + this.getId(), sdpOfferParam ); - this.openVidu.sendRequest( "receiveVideoFrom", { + console.log("Sending SDP offer to subscribe to " + + this.getId(), sdpOfferParam); + this.openVidu.sendRequest("receiveVideoFrom", { sender: this.getId(), sdpOffer: sdpOfferParam - }, ( error, response ) => { - if ( error ) { - console.error( "Error on recvVideoFrom: " + JSON.stringify( error ) ); + }, (error, response) => { + if (error) { + console.error("Error on recvVideoFrom: " + JSON.stringify(error)); } else { - this.processSdpAnswer( response.sdpAnswer ); + this.processSdpAnswer(response.sdpAnswer); } }); } - private initWebRtcPeer( sdpOfferCallback ) { - if ( this.local ) { + private initWebRtcPeer(sdpOfferCallback) { + if (this.local) { let userMediaConstraints = { - audio : this.sendAudio, - video : this.sendVideo + audio: this.sendAudio, + video: this.sendVideo } - + let options: any = { videoStream: this.wrStream, mediaConstraints: userMediaConstraints, - onicecandidate: this.participant.sendIceCandidate.bind( this.participant ), + onicecandidate: this.participant.sendIceCandidate.bind(this.participant), } - - if ( this.dataChannel ) { + + if (this.dataChannel) { options.dataChannelConfig = { id: this.getChannelName(), onopen: this.onDataChannelOpen, @@ -347,20 +350,20 @@ export class Stream { }; options.dataChannels = true; } - - if ( this.displayMyRemote() ) { - this.wp = new kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv( options, error => { - if ( error ) { - return console.error( error ); + + if (this.displayMyRemote()) { + this.wp = new kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv(options, error => { + if (error) { + return console.error(error); } - this.wp.generateOffer( sdpOfferCallback.bind( this ) ); + this.wp.generateOffer(sdpOfferCallback.bind(this)); }); } else { - this.wp = new kurentoUtils.WebRtcPeer.WebRtcPeerSendonly( options, error => { - if ( error ) { - return console.error( error ); + this.wp = new kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(options, error => { + if (error) { + return console.error(error); } - this.wp.generateOffer( sdpOfferCallback.bind( this ) ); + this.wp.generateOffer(sdpOfferCallback.bind(this)); }); } } else { @@ -370,28 +373,28 @@ export class Stream { OfferToReceiveAudio: this.recvAudio } }; - console.log( "Constraints of generate SDP offer (subscribing)", - offerConstraints ); + console.log("Constraints of generate SDP offer (subscribing)", + offerConstraints); let options = { - onicecandidate: this.participant.sendIceCandidate.bind( this.participant ), + onicecandidate: this.participant.sendIceCandidate.bind(this.participant), connectionConstraints: offerConstraints } - this.wp = new kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly( options, error => { - if ( error ) { - return console.error( error ); + this.wp = new kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options, error => { + if (error) { + return console.error(error); } - this.wp.generateOffer( sdpOfferCallback.bind( this ) ); + this.wp.generateOffer(sdpOfferCallback.bind(this)); }); } - console.log( "Waiting for SDP offer to be generated (" - + ( this.local ? "local" : "remote" ) + " peer: " + this.getId() + ")" ); + console.log("Waiting for SDP offer to be generated (" + + (this.local ? "local" : "remote") + " peer: " + this.getId() + ")"); } publish() { // FIXME: Throw error when stream is not local - this.initWebRtcPeer( this.publishVideoCallback ); + this.initWebRtcPeer(this.publishVideoCallback); // FIXME: Now we have coupled connecting to a room and adding a // stream to this room. But in the new API, there are two steps. @@ -405,116 +408,116 @@ export class Stream { // automatically to all other participants. We use this method only to // negotiate SDP - this.initWebRtcPeer( this.startVideoCallback ); + this.initWebRtcPeer(this.startVideoCallback); } - processSdpAnswer( sdpAnswer ) { + processSdpAnswer(sdpAnswer) { - let answer = new RTCSessionDescription( { + let answer = new RTCSessionDescription({ type: 'answer', sdp: sdpAnswer, }); - console.log( this.getId() + ": set peer connection with recvd SDP answer", - sdpAnswer ); + console.log(this.getId() + ": set peer connection with recvd SDP answer", + sdpAnswer); let participantId = this.getId(); let pc = this.wp.peerConnection; - pc.setRemoteDescription( answer, () => { + pc.setRemoteDescription(answer, () => { // Avoids to subscribe to your own stream remotely // except when showMyRemote is true - if ( !this.local || this.displayMyRemote() ) { + if (!this.local || this.displayMyRemote()) { this.wrStream = pc.getRemoteStreams()[0]; - console.log( "Peer remote stream", this.wrStream ); - - if ( this.wrStream != undefined ) { - - this.speechEvent = kurentoUtils.WebRtcPeer.hark( this.wrStream, { threshold: this.room.thresholdSpeaker }); - - this.speechEvent.on( 'speaking', () => { - this.room.addParticipantSpeaking( participantId ); - this.room.emitEvent( 'stream-speaking', [{ + console.log("Peer remote stream", this.wrStream); + + if (this.wrStream != undefined) { + + this.speechEvent = kurentoUtils.WebRtcPeer.hark(this.wrStream, { threshold: this.room.thresholdSpeaker }); + + this.speechEvent.on('speaking', () => { + this.room.addParticipantSpeaking(participantId); + this.room.emitEvent('stream-speaking', [{ participantId: participantId - }] ); + }]); }); - this.speechEvent.on( 'stopped_speaking', () => { - this.room.removeParticipantSpeaking( participantId ); - this.room.emitEvent( 'stream-stopped-speaking', [{ + this.speechEvent.on('stopped_speaking', () => { + this.room.removeParticipantSpeaking(participantId); + this.room.emitEvent('stream-stopped-speaking', [{ participantId: participantId - }] ); + }]); }); } for (let videoElement of this.videoElements) { let thumbnailId = videoElement.thumb; let video = videoElement.video; - video.src = URL.createObjectURL( this.wrStream ); + video.src = URL.createObjectURL(this.wrStream); video.onplay = () => { - console.log( this.getId() + ': ' + 'Video playing' ); + console.log(this.getId() + ': ' + 'Video playing'); show(thumbnailId); - this.hideSpinner( this.getId() ); + this.hideSpinner(this.getId()); }; } - this.room.emitEvent( 'stream-subscribed', [{ + this.room.emitEvent('stream-subscribed', [{ stream: this - }] ); + }]); } }, error => { - console.error( this.getId() + ": Error setting SDP to the peer connection: " - + JSON.stringify( error ) ); + console.error(this.getId() + ": Error setting SDP to the peer connection: " + + JSON.stringify(error)); }); } unpublish() { - if ( this.wp ) { + if (this.wp) { this.wp.dispose(); } else { - if ( this.wrStream ) { - this.wrStream.getAudioTracks().forEach( function( track ) { + if (this.wrStream) { + this.wrStream.getAudioTracks().forEach(function (track) { track.stop && track.stop() }) - this.wrStream.getVideoTracks().forEach( function( track ) { + this.wrStream.getVideoTracks().forEach(function (track) { track.stop && track.stop() }) } } - if ( this.speechEvent ) { + if (this.speechEvent) { this.speechEvent.stop(); } - console.log( this.getId() + ": Stream '" + this.id + "' unpublished" ); + console.log(this.getId() + ": Stream '" + this.id + "' unpublished"); } dispose() { - function disposeElement( element ) { - if ( element && element.parentNode ) { - element.parentNode.removeChild( element ); + function disposeElement(element) { + if (element && element.parentNode) { + element.parentNode.removeChild(element); } } - this.elements.forEach( e => disposeElement( e ) ); + this.elements.forEach(e => disposeElement(e)); - this.videoElements.forEach( ve => disposeElement( ve ) ); + this.videoElements.forEach(ve => disposeElement(ve)); - disposeElement( "progress-" + this.getId() ); + disposeElement("progress-" + this.getId()); - if ( this.wp ) { + if (this.wp) { this.wp.dispose(); } else { - if ( this.wrStream ) { - this.wrStream.getAudioTracks().forEach( function( track ) { + if (this.wrStream) { + this.wrStream.getAudioTracks().forEach(function (track) { track.stop && track.stop() }) - this.wrStream.getVideoTracks().forEach( function( track ) { + this.wrStream.getVideoTracks().forEach(function (track) { track.stop && track.stop() }) } } - if ( this.speechEvent ) { + if (this.speechEvent) { this.speechEvent.stop(); } - console.log( this.getId() + ": Stream '" + this.id + "' disposed" ); + console.log(this.getId() + ": Stream '" + this.id + "' disposed"); } } diff --git a/openvidu-ng-testapp/src/app/app.component.html b/openvidu-ng-testapp/src/app/app.component.html index e45f6506..1473b5fb 100644 --- a/openvidu-ng-testapp/src/app/app.component.html +++ b/openvidu-ng-testapp/src/app/app.component.html @@ -1,34 +1,75 @@
-

Join a video session

-
-

- - -

-

- - -

-

- Send video -

-

- Send audio -

-

- -

-
+

Join a video session

+
+
+ + +
+
+ + +
+
+ Send video +
+ + + + + + + + + + + + + + + + + +
ConstraintValue
width
height
frameRate
+
+
+
+ Send audio +
+
+ +
+
-

{{sessionId}}

- - Toggle your video - Toggle your audio -
- -
-
\ No newline at end of file +

{{sessionId}}

+ + Toggle your video + Toggle your audio +
+
+
+ +
+
+
+
+

Bitrate: {{stats[i].bitrate}}

+
+ + + + + + + + + + + +
TypeTimeOther attributes
{{st.type}}{{st.timestamp}}{{getStatAttributes(st.res)}}
+
+
+
+
+ diff --git a/openvidu-ng-testapp/src/app/app.component.ts b/openvidu-ng-testapp/src/app/app.component.ts index ca582304..f7aada2c 100644 --- a/openvidu-ng-testapp/src/app/app.component.ts +++ b/openvidu-ng-testapp/src/app/app.component.ts @@ -1,3 +1,5 @@ +import { Observable } from 'rxjs/Rx'; +import { enableDebugTools } from '@angular/platform-browser'; import { Component } from '@angular/core'; import { OpenVidu, Session, Stream } from 'openvidu-browser'; @@ -18,16 +20,24 @@ export class AppComponent { streams: Stream[] = []; // Publish options - joinWithVideo: boolean = true; - joinWithAudio: boolean = true; + joinWithVideo: boolean = false; + joinWithAudio: boolean = false; toggleVideo: boolean; toggleAudio: boolean; + //Statistics + stats = []; + bytesPrev = []; + timestampPrev = []; + + constructor() { this.generateParticipantInfo(); window.onbeforeunload = () => { this.openVidu.close(true); } + + //this.obtainSupportedConstraints(); } private generateParticipantInfo() { @@ -38,19 +48,32 @@ export class AppComponent { private addVideoTag(stream: Stream) { console.log("Stream added"); this.streams.push(stream); + + //For statistics + this.timestampPrev.push(0); + this.bytesPrev.push(0); } private removeVideoTag(stream: Stream) { console.log("Stream removed"); - this.streams.slice(this.streams.indexOf(stream), 1); + let index = this.streams.indexOf(stream); + this.streams.splice(index, 1); + + this.stats.splice(index, 1); + this.timestampPrev.splice(index, 1); + this.bytesPrev.splice(index, 1); } joinSession() { + let mediaConstraints = this.generateMediaConstraints(); + + console.log(mediaConstraints); + var cameraOptions = { audio: this.joinWithAudio, video: this.joinWithVideo, data: true, - mediaConstraints: {} + mediaConstraints: mediaConstraints } this.joinSessionShared(cameraOptions); } @@ -90,6 +113,8 @@ export class AppComponent { camera.publish(); + this.intervalStats().subscribe(); + session.addEventListener("stream-added", streamEvent => { this.addVideoTag(streamEvent.stream); console.log("Stream " + streamEvent.stream + " added"); @@ -124,4 +149,96 @@ export class AppComponent { console.log(msg); } -} + /*obtainSupportedConstraints() { + let constraints = Object.keys(navigator.mediaDevices.getSupportedConstraints()); + this.supportedVideoContstraints = constraints.filter((e) => { + return this.mediaTrackSettingsVideo.indexOf(e) > -1; + }); + this.supportedAudioContstraints = constraints.filter((e) => { + return this.mediaTrackSettingsAudio.indexOf(e) > -1; + }); + + console.log(constraints); + console.log(this.supportedVideoContstraints); + console.log(this.supportedAudioContstraints); + }*/ + + generateMediaConstraints() { + let mediaConstraints = { + audio: true, + video: {} + } + if (this.joinWithVideo) { + mediaConstraints.video['width'] = { exact: Number((document.getElementById('width')).value) }; + mediaConstraints.video['height'] = { exact: Number((document.getElementById('height')).value) }; + mediaConstraints.video['frameRate'] = { ideal: Number((document.getElementById('frameRate')).value) }; + } + + return mediaConstraints; + } + + + intervalStats() { + return Observable + .interval(1000) + .flatMap(() => { + let i = 0; + for (let str of this.streams) { + if (str.getWebRtcPeer().peerConnection) { + this.intervalStatsAux(i, str); + i++; + } + } + return []; + }); + } + + intervalStatsAux(i: number, stream: Stream) { + stream.getWebRtcPeer().peerConnection.getStats(null) + .then((results) => { + this.stats[i] = this.dumpStats(results, i); + }); + } + + + dumpStats(results, i) { + var statsArray = []; + let bitrate; + + results.forEach((res) => { + let date = new Date(res.timestamp); + statsArray.push({ res: res, type: res.type, timestamp: date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds() }); + + let now = res.timestamp; + + if (res.type === 'inbound-rtp' && res.mediaType === 'video') { + // firefox calculates the bitrate for us + // https://bugzilla.mozilla.org/show_bug.cgi?id=951496 + bitrate = Math.floor(res.bitrateMean / 1024); + + } else if (res.type === 'ssrc' && res.bytesReceived && res.googFrameRateReceived) { + // chrome does not so we need to do it ourselves + var bytes = res.bytesReceived; + if (this.timestampPrev[i]) { + bitrate = 8 * (bytes - this.bytesPrev[i]) / (now - this.timestampPrev[i]); + bitrate = Math.floor(bitrate); + } + this.bytesPrev[i] = bytes; + this.timestampPrev[i] = now; + } + + }); + if (bitrate) { + bitrate += ' kbits/sec'; + } + return { statsArray: statsArray, bitrate: bitrate }; + } + + getStatAttributes(stat) { + let s = ''; + Object.keys(stat).forEach((key) => { + if (key != 'type' && key != 'timestamp') s += (' | ' + key + ' | '); + }); + return s; + } +} \ No newline at end of file diff --git a/openvidu-ng-testapp/src/app/stream.component.ts b/openvidu-ng-testapp/src/app/stream.component.ts index 15339a4d..55261dda 100644 --- a/openvidu-ng-testapp/src/app/stream.component.ts +++ b/openvidu-ng-testapp/src/app/stream.component.ts @@ -7,12 +7,10 @@ import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; styles: [` .participant { float: left; - width: 20%; margin: 10px; } .participant video { - width: 100%; - height: auto; + }`], template: `