mirror of https://github.com/OpenVidu/openvidu.git
Resolution and framrate selection. Statistics are shwon
parent
f60fae576a
commit
7a3c064068
|
@ -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:
|
||||
|
||||
<center></center>
|
||||

|
||||
|
||||
|
||||
- **Kurento Media Server**: External module which provides the low-level functionalities related to the media transmission.
|
||||
|
|
|
@ -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:
|
||||
|
||||
<center></center>
|
||||

|
||||
|
||||
What is Kurento
|
||||
---------------
|
||||
|
|
|
@ -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<Stream>) {
|
||||
|
||||
this.participant.addStream( this );
|
||||
this.participant.addStream(this);
|
||||
|
||||
let constraints = {
|
||||
let constraints = this.mediaConstraints;
|
||||
|
||||
let constraints2 = {
|
||||
audio: true,
|
||||
video: {
|
||||
width: {
|
||||
|
@ -274,72 +276,73 @@ export class Stream {
|
|||
userStream.getVideoTracks()[0].enabled = this.sendVideo;
|
||||
|
||||
this.wrStream = userStream;
|
||||
callback(undefined, this);})
|
||||
.catch(function(e) {
|
||||
console.error( "Access denied", e );
|
||||
callback(undefined, this);
|
||||
})
|
||||
.catch(function (e) {
|
||||
console.error("Access denied", e);
|
||||
callback(e, undefined);
|
||||
});
|
||||
}
|
||||
|
||||
publishVideoCallback( error, sdpOfferParam, wp ) {
|
||||
publishVideoCallback(error, sdpOfferParam, wp) {
|
||||
|
||||
if ( error ) {
|
||||
return console.error( "(publish) SDP offer error: "
|
||||
+ JSON.stringify( error ) );
|
||||
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,
|
||||
|
@ -348,19 +351,19 @@ 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 );
|
||||
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.room.addParticipantSpeaking( participantId );
|
||||
this.room.emitEvent( 'stream-speaking', [{
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,34 +1,75 @@
|
|||
<div *ngIf="!session">
|
||||
<h1>Join a video session</h1>
|
||||
<form (submit)="joinSession()" accept-charset="UTF-8">
|
||||
<p>
|
||||
<div>
|
||||
<label>Participant:</label>
|
||||
<input type="text" name="participantId" [(ngModel)]="participantId" required>
|
||||
</p>
|
||||
<p>
|
||||
</div>
|
||||
<div>
|
||||
<label>Session:</label>
|
||||
<input type="text" name="sessionId" [(ngModel)]="sessionId" required>
|
||||
</p>
|
||||
<p>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" [(ngModel)]="joinWithVideo" id="join-with-video" name="join-with-video"> Send video
|
||||
</p>
|
||||
<p>
|
||||
<div *ngIf="joinWithVideo">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Constraint</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<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
|
||||
</p>
|
||||
<p>
|
||||
</div>
|
||||
<div>
|
||||
<input type="submit" name="commit" value="Join!">
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div *ngIf="session">
|
||||
<h2>{{sessionId}}</h2>
|
||||
<input type="button" (click)="leaveSession()" value="Leave session">
|
||||
<input type="checkbox" id="toggle-video" name="toggle-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-video" name="toggle-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
|
||||
<div>
|
||||
<stream *ngFor="let s of streams" [stream]="s"></stream>
|
||||
<div *ngFor="let s of streams; let i = index" style="display: table-row;">
|
||||
<div style="display: inline; top: 0;">
|
||||
<stream [stream]="s"></stream>
|
||||
</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>
|
|
@ -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((<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;
|
||||
}
|
||||
}
|
|
@ -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: `
|
||||
<div class='participant'>
|
||||
|
|
Loading…
Reference in New Issue