Resolution and framrate selection. Statistics are shwon

pull/3/head
pabloFuente 2017-03-17 19:22:02 +01:00
parent f60fae576a
commit 7a3c064068
6 changed files with 366 additions and 207 deletions

View File

@ -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: Here's a simple summary about the structure of OpenVidu:
<center>![OpenVidu structure](https://drive.google.com/uc?export=view&id=0B61cQ4sbhmWSQ1AwaXlnRTR4djA)</center> ![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. - **Kurento Media Server**: External module which provides the low-level functionalities related to the media transmission.

View File

@ -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: Typescript is currently used to develop openvidu-browser. The class diagram is shown below:
<center>![OpenVidu structure](https://drive.google.com/uc?export=view&id=0B61cQ4sbhmWSM1N3SmE5amt3TzA)</center> ![OpenVidu structure](https://drive.google.com/uc?export=view&id=0B61cQ4sbhmWSM1N3SmE5amt3TzA)
What is Kurento What is Kurento
--------------- ---------------

View File

@ -15,15 +15,15 @@ import 'webrtc-adapter';
declare var navigator: any; declare var navigator: any;
declare var RTCSessionDescription: any; declare var RTCSessionDescription: any;
function jq(id: string):string { function jq(id: string): string {
return id.replace(/(@|:|\.|\[|\]|,)/g, "\\$1"); return id.replace(/(@|:|\.|\[|\]|,)/g, "\\$1");
} }
function show(id: string){ function show(id: string) {
document.getElementById(jq(id))!.style.display = 'block'; document.getElementById(jq(id))!.style.display = 'block';
} }
function hide(id: string){ function hide(id: string) {
document.getElementById(jq(id))!.style.display = 'none'; document.getElementById(jq(id))!.style.display = 'none';
} }
@ -65,9 +65,9 @@ export class Stream {
private dataChannel: boolean; private dataChannel: boolean;
private dataChannelOpened = false; 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; this.id = options.id;
} else { } else {
this.id = "webcam"; this.id = "webcam";
@ -99,10 +99,10 @@ export class Stream {
return this.showMyRemote; return this.showMyRemote;
} }
mirrorLocalStream( wr ) { mirrorLocalStream(wr) {
this.showMyRemote = true; this.showMyRemote = true;
this.localMirrored = true; this.localMirrored = true;
if ( wr ) { if (wr) {
this.wrStream = wr; this.wrStream = wr;
} }
} }
@ -125,25 +125,25 @@ export class Stream {
return this.dataChannelOpened; return this.dataChannelOpened;
} }
onDataChannelOpen( event ) { onDataChannelOpen(event) {
console.log( 'Data channel is opened' ); console.log('Data channel is opened');
this.dataChannelOpened = true; this.dataChannelOpened = true;
} }
onDataChannelClosed( event ) { onDataChannelClosed(event) {
console.log( 'Data channel is closed' ); console.log('Data channel is closed');
this.dataChannelOpened = false; this.dataChannelOpened = false;
} }
sendData( data ) { sendData(data) {
if ( this.wp === undefined ) { if (this.wp === undefined) {
throw new Error( 'WebRTC peer has not been created yet' ); throw new Error('WebRTC peer has not been created yet');
} }
if ( !this.dataChannelOpened ) { if (!this.dataChannelOpened) {
throw new Error( 'Data channel is not opened' ); throw new Error('Data channel is not opened');
} }
console.log( "Sending through data channel: " + data ); console.log("Sending through data channel: " + data);
this.wp.send( data ); this.wp.send(data);
} }
getWrStream() { getWrStream() {
@ -154,86 +154,86 @@ export class Stream {
return this.wp; return this.wp;
} }
addEventListener( eventName: string, listener: any ) { addEventListener(eventName: string, listener: any) {
this.ee.addListener( eventName, listener ); this.ee.addListener(eventName, listener);
} }
showSpinner( spinnerParentId: string ) { showSpinner(spinnerParentId: string) {
let progress = document.createElement( 'div' ); let progress = document.createElement('div');
progress.id = 'progress-' + this.getId(); progress.id = 'progress-' + this.getId();
progress.style.background = "center transparent url('img/spinner.gif') no-repeat"; progress.style.background = "center transparent url('img/spinner.gif') no-repeat";
let spinnerParent = document.getElementById( spinnerParentId ); let spinnerParent = document.getElementById(spinnerParentId);
if(spinnerParent){ if (spinnerParent) {
spinnerParent.appendChild( progress ); spinnerParent.appendChild(progress);
} }
} }
hideSpinner( spinnerId?: string ) { hideSpinner(spinnerId?: string) {
spinnerId = ( spinnerId === undefined ) ? this.getId() : spinnerId; spinnerId = (spinnerId === undefined) ? this.getId() : spinnerId;
hide( 'progress-' + spinnerId ); hide('progress-' + spinnerId);
} }
playOnlyVideo( parentElement, thumbnailId ) { playOnlyVideo(parentElement, thumbnailId) {
this.video = document.createElement( 'video' ); this.video = document.createElement('video');
this.video.id = 'native-video-' + this.getId(); this.video.id = 'native-video-' + this.getId();
this.video.autoplay = true; this.video.autoplay = true;
this.video.controls = false; this.video.controls = false;
if ( this.wrStream ) { if (this.wrStream) {
this.video.src = URL.createObjectURL( this.wrStream ); this.video.src = URL.createObjectURL(this.wrStream);
show( thumbnailId ); show(thumbnailId);
this.hideSpinner(); this.hideSpinner();
} else { } 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, thumb: thumbnailId,
video: this.video video: this.video
}); });
if ( this.local ) { if (this.local) {
this.video.muted = true; this.video.muted = true;
} }
if ( typeof parentElement === "string" ) { if (typeof parentElement === "string") {
let parentElementDom = document.getElementById( parentElement ); let parentElementDom = document.getElementById(parentElement);
if(parentElementDom){ if (parentElementDom) {
parentElementDom.appendChild( this.video ); parentElementDom.appendChild(this.video);
} }
} else { } else {
parentElement.appendChild( this.video ); parentElement.appendChild(this.video);
} }
return this.video; return this.video;
} }
playThumbnail( thumbnailId ) { playThumbnail(thumbnailId) {
let container = document.createElement( 'div' ); let container = document.createElement('div');
container.className = "participant"; container.className = "participant";
container.id = this.getId(); container.id = this.getId();
let thumbnail = document.getElementById( thumbnailId ); let thumbnail = document.getElementById(thumbnailId);
if(thumbnail){ if (thumbnail) {
thumbnail.appendChild( container ); thumbnail.appendChild(container);
} }
this.elements.push( container ); this.elements.push(container);
let name = document.createElement( 'div' ); let name = document.createElement('div');
container.appendChild( name ); container.appendChild(name);
let userName = this.getId().replace( '_webcam', '' ); let userName = this.getId().replace('_webcam', '');
if ( userName.length >= 16 ) { if (userName.length >= 16) {
userName = userName.substring( 0, 16 ) + "..."; userName = userName.substring(0, 16) + "...";
} }
name.appendChild( document.createTextNode( userName ) ); name.appendChild(document.createTextNode(userName));
name.id = "name-" + this.getId(); name.id = "name-" + this.getId();
name.className = "name"; name.className = "name";
name.title = this.getId(); name.title = this.getId();
this.showSpinner( thumbnailId ); this.showSpinner(thumbnailId);
return this.playOnlyVideo( container, thumbnailId ); return this.playOnlyVideo(container, thumbnailId);
} }
getIdInParticipant() { getIdInParticipant() {
@ -245,7 +245,7 @@ export class Stream {
} }
getId() { getId() {
if ( this.participant ) { if (this.participant) {
return this.participant.getId() + "_" + this.id; return this.participant.getId() + "_" + this.id;
} else { } else {
return this.id + "_webcam"; return this.id + "_webcam";
@ -254,9 +254,11 @@ export class Stream {
requestCameraAccess(callback: Callback<Stream>) { requestCameraAccess(callback: Callback<Stream>) {
this.participant.addStream( this ); this.participant.addStream(this);
let constraints = { let constraints = this.mediaConstraints;
let constraints2 = {
audio: true, audio: true,
video: { video: {
width: { width: {
@ -268,78 +270,79 @@ export class Stream {
} }
}; };
navigator.mediaDevices.getUserMedia(constraints) navigator.mediaDevices.getUserMedia(constraints)
.then(userStream => { .then(userStream => {
userStream.getAudioTracks()[0].enabled = this.sendAudio; userStream.getAudioTracks()[0].enabled = this.sendAudio;
userStream.getVideoTracks()[0].enabled = this.sendVideo; userStream.getVideoTracks()[0].enabled = this.sendVideo;
this.wrStream = userStream; this.wrStream = userStream;
callback(undefined, this);}) callback(undefined, this);
.catch(function(e) { })
console.error( "Access denied", e ); .catch(function (e) {
callback(e, undefined); console.error("Access denied", e);
}); callback(e, undefined);
});
} }
publishVideoCallback( error, sdpOfferParam, wp ) { publishVideoCallback(error, sdpOfferParam, wp) {
if ( error ) { if (error) {
return console.error( "(publish) SDP offer error: " return console.error("(publish) SDP offer error: "
+ JSON.stringify( error ) ); + JSON.stringify(error));
} }
console.log( "Sending SDP offer to publish as " console.log("Sending SDP offer to publish as "
+ this.getId(), sdpOfferParam ); + this.getId(), sdpOfferParam);
this.openVidu.sendRequest( "publishVideo", { this.openVidu.sendRequest("publishVideo", {
sdpOffer: sdpOfferParam, sdpOffer: sdpOfferParam,
doLoopback: this.displayMyRemote() || false doLoopback: this.displayMyRemote() || false
}, ( error, response ) => { }, (error, response) => {
if ( error ) { if (error) {
console.error( "Error on publishVideo: " + JSON.stringify( error ) ); console.error("Error on publishVideo: " + JSON.stringify(error));
} else { } else {
this.room.emitEvent( 'stream-published', [{ this.room.emitEvent('stream-published', [{
stream: this stream: this
}] ) }])
this.processSdpAnswer( response.sdpAnswer ); this.processSdpAnswer(response.sdpAnswer);
} }
}); });
} }
startVideoCallback( error, sdpOfferParam, wp ) { startVideoCallback(error, sdpOfferParam, wp) {
if ( error ) { if (error) {
return console.error( "(subscribe) SDP offer error: " return console.error("(subscribe) SDP offer error: "
+ JSON.stringify( error ) ); + JSON.stringify(error));
} }
console.log( "Sending SDP offer to subscribe to " console.log("Sending SDP offer to subscribe to "
+ this.getId(), sdpOfferParam ); + this.getId(), sdpOfferParam);
this.openVidu.sendRequest( "receiveVideoFrom", { this.openVidu.sendRequest("receiveVideoFrom", {
sender: this.getId(), sender: this.getId(),
sdpOffer: sdpOfferParam sdpOffer: sdpOfferParam
}, ( error, response ) => { }, (error, response) => {
if ( error ) { if (error) {
console.error( "Error on recvVideoFrom: " + JSON.stringify( error ) ); console.error("Error on recvVideoFrom: " + JSON.stringify(error));
} else { } else {
this.processSdpAnswer( response.sdpAnswer ); this.processSdpAnswer(response.sdpAnswer);
} }
}); });
} }
private initWebRtcPeer( sdpOfferCallback ) { private initWebRtcPeer(sdpOfferCallback) {
if ( this.local ) { if (this.local) {
let userMediaConstraints = { let userMediaConstraints = {
audio : this.sendAudio, audio: this.sendAudio,
video : this.sendVideo video: this.sendVideo
} }
let options: any = { let options: any = {
videoStream: this.wrStream, videoStream: this.wrStream,
mediaConstraints: userMediaConstraints, mediaConstraints: userMediaConstraints,
onicecandidate: this.participant.sendIceCandidate.bind( this.participant ), onicecandidate: this.participant.sendIceCandidate.bind(this.participant),
} }
if ( this.dataChannel ) { if (this.dataChannel) {
options.dataChannelConfig = { options.dataChannelConfig = {
id: this.getChannelName(), id: this.getChannelName(),
onopen: this.onDataChannelOpen, onopen: this.onDataChannelOpen,
@ -348,19 +351,19 @@ export class Stream {
options.dataChannels = true; options.dataChannels = true;
} }
if ( this.displayMyRemote() ) { if (this.displayMyRemote()) {
this.wp = new kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv( options, error => { this.wp = new kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv(options, error => {
if ( error ) { if (error) {
return console.error( error ); return console.error(error);
} }
this.wp.generateOffer( sdpOfferCallback.bind( this ) ); this.wp.generateOffer(sdpOfferCallback.bind(this));
}); });
} else { } else {
this.wp = new kurentoUtils.WebRtcPeer.WebRtcPeerSendonly( options, error => { this.wp = new kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(options, error => {
if ( error ) { if (error) {
return console.error( error ); return console.error(error);
} }
this.wp.generateOffer( sdpOfferCallback.bind( this ) ); this.wp.generateOffer(sdpOfferCallback.bind(this));
}); });
} }
} else { } else {
@ -370,28 +373,28 @@ export class Stream {
OfferToReceiveAudio: this.recvAudio OfferToReceiveAudio: this.recvAudio
} }
}; };
console.log( "Constraints of generate SDP offer (subscribing)", console.log("Constraints of generate SDP offer (subscribing)",
offerConstraints ); offerConstraints);
let options = { let options = {
onicecandidate: this.participant.sendIceCandidate.bind( this.participant ), onicecandidate: this.participant.sendIceCandidate.bind(this.participant),
connectionConstraints: offerConstraints connectionConstraints: offerConstraints
} }
this.wp = new kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly( options, error => { this.wp = new kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options, error => {
if ( error ) { if (error) {
return console.error( 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 (" console.log("Waiting for SDP offer to be generated ("
+ ( this.local ? "local" : "remote" ) + " peer: " + this.getId() + ")" ); + (this.local ? "local" : "remote") + " peer: " + this.getId() + ")");
} }
publish() { publish() {
// FIXME: Throw error when stream is not local // 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 // 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. // 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 // automatically to all other participants. We use this method only to
// negotiate SDP // negotiate SDP
this.initWebRtcPeer( this.startVideoCallback ); this.initWebRtcPeer(this.startVideoCallback);
} }
processSdpAnswer( sdpAnswer ) { processSdpAnswer(sdpAnswer) {
let answer = new RTCSessionDescription( { let answer = new RTCSessionDescription({
type: 'answer', type: 'answer',
sdp: sdpAnswer, sdp: sdpAnswer,
}); });
console.log( this.getId() + ": set peer connection with recvd SDP answer", console.log(this.getId() + ": set peer connection with recvd SDP answer",
sdpAnswer ); sdpAnswer);
let participantId = this.getId(); let participantId = this.getId();
let pc = this.wp.peerConnection; let pc = this.wp.peerConnection;
pc.setRemoteDescription( answer, () => { pc.setRemoteDescription(answer, () => {
// Avoids to subscribe to your own stream remotely // Avoids to subscribe to your own stream remotely
// except when showMyRemote is true // except when showMyRemote is true
if ( !this.local || this.displayMyRemote() ) { if (!this.local || this.displayMyRemote()) {
this.wrStream = pc.getRemoteStreams()[0]; this.wrStream = pc.getRemoteStreams()[0];
console.log( "Peer remote stream", this.wrStream ); console.log("Peer remote stream", this.wrStream);
if ( this.wrStream != undefined ) { if (this.wrStream != undefined) {
this.speechEvent = kurentoUtils.WebRtcPeer.hark( this.wrStream, { threshold: this.room.thresholdSpeaker }); this.speechEvent = kurentoUtils.WebRtcPeer.hark(this.wrStream, { threshold: this.room.thresholdSpeaker });
this.speechEvent.on( 'speaking', () => { this.speechEvent.on('speaking', () => {
this.room.addParticipantSpeaking( participantId ); this.room.addParticipantSpeaking(participantId);
this.room.emitEvent( 'stream-speaking', [{ this.room.emitEvent('stream-speaking', [{
participantId: participantId participantId: participantId
}] ); }]);
}); });
this.speechEvent.on( 'stopped_speaking', () => { this.speechEvent.on('stopped_speaking', () => {
this.room.removeParticipantSpeaking( participantId ); this.room.removeParticipantSpeaking(participantId);
this.room.emitEvent( 'stream-stopped-speaking', [{ this.room.emitEvent('stream-stopped-speaking', [{
participantId: participantId participantId: participantId
}] ); }]);
}); });
} }
for (let videoElement of this.videoElements) { for (let videoElement of this.videoElements) {
let thumbnailId = videoElement.thumb; let thumbnailId = videoElement.thumb;
let video = videoElement.video; let video = videoElement.video;
video.src = URL.createObjectURL( this.wrStream ); video.src = URL.createObjectURL(this.wrStream);
video.onplay = () => { video.onplay = () => {
console.log( this.getId() + ': ' + 'Video playing' ); console.log(this.getId() + ': ' + 'Video playing');
show(thumbnailId); show(thumbnailId);
this.hideSpinner( this.getId() ); this.hideSpinner(this.getId());
}; };
} }
this.room.emitEvent( 'stream-subscribed', [{ this.room.emitEvent('stream-subscribed', [{
stream: this stream: this
}] ); }]);
} }
}, error => { }, error => {
console.error( this.getId() + ": Error setting SDP to the peer connection: " console.error(this.getId() + ": Error setting SDP to the peer connection: "
+ JSON.stringify( error ) ); + JSON.stringify(error));
}); });
} }
unpublish() { unpublish() {
if ( this.wp ) { if (this.wp) {
this.wp.dispose(); this.wp.dispose();
} else { } else {
if ( this.wrStream ) { if (this.wrStream) {
this.wrStream.getAudioTracks().forEach( function( track ) { this.wrStream.getAudioTracks().forEach(function (track) {
track.stop && track.stop() track.stop && track.stop()
}) })
this.wrStream.getVideoTracks().forEach( function( track ) { this.wrStream.getVideoTracks().forEach(function (track) {
track.stop && track.stop() track.stop && track.stop()
}) })
} }
} }
if ( this.speechEvent ) { if (this.speechEvent) {
this.speechEvent.stop(); this.speechEvent.stop();
} }
console.log( this.getId() + ": Stream '" + this.id + "' unpublished" ); console.log(this.getId() + ": Stream '" + this.id + "' unpublished");
} }
dispose() { dispose() {
function disposeElement( element ) { function disposeElement(element) {
if ( element && element.parentNode ) { if (element && element.parentNode) {
element.parentNode.removeChild( element ); 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(); this.wp.dispose();
} else { } else {
if ( this.wrStream ) { if (this.wrStream) {
this.wrStream.getAudioTracks().forEach( function( track ) { this.wrStream.getAudioTracks().forEach(function (track) {
track.stop && track.stop() track.stop && track.stop()
}) })
this.wrStream.getVideoTracks().forEach( function( track ) { this.wrStream.getVideoTracks().forEach(function (track) {
track.stop && track.stop() track.stop && track.stop()
}) })
} }
} }
if ( this.speechEvent ) { if (this.speechEvent) {
this.speechEvent.stop(); this.speechEvent.stop();
} }
console.log( this.getId() + ": Stream '" + this.id + "' disposed" ); console.log(this.getId() + ": Stream '" + this.id + "' disposed");
} }
} }

View File

@ -1,34 +1,75 @@
<div *ngIf="!session"> <div *ngIf="!session">
<h1>Join a video session</h1> <h1>Join a video session</h1>
<form (submit)="joinSession()" accept-charset="UTF-8"> <form (submit)="joinSession()" accept-charset="UTF-8">
<p> <div>
<label>Participant:</label> <label>Participant:</label>
<input type="text" name="participantId" [(ngModel)]="participantId" required> <input type="text" name="participantId" [(ngModel)]="participantId" required>
</p> </div>
<p> <div>
<label>Session:</label> <label>Session:</label>
<input type="text" name="sessionId" [(ngModel)]="sessionId" required> <input type="text" name="sessionId" [(ngModel)]="sessionId" required>
</p> </div>
<p> <div>
<input type="checkbox" [(ngModel)]="joinWithVideo" id="join-with-video" name="join-with-video"> Send video <input type="checkbox" [(ngModel)]="joinWithVideo" id="join-with-video" name="join-with-video"> Send video
</p> <div *ngIf="joinWithVideo">
<p> <table>
<input type="checkbox" [(ngModel)]="joinWithAudio" id="join-with-audio" name="join-with-audio"> Send audio <tr>
</p> <th>Constraint</th>
<p> <th>Value</th>
<input type="submit" name="commit" value="Join!"> </tr>
</p> <tr>
</form> <td>width</td>
<td><input type="text" id="width" required></td>
</tr>
<tr>
<td>height</td>
<td><input type="text" id="height" required></td>
</tr>
<tr>
<td>frameRate</td>
<td><input type="text" id="frameRate" required></td>
</tr>
</table>
</div>
</div>
<div>
<input type="checkbox" [(ngModel)]="joinWithAudio" id="join-with-audio" name="join-with-audio"> Send audio
</div>
<div>
<input type="submit" name="commit" value="Join!">
</div>
</form>
</div> </div>
<div *ngIf="session"> <div *ngIf="session">
<h2>{{sessionId}}</h2> <h2>{{sessionId}}</h2>
<input type="button" (click)="leaveSession()" value="Leave session"> <input type="button" (click)="leaveSession()" value="Leave session">
<input type="checkbox" id="toggle-video" name="toggle-video" <input type="checkbox" id="toggle-video" name="toggle-video" [checked]="toggleVideo" (change)="updateToggleVideo($event)"> Toggle your video
[checked]="toggleVideo" (change)="updateToggleVideo($event)"> Toggle your video <input type="checkbox" id="toggle-audio" name="toggle-audio" [checked]="toggleAudio" (change)="updateToggleAudio($event)"> Toggle your audio
<input type="checkbox" id="toggle-audio" name="toggle-audio" <div>
[checked]="toggleAudio" (change)="updateToggleAudio($event)"> Toggle your audio <div *ngFor="let s of streams; let i = index" style="display: table-row;">
<div> <div style="display: inline; top: 0;">
<stream *ngFor="let s of streams" [stream]="s"></stream> <stream [stream]="s"></stream>
</div> </div>
<div *ngIf="stats[i]" style="display: inline-block;">
<div style="display: inline; margin-left: 50px;">
<div *ngIf="stats[i].bitrate">
<h2>Bitrate: {{stats[i].bitrate}}</h2>
</div>
<table>
<tr>
<th>Type</th>
<th>Time</th>
<th>Other attributes</th>
</tr>
<tr *ngFor="let st of stats[i].statsArray">
<td>{{st.type}}</td>
<td>{{st.timestamp}}</td>
<td>{{getStatAttributes(st.res)}}</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div> </div>

View File

@ -1,3 +1,5 @@
import { Observable } from 'rxjs/Rx';
import { enableDebugTools } from '@angular/platform-browser';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { OpenVidu, Session, Stream } from 'openvidu-browser'; import { OpenVidu, Session, Stream } from 'openvidu-browser';
@ -18,16 +20,24 @@ export class AppComponent {
streams: Stream[] = []; streams: Stream[] = [];
// Publish options // Publish options
joinWithVideo: boolean = true; joinWithVideo: boolean = false;
joinWithAudio: boolean = true; joinWithAudio: boolean = false;
toggleVideo: boolean; toggleVideo: boolean;
toggleAudio: boolean; toggleAudio: boolean;
//Statistics
stats = [];
bytesPrev = [];
timestampPrev = [];
constructor() { constructor() {
this.generateParticipantInfo(); this.generateParticipantInfo();
window.onbeforeunload = () => { window.onbeforeunload = () => {
this.openVidu.close(true); this.openVidu.close(true);
} }
//this.obtainSupportedConstraints();
} }
private generateParticipantInfo() { private generateParticipantInfo() {
@ -38,19 +48,32 @@ export class AppComponent {
private addVideoTag(stream: Stream) { private addVideoTag(stream: Stream) {
console.log("Stream added"); console.log("Stream added");
this.streams.push(stream); this.streams.push(stream);
//For statistics
this.timestampPrev.push(0);
this.bytesPrev.push(0);
} }
private removeVideoTag(stream: Stream) { private removeVideoTag(stream: Stream) {
console.log("Stream removed"); 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() { joinSession() {
let mediaConstraints = this.generateMediaConstraints();
console.log(mediaConstraints);
var cameraOptions = { var cameraOptions = {
audio: this.joinWithAudio, audio: this.joinWithAudio,
video: this.joinWithVideo, video: this.joinWithVideo,
data: true, data: true,
mediaConstraints: {} mediaConstraints: mediaConstraints
} }
this.joinSessionShared(cameraOptions); this.joinSessionShared(cameraOptions);
} }
@ -90,6 +113,8 @@ export class AppComponent {
camera.publish(); camera.publish();
this.intervalStats().subscribe();
session.addEventListener("stream-added", streamEvent => { session.addEventListener("stream-added", streamEvent => {
this.addVideoTag(streamEvent.stream); this.addVideoTag(streamEvent.stream);
console.log("Stream " + streamEvent.stream + " added"); console.log("Stream " + streamEvent.stream + " added");
@ -124,4 +149,96 @@ export class AppComponent {
console.log(msg); 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((<HTMLInputElement>document.getElementById('width')).value) };
mediaConstraints.video['height'] = { exact: Number((<HTMLInputElement>document.getElementById('height')).value) };
mediaConstraints.video['frameRate'] = { ideal: Number((<HTMLInputElement>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;
}
} }

View File

@ -7,12 +7,10 @@ import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
styles: [` styles: [`
.participant { .participant {
float: left; float: left;
width: 20%;
margin: 10px; margin: 10px;
} }
.participant video { .participant video {
width: 100%;
height: auto;
}`], }`],
template: ` template: `
<div class='participant'> <div class='participant'>