2018-06-11 13:08:30 +02:00
/ *
2020-02-04 11:25:54 +01:00
* ( C ) Copyright 2017 - 2020 OpenVidu ( https : //openvidu.io)
2018-06-11 13:08:30 +02:00
*
* Licensed under the Apache License , Version 2.0 ( the "License" ) ;
* you may not use this file except in compliance with the License .
* You may obtain a copy of the License at
*
* http : //www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing , software
* distributed under the License is distributed on an "AS IS" BASIS ,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
* See the License for the specific language governing permissions and
* limitations under the License .
*
* /
import freeice = require ( 'freeice' ) ;
import uuid = require ( 'uuid' ) ;
2018-12-04 09:55:00 +01:00
import platform = require ( 'platform' ) ;
2018-06-11 13:08:30 +02:00
export interface WebRtcPeerConfiguration {
mediaConstraints : {
audio : boolean ,
video : boolean
} ;
simulcast : boolean ;
onicecandidate : ( event ) = > void ;
iceServers : RTCIceServer [ ] | undefined ;
mediaStream? : MediaStream ;
2018-11-21 12:03:14 +01:00
mode ? : 'sendonly' | 'recvonly' | 'sendrecv' ;
2018-06-11 13:08:30 +02:00
id? : string ;
}
export class WebRtcPeer {
pc : RTCPeerConnection ;
id : string ;
2018-06-19 09:52:04 +02:00
remoteCandidatesQueue : RTCIceCandidate [ ] = [ ] ;
localCandidatesQueue : RTCIceCandidate [ ] = [ ] ;
2018-06-11 13:08:30 +02:00
2018-06-19 14:32:23 +02:00
iceCandidateList : RTCIceCandidate [ ] = [ ] ;
2018-06-11 13:08:30 +02:00
private candidategatheringdone = false ;
constructor ( private configuration : WebRtcPeerConfiguration ) {
this . configuration . iceServers = ( ! ! this . configuration . iceServers && this . configuration . iceServers . length > 0 ) ? this . configuration.iceServers : freeice ( ) ;
this . pc = new RTCPeerConnection ( { iceServers : this.configuration.iceServers } ) ;
this . id = ! ! configuration . id ? configuration.id : uuid.v4 ( ) ;
this . pc . onicecandidate = event = > {
2018-08-31 15:07:34 +02:00
if ( ! ! event . candidate ) {
const candidate : RTCIceCandidate = event . candidate ;
if ( candidate ) {
this . localCandidatesQueue . push ( < RTCIceCandidate > { candidate : candidate.candidate } ) ;
this . candidategatheringdone = false ;
this . configuration . onicecandidate ( event . candidate ) ;
} else if ( ! this . candidategatheringdone ) {
this . candidategatheringdone = true ;
}
2018-06-11 13:08:30 +02:00
}
} ;
this . pc . onsignalingstatechange = ( ) = > {
if ( this . pc . signalingState === 'stable' ) {
2018-06-19 14:32:23 +02:00
while ( this . iceCandidateList . length > 0 ) {
2019-06-03 17:16:18 +02:00
this . pc . addIceCandidate ( < RTCIceCandidate > this . iceCandidateList . shift ( ) ) ;
2018-06-11 13:08:30 +02:00
}
}
} ;
this . start ( ) ;
}
/ * *
* This function creates the RTCPeerConnection object taking into account the
* properties received in the constructor . It starts the SDP negotiation
* process : generates the SDP offer and invokes the onsdpoffer callback . This
* callback is expected to send the SDP offer , in order to obtain an SDP
* answer from another peer .
* /
start ( ) : Promise < any > {
return new Promise ( ( resolve , reject ) = > {
if ( this . pc . signalingState === 'closed' ) {
reject ( 'The peer connection object is in "closed" state. This is most likely due to an invocation of the dispose method before accepting in the dialogue' ) ;
}
if ( ! ! this . configuration . mediaStream ) {
2019-12-05 12:21:42 +01:00
for ( const track of this . configuration . mediaStream . getTracks ( ) ) {
this . pc . addTrack ( track , this . configuration . mediaStream ) ;
}
2018-08-31 15:07:34 +02:00
resolve ( ) ;
2018-06-11 13:08:30 +02:00
}
} ) ;
}
/ * *
* This method frees the resources used by WebRtcPeer
* /
2018-08-31 15:07:34 +02:00
dispose ( videoSourceIsMediaStreamTrack : boolean ) {
2018-06-11 13:08:30 +02:00
console . debug ( 'Disposing WebRtcPeer' ) ;
try {
if ( this . pc ) {
if ( this . pc . signalingState === 'closed' ) {
return ;
}
2018-06-19 09:52:04 +02:00
this . remoteCandidatesQueue = [ ] ;
this . localCandidatesQueue = [ ] ;
2019-12-05 12:21:42 +01:00
// Stop senders
for ( const sender of this . pc . getSenders ( ) ) {
if ( ! videoSourceIsMediaStreamTrack ) {
if ( ! ! sender . track ) {
sender . track . stop ( ) ;
2018-11-28 09:42:26 +01:00
}
2018-08-31 15:07:34 +02:00
}
2019-12-05 12:21:42 +01:00
this . pc . removeTrack ( sender ) ;
}
// Stop receivers
for ( const receiver of this . pc . getReceivers ( ) ) {
if ( ! ! receiver . track ) {
receiver . track . stop ( ) ;
2018-08-31 15:07:34 +02:00
}
2019-12-05 12:21:42 +01:00
}
2018-06-11 13:08:30 +02:00
this . pc . close ( ) ;
}
} catch ( err ) {
console . warn ( 'Exception disposing webrtc peer ' + err ) ;
}
}
/ * *
2019-05-10 10:36:10 +02:00
* Function that creates an offer , sets it as local description and returns the offer param
2018-06-11 13:08:30 +02:00
* to send to OpenVidu Server ( will be the remote description of other peer )
* /
generateOffer ( ) : Promise < string > {
return new Promise ( ( resolve , reject ) = > {
let offerAudio , offerVideo = true ;
// Constraints must have both blocks
if ( ! ! this . configuration . mediaConstraints ) {
offerAudio = ( typeof this . configuration . mediaConstraints . audio === 'boolean' ) ?
this . configuration.mediaConstraints.audio : true ;
offerVideo = ( typeof this . configuration . mediaConstraints . video === 'boolean' ) ?
this . configuration.mediaConstraints.video : true ;
}
2018-06-13 17:31:25 +02:00
const constraints : RTCOfferOptions = {
2018-08-31 15:07:34 +02:00
offerToReceiveAudio : ( this . configuration . mode !== 'sendonly' && offerAudio ) ,
offerToReceiveVideo : ( this . configuration . mode !== 'sendonly' && offerVideo )
2018-06-11 13:08:30 +02:00
} ;
console . debug ( 'RTCPeerConnection constraints: ' + JSON . stringify ( constraints ) ) ;
2018-11-28 09:42:26 +01:00
if ( platform . name === 'Safari' && platform . ua ! ! . indexOf ( 'Safari' ) !== - 1 ) {
// Safari (excluding Ionic), at least on iOS just seems to support unified plan, whereas in other browsers is not yet ready and considered experimental
2018-11-21 12:03:14 +01:00
if ( offerAudio ) {
this . pc . addTransceiver ( 'audio' , {
direction : this.configuration.mode ,
} ) ;
2018-06-11 13:08:30 +02:00
}
2018-11-21 12:03:14 +01:00
if ( offerVideo ) {
this . pc . addTransceiver ( 'video' , {
direction : this.configuration.mode ,
} ) ;
}
this . pc
. createOffer ( )
. then ( offer = > {
console . debug ( 'Created SDP offer' ) ;
return this . pc . setLocalDescription ( offer ) ;
} )
. then ( ( ) = > {
const localDescription = this . pc . localDescription ;
if ( ! ! localDescription ) {
console . debug ( 'Local description set' , localDescription . sdp ) ;
resolve ( localDescription . sdp ) ;
} else {
reject ( 'Local description is not defined' ) ;
}
} )
. catch ( error = > reject ( error ) ) ;
2019-05-10 10:36:10 +02:00
} else {
2018-06-11 13:08:30 +02:00
2019-05-10 10:36:10 +02:00
// Rest of platforms
this . pc . createOffer ( constraints ) . then ( offer = > {
console . debug ( 'Created SDP offer' ) ;
return this . pc . setLocalDescription ( offer ) ;
} )
2019-05-29 14:54:37 +02:00
. then ( ( ) = > {
const localDescription = this . pc . localDescription ;
if ( ! ! localDescription ) {
console . debug ( 'Local description set' , localDescription . sdp ) ;
resolve ( localDescription . sdp ) ;
} else {
reject ( 'Local description is not defined' ) ;
}
} )
. catch ( error = > reject ( error ) ) ;
2019-05-10 10:36:10 +02:00
}
2018-06-11 13:08:30 +02:00
} ) ;
}
/ * *
2019-05-10 10:36:10 +02:00
* Function invoked when a SDP answer is received . Final step in SDP negotiation , the peer
2018-06-11 13:08:30 +02:00
* just needs to set the answer as its remote description
* /
2019-07-04 16:28:14 +02:00
processAnswer ( sdpAnswer : string , needsTimeoutOnProcessAnswer : boolean ) : Promise < string > {
2018-06-11 13:08:30 +02:00
return new Promise ( ( resolve , reject ) = > {
const answer : RTCSessionDescriptionInit = {
type : 'answer' ,
sdp : sdpAnswer
} ;
console . debug ( 'SDP answer received, setting remote description' ) ;
if ( this . pc . signalingState === 'closed' ) {
reject ( 'RTCPeerConnection is closed' ) ;
}
2019-06-06 18:14:36 +02:00
if ( platform [ 'isIonicIos' ] ) {
2019-07-04 16:28:14 +02:00
// Ionic iOS platform
if ( needsTimeoutOnProcessAnswer ) {
// 400 ms have not elapsed yet since first remote stream triggered Stream#initWebRtcPeerReceive
setTimeout ( ( ) = > {
console . info ( 'setRemoteDescription run after timeout for Ionic iOS device' ) ;
this . pc . setRemoteDescription ( new RTCSessionDescription ( answer ) ) . then ( ( ) = > resolve ( ) ) . catch ( error = > reject ( error ) ) ;
} , 250 ) ;
} else {
// 400 ms have elapsed
this . pc . setRemoteDescription ( new RTCSessionDescription ( answer ) ) . then ( ( ) = > resolve ( ) ) . catch ( error = > reject ( error ) ) ;
}
2018-12-05 11:19:40 +01:00
} else {
2019-06-03 17:16:18 +02:00
// Rest of platforms
this . pc . setRemoteDescription ( answer ) . then ( ( ) = > resolve ( ) ) . catch ( error = > reject ( error ) ) ;
2018-12-05 11:19:40 +01:00
}
2018-06-11 13:08:30 +02:00
} ) ;
}
/ * *
* Callback function invoked when an ICE candidate is received
* /
addIceCandidate ( iceCandidate : RTCIceCandidate ) : Promise < void > {
return new Promise ( ( resolve , reject ) = > {
console . debug ( 'Remote ICE candidate received' , iceCandidate ) ;
2018-06-19 14:32:23 +02:00
this . remoteCandidatesQueue . push ( iceCandidate ) ;
2018-06-11 13:08:30 +02:00
switch ( this . pc . signalingState ) {
case 'closed' :
reject ( new Error ( 'PeerConnection object is closed' ) ) ;
break ;
case 'stable' :
if ( ! ! this . pc . remoteDescription ) {
2019-06-03 17:16:18 +02:00
this . pc . addIceCandidate ( iceCandidate ) . then ( ( ) = > resolve ( ) ) . catch ( error = > reject ( error ) ) ;
2019-05-17 14:54:27 +02:00
} else {
this . iceCandidateList . push ( iceCandidate ) ;
resolve ( ) ;
2018-06-11 13:08:30 +02:00
}
break ;
default :
2018-06-19 14:32:23 +02:00
this . iceCandidateList . push ( iceCandidate ) ;
2018-06-11 13:08:30 +02:00
resolve ( ) ;
}
} ) ;
}
}
export class WebRtcPeerRecvonly extends WebRtcPeer {
constructor ( configuration : WebRtcPeerConfiguration ) {
configuration . mode = 'recvonly' ;
super ( configuration ) ;
}
}
export class WebRtcPeerSendonly extends WebRtcPeer {
constructor ( configuration : WebRtcPeerConfiguration ) {
configuration . mode = 'sendonly' ;
super ( configuration ) ;
}
}
export class WebRtcPeerSendrecv extends WebRtcPeer {
constructor ( configuration : WebRtcPeerConfiguration ) {
configuration . mode = 'sendrecv' ;
super ( configuration ) ;
}
}