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' ) ;
2020-05-04 20:01:56 +02:00
import { OpenViduLogger } from '../Logger/OpenViduLogger' ;
2020-10-13 16:13:37 +02:00
import { PlatformUtils } from '../Utils/Platform' ;
2020-05-04 20:01:56 +02:00
/ * *
* @hidden
* /
const logger : OpenViduLogger = OpenViduLogger . getInstance ( ) ;
2020-10-13 16:13:37 +02:00
/ * *
* @hidden
* /
2020-11-26 13:17:55 +01:00
let platform : PlatformUtils ;
2020-05-04 20:01:56 +02:00
2018-06-11 13:08:30 +02:00
export interface WebRtcPeerConfiguration {
mediaConstraints : {
audio : boolean ,
video : boolean
} ;
simulcast : boolean ;
onicecandidate : ( event ) = > void ;
2020-12-14 20:09:45 +01:00
iceServers? : RTCIceServer [ ] ;
mediaStream? : MediaStream | null ;
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 {
2020-12-14 20:09:45 +01:00
public pc : RTCPeerConnection ;
public remoteCandidatesQueue : RTCIceCandidate [ ] = [ ] ;
public localCandidatesQueue : RTCIceCandidate [ ] = [ ] ;
2018-06-11 13:08:30 +02:00
2020-12-14 20:09:45 +01:00
// Same as WebRtcPeerConfiguration but without optional fields.
protected configuration : Required < WebRtcPeerConfiguration > ;
2018-06-19 14:32:23 +02:00
2020-12-14 20:09:45 +01:00
private iceCandidateList : RTCIceCandidate [ ] = [ ] ;
2018-06-11 13:08:30 +02:00
private candidategatheringdone = false ;
2020-12-14 20:09:45 +01:00
constructor ( configuration : WebRtcPeerConfiguration ) {
2020-11-26 13:17:55 +01:00
platform = PlatformUtils . getInstance ( ) ;
2020-12-14 20:09:45 +01:00
this . configuration = {
. . . configuration ,
iceServers : ( ! ! configuration . iceServers && configuration . iceServers . length > 0 ) ? configuration.iceServers : freeice ( ) ,
mediaStream : ! ! configuration . mediaStream
? configuration . mediaStream
: null ,
mode : ! ! configuration . mode ? configuration . mode : "sendrecv" ,
id : ! ! configuration . id ? configuration.id : this.generateUniqueId ( ) ,
} ;
2018-06-11 13:08:30 +02:00
this . pc = new RTCPeerConnection ( { iceServers : this.configuration.iceServers } ) ;
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 ) {
2020-06-30 10:48:55 +02:00
let candidate = this . iceCandidateList . shift ( ) ;
this . pc . addIceCandidate ( < RTCIceCandidate > candidate ) ;
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
* /
2020-02-14 20:51:52 +01:00
dispose() {
2020-05-04 20:01:56 +02:00
logger . debug ( 'Disposing WebRtcPeer' ) ;
2020-02-14 20:51:52 +01:00
if ( this . pc ) {
if ( this . pc . signalingState === 'closed' ) {
return ;
2018-06-11 13:08:30 +02:00
}
2020-02-14 20:51:52 +01:00
this . pc . close ( ) ;
this . remoteCandidatesQueue = [ ] ;
this . localCandidatesQueue = [ ] ;
2018-06-11 13:08:30 +02:00
}
}
/ * *
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 ) = > {
2020-12-14 20:09:45 +01:00
const useAudio = this . configuration . mediaConstraints . audio ;
const useVideo = this . configuration . mediaConstraints . video ;
let offerPromise : Promise < RTCSessionDescriptionInit > ;
// TODO: Delete this conditional when all supported browsers are
// modern enough to implement the getTransceivers() method.
if ( "getTransceivers" in this . pc ) {
logger . debug ( "[generateOffer] Method pc.getTransceivers() is available; using it" ) ;
// At this point, all "send" audio/video tracks have been added
// with pc.addTrack(), which in modern versions of libwebrtc
// will have created Transceivers with "sendrecv" direction.
// Source: [addTrack/9.3](https://www.w3.org/TR/2020/CRD-webrtc-20201203/#dom-rtcpeerconnection-addtrack).
//
// Here we just need to enforce that those Transceivers have the
// correct direction, either "sendrecv" or "sendonly".
//
// Otherwise, if the tracks are "recv", no Transceiver should
// have been added yet.
const tcs = this . pc . getTransceivers ( ) ;
if ( tcs . length > 0 ) {
// Assert correct mode.
if (
this . configuration . mode !== "sendrecv" &&
this . configuration . mode !== "sendonly"
) {
throw new Error (
"BUG: Transceivers added, but direction is not send"
) ;
}
2018-06-11 13:08:30 +02:00
2020-12-14 20:09:45 +01:00
for ( const tc of tcs ) {
tc . direction = this . configuration . mode ;
logger . debug (
` RTCRtpTransceiver direction: ${ tc . direction } `
) ;
}
} else {
if ( this . configuration . mode !== "recvonly" ) {
throw new Error (
"BUG: Transceivers missing, but direction is not recv"
) ;
}
2018-06-11 13:08:30 +02:00
2020-12-14 20:09:45 +01:00
if ( useAudio ) {
this . pc . addTransceiver ( "audio" , {
direction : this.configuration.mode ,
} ) ;
}
2018-11-21 12:03:14 +01:00
2020-12-14 20:09:45 +01:00
if ( useVideo ) {
this . pc . addTransceiver ( "video" , {
direction : this.configuration.mode ,
} ) ;
}
2018-11-21 12:03:14 +01:00
}
2020-12-14 20:09:45 +01:00
offerPromise = this . pc . createOffer ( ) ;
2019-05-10 10:36:10 +02:00
} else {
2020-12-14 20:09:45 +01:00
logger . debug ( "[generateOffer] Method pc.getTransceivers() NOT available; using LEGACY offerToReceive{Audio,Video}" ) ;
// DEPRECATED: LEGACY METHOD: Old WebRTC versions don't implement
// Transceivers, and instead depend on the deprecated
// "offerToReceiveAudio" and "offerToReceiveVideo".
const constraints : RTCOfferOptions = {
offerToReceiveAudio :
this . configuration . mode !== "sendonly" && useAudio ,
offerToReceiveVideo :
this . configuration . mode !== "sendonly" && useVideo ,
} ;
logger . debug (
"RTCPeerConnection constraints: " +
JSON . stringify ( constraints )
) ;
// @ts-ignore: Compiler is too clever and thinks this branch
// will never execute.
offerPromise = this . pc . createOffer ( constraints ) ;
}
2018-06-11 13:08:30 +02:00
2020-12-14 20:09:45 +01:00
offerPromise
. then ( ( offer ) = > {
logger . debug ( "Created SDP offer" ) ;
2019-05-10 10:36:10 +02:00
return this . pc . setLocalDescription ( offer ) ;
} )
2020-12-14 20:09:45 +01:00
. then ( ( ) = > {
const localDescription = this . pc . localDescription ;
if ( ! ! localDescription ) {
logger . debug (
"Local description set:" ,
localDescription . sdp
) ;
resolve ( localDescription . sdp ) ;
} else {
reject ( "Local description is not defined" ) ;
}
} )
. catch ( ( error ) = > reject ( error ) ) ;
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
} ;
2020-05-04 20:01:56 +02:00
logger . debug ( 'SDP answer received, setting remote description' ) ;
2018-06-11 13:08:30 +02:00
if ( this . pc . signalingState === 'closed' ) {
reject ( 'RTCPeerConnection is closed' ) ;
}
2020-06-30 10:48:55 +02:00
this . setRemoteDescription ( answer , needsTimeoutOnProcessAnswer , resolve , reject ) ;
} ) ;
}
/ * *
* @hidden
* /
setRemoteDescription ( answer : RTCSessionDescriptionInit , needsTimeoutOnProcessAnswer : boolean , resolve : ( value? : string | PromiseLike < string > | undefined ) = > void , reject : ( reason? : any ) = > void ) {
2020-10-13 16:13:37 +02:00
if ( platform . isIonicIos ( ) ) {
2020-06-30 10:48:55 +02:00
// Ionic iOS platform
if ( needsTimeoutOnProcessAnswer ) {
// 400 ms have not elapsed yet since first remote stream triggered Stream#initWebRtcPeerReceive
setTimeout ( ( ) = > {
logger . info ( 'setRemoteDescription run after timeout for Ionic iOS device' ) ;
2019-07-04 16:28:14 +02:00
this . pc . setRemoteDescription ( new RTCSessionDescription ( answer ) ) . then ( ( ) = > resolve ( ) ) . catch ( error = > reject ( error ) ) ;
2020-06-30 10:48:55 +02:00
} , 250 ) ;
2018-12-05 11:19:40 +01:00
} else {
2020-06-30 10:48:55 +02:00
// 400 ms have elapsed
this . pc . setRemoteDescription ( new RTCSessionDescription ( answer ) ) . then ( ( ) = > resolve ( ) ) . catch ( error = > reject ( error ) ) ;
2018-12-05 11:19:40 +01:00
}
2020-06-30 10:48:55 +02:00
} else {
// Rest of platforms
this . pc . setRemoteDescription ( answer ) . then ( ( ) = > resolve ( ) ) . catch ( error = > reject ( error ) ) ;
}
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 ) = > {
2020-05-04 20:01:56 +02:00
logger . 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 ( ) ;
}
} ) ;
}
2020-02-14 20:51:52 +01:00
addIceConnectionStateChangeListener ( otherId : string ) {
this . pc . oniceconnectionstatechange = ( ) = > {
const iceConnectionState : RTCIceConnectionState = this . pc . iceConnectionState ;
switch ( iceConnectionState ) {
case 'disconnected' :
// Possible network disconnection
2020-12-14 20:09:45 +01:00
logger . warn ( 'IceConnectionState of RTCPeerConnection ' + this . configuration . id + ' (' + otherId + ') change to "disconnected". Possible network disconnection' ) ;
2020-02-14 20:51:52 +01:00
break ;
case 'failed' :
2020-12-14 20:09:45 +01:00
logger . error ( 'IceConnectionState of RTCPeerConnection ' + this . configuration . id + ' (' + otherId + ') to "failed"' ) ;
2020-02-14 20:51:52 +01:00
break ;
case 'closed' :
2020-12-14 20:09:45 +01:00
logger . log ( 'IceConnectionState of RTCPeerConnection ' + this . configuration . id + ' (' + otherId + ') change to "closed"' ) ;
2020-02-14 20:51:52 +01:00
break ;
case 'new' :
2020-12-14 20:09:45 +01:00
logger . log ( 'IceConnectionState of RTCPeerConnection ' + this . configuration . id + ' (' + otherId + ') change to "new"' ) ;
2020-02-14 20:51:52 +01:00
break ;
case 'checking' :
2020-12-14 20:09:45 +01:00
logger . log ( 'IceConnectionState of RTCPeerConnection ' + this . configuration . id + ' (' + otherId + ') change to "checking"' ) ;
2020-02-14 20:51:52 +01:00
break ;
case 'connected' :
2020-12-14 20:09:45 +01:00
logger . log ( 'IceConnectionState of RTCPeerConnection ' + this . configuration . id + ' (' + otherId + ') change to "connected"' ) ;
2020-02-14 20:51:52 +01:00
break ;
case 'completed' :
2020-12-14 20:09:45 +01:00
logger . log ( 'IceConnectionState of RTCPeerConnection ' + this . configuration . id + ' (' + otherId + ') change to "completed"' ) ;
2020-02-14 20:51:52 +01:00
break ;
}
}
}
2020-06-30 10:48:55 +02:00
/ * *
* @hidden
* /
generateUniqueId ( ) : string {
return uuid . v4 ( ) ;
}
2018-06-11 13:08:30 +02:00
}
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 ) ;
}
2020-12-14 20:09:45 +01:00
}