2020-11-07 12:16:07 +00:00
import { AbstractServerConnection , ServerCommand , ServerConnectionEvents } from "tc-shared/connection/ConnectionBase" ;
import { ConnectionState } from "tc-shared/ConnectionHandler" ;
2021-01-10 16:36:57 +00:00
import { LogCategory , logDebug , logError , logGroupNative , logTrace , LogType , logWarn } from "tc-shared/log" ;
2020-11-07 12:16:07 +00:00
import { AbstractCommandHandler } from "tc-shared/connection/AbstractCommandHandler" ;
import { CommandResult } from "tc-shared/connection/ServerConnectionDeclaration" ;
2020-11-29 13:42:02 +00:00
import { tr , tra } from "tc-shared/i18n/localize" ;
2020-11-07 12:16:07 +00:00
import { Registry } from "tc-shared/events" ;
2020-11-28 18:33:54 +00:00
import { RemoteRTPAudioTrack , RemoteRTPTrackState , RemoteRTPVideoTrack , TrackClientInfo } from "./RemoteTrack" ;
import { SdpCompressor , SdpProcessor } from "./SdpUtils" ;
2020-11-17 13:27:46 +00:00
import { ErrorCode } from "tc-shared/connection/ErrorCode" ;
2020-11-28 11:58:22 +00:00
import { WhisperTarget } from "tc-shared/voice/VoiceWhisper" ;
2020-11-29 13:42:02 +00:00
import { globalAudioContext } from "tc-backend/audio/player" ;
2021-01-04 20:28:47 +00:00
import { VideoBroadcastConfig , VideoBroadcastType } from "tc-shared/connection/VideoConnection" ;
2021-02-15 14:53:01 +00:00
import { Settings , settings } from "tc-shared/settings" ;
2020-11-07 12:16:07 +00:00
const kSdpCompressionMode = 1 ;
2020-11-15 22:28:00 +00:00
declare global {
interface RTCIceCandidate {
/* Firefox has this */
address : string | undefined ;
}
interface HTMLCanvasElement {
captureStream ( framed : number ) : MediaStream ;
}
}
2020-11-22 12:48:15 +00:00
export type RtcVideoBroadcastStatistics = {
dimensions : { width : number , height : number } ,
frameRate : number ,
codec ? : { name : string , payloadType : number }
bandwidth ? : {
/* bits per second */
currentBps : number ,
/* bits per second */
maxBps : number
} ,
qualityLimitation : "cpu" | "bandwidth" | "none" ,
source : {
frameRate : number ,
dimensions : { width : number , height : number } ,
} ,
} ;
2020-11-17 12:10:24 +00:00
class RetryTimeCalculator {
private readonly minTime : number ;
private readonly maxTime : number ;
private readonly increment : number ;
private retryCount : number ;
private currentTime : number ;
constructor ( minTime : number , maxTime : number , increment : number ) {
this . minTime = minTime ;
this . maxTime = maxTime ;
this . increment = increment ;
this . reset ( ) ;
}
calculateRetryTime() {
if ( this . retryCount >= 5 ) {
/* no more retries */
return 0 ;
}
this . retryCount ++ ;
const time = this . currentTime ;
this . currentTime = Math . min ( this . currentTime + this . increment , this . maxTime ) ;
return time ;
}
reset() {
this . currentTime = this . minTime ;
this . retryCount = 0 ;
}
}
2020-11-22 12:48:15 +00:00
class RTCStatsWrapper {
private readonly supplier : ( ) = > Promise < RTCStatsReport > ;
private readonly statistics ;
constructor ( supplier : ( ) = > Promise < RTCStatsReport > ) {
this . supplier = supplier ;
this . statistics = { } ;
}
async initialize() {
for ( const [ key , value ] of await this . supplier ( ) ) {
if ( typeof this . statistics [ key ] !== "undefined" ) {
logWarn ( LogCategory . WEBRTC , tr ( "Duplicated statistics entry for key %s. Dropping duplicate." ) , key ) ;
continue ;
}
this . statistics [ key ] = value ;
}
}
getValues ( ) : ( RTCStats & { [ T : string ] : string | number } ) [ ] {
return Object . values ( this . statistics ) ;
}
getStatistic ( key : string ) : RTCStats & { [ T : string ] : string | number } {
return this . statistics [ key ] ;
}
getStatisticsByType ( type : string ) : ( RTCStats & { [ T : string ] : string | number } ) [ ] {
return Object . values ( this . statistics ) . filter ( ( e : any ) = > e . type ? . replace ( /-/g , "" ) === type ) as any ;
}
getStatisticByType ( type : string ) : RTCStats & { [ T : string ] : string | number } {
const entries = this . getStatisticsByType ( type ) ;
if ( entries . length === 0 ) {
throw tra ( "missing statistic entry {}" , type ) ;
} else if ( entries . length === 1 ) {
return entries [ 0 ] ;
} else {
throw tra ( "duplicated statistics entry of type {}" , type ) ;
}
}
}
2020-11-15 22:28:00 +00:00
let dummyVideoTrack : MediaStreamTrack | undefined ;
let dummyAudioTrack : MediaStreamTrack | undefined ;
/ *
* For Firefox as soon we stop a sender we ' re never able to get the sender starting again . . .
* ( This only applies after the initial negotiation . Before values of null are allowed )
* So we ' ve to keep it alive with a dummy track .
* /
function getIdleTrack ( kind : "video" | "audio" ) : MediaStreamTrack | null {
2020-11-16 20:02:18 +00:00
if ( window . detectedBrowser ? . name === "firefox" ) {
2020-11-15 22:28:00 +00:00
if ( kind === "video" ) {
if ( ! dummyVideoTrack ) {
const canvas = document . createElement ( "canvas" ) ;
canvas . getContext ( "2d" ) ;
const stream = canvas . captureStream ( 1 ) ;
dummyVideoTrack = stream . getVideoTracks ( ) [ 0 ] ;
}
return dummyVideoTrack ;
} else if ( kind === "audio" ) {
if ( ! dummyAudioTrack ) {
2020-11-29 13:42:02 +00:00
const dest = globalAudioContext ( ) . createMediaStreamDestination ( ) ;
2020-11-15 22:28:00 +00:00
dummyAudioTrack = dest . stream . getAudioTracks ( ) [ 0 ] ;
}
return dummyAudioTrack ;
}
}
return null ;
}
2020-11-07 12:16:07 +00:00
class CommandHandler extends AbstractCommandHandler {
private readonly handle : RTCConnection ;
private readonly sdpProcessor : SdpProcessor ;
constructor ( connection : AbstractServerConnection , handle : RTCConnection , sdpProcessor : SdpProcessor ) {
super ( connection ) ;
this . handle = handle ;
this . sdpProcessor = sdpProcessor ;
this . ignore_consumed = true ;
}
handle_command ( command : ServerCommand ) : boolean {
if ( command . command === "notifyrtcsessiondescription" ) {
const data = command . arguments [ 0 ] ;
if ( ! this . handle [ "peer" ] ) {
logWarn ( LogCategory . WEBRTC , tr ( "Received remote %s without an active peer" ) , data . mode ) ;
return ;
}
/* webrtc-sdp somehow places some empty lines into the sdp */
let sdp = data . sdp . replace ( /\r?\n\r?\n/g , "\n" ) ;
try {
sdp = SdpCompressor . decompressSdp ( sdp , 1 ) ;
} catch ( error ) {
logError ( LogCategory . WEBRTC , tr ( "Failed to decompress remote SDP: %o" ) , error ) ;
2020-11-17 12:10:24 +00:00
this . handle [ "handleFatalError" ] ( tr ( "Failed to decompress remote SDP" ) , true ) ;
2020-11-07 12:16:07 +00:00
return ;
}
if ( RTCConnection . kEnableSdpTrace ) {
2020-12-12 13:18:50 +00:00
const gr = logGroupNative ( LogType . TRACE , LogCategory . WEBRTC , tra ( "Original remote SDP ({})" , data . mode as string ) ) ;
gr . collapsed ( true ) ;
gr . log ( "%s" , data . sdp ) ;
gr . end ( ) ;
2020-11-07 12:16:07 +00:00
}
try {
sdp = this . sdpProcessor . processIncomingSdp ( sdp , data . mode ) ;
} catch ( error ) {
logError ( LogCategory . WEBRTC , tr ( "Failed to reprocess SDP %s: %o" ) , data . mode , error ) ;
2020-11-17 12:10:24 +00:00
this . handle [ "handleFatalError" ] ( tra ( "Failed to preprocess SDP {}" , data . mode as string ) , true ) ;
2020-11-07 12:16:07 +00:00
return ;
}
if ( RTCConnection . kEnableSdpTrace ) {
2020-12-12 13:18:50 +00:00
const gr = logGroupNative ( LogType . TRACE , LogCategory . WEBRTC , tra ( "Patched remote SDP ({})" , data . mode as string ) ) ;
gr . collapsed ( true ) ;
gr . log ( "%s" , sdp ) ;
gr . end ( ) ;
2020-11-07 12:16:07 +00:00
}
if ( data . mode === "answer" ) {
this . handle [ "peer" ] . setRemoteDescription ( {
sdp : sdp ,
type : "answer"
2020-12-05 21:21:14 +00:00
} ) . then ( ( ) = > {
2020-12-11 23:16:17 +00:00
this . handle [ "cachedRemoteSessionDescription" ] = sdp ;
2020-12-05 21:21:14 +00:00
this . handle [ "peerRemoteDescriptionReceived" ] = true ;
2020-12-12 12:19:04 +00:00
setTimeout ( ( ) = > this . handle . applyCachedRemoteIceCandidates ( ) , 50 ) ;
2020-11-07 12:16:07 +00:00
} ) . catch ( error = > {
logError ( LogCategory . WEBRTC , tr ( "Failed to set the remote description: %o" ) , error ) ;
2020-11-17 12:10:24 +00:00
this . handle [ "handleFatalError" ] ( tr ( "Failed to set the remote description (answer)" ) , true ) ;
2020-12-05 21:21:14 +00:00
} ) ;
2020-11-07 12:16:07 +00:00
} else if ( data . mode === "offer" ) {
2020-12-11 23:16:17 +00:00
this . handle [ "cachedRemoteSessionDescription" ] = sdp ;
2020-11-07 12:16:07 +00:00
this . handle [ "peer" ] . setRemoteDescription ( {
sdp : sdp ,
type : "offer"
} ) . then ( ( ) = > this . handle [ "peer" ] . createAnswer ( ) )
2020-11-15 22:28:00 +00:00
. then ( async answer = > {
if ( RTCConnection . kEnableSdpTrace ) {
2020-12-16 21:06:46 +00:00
const gr = logGroupNative ( LogType . TRACE , LogCategory . WEBRTC , tra ( "Original local SDP ({})" , answer . type as string ) ) ;
2020-12-12 13:18:50 +00:00
gr . collapsed ( true ) ;
gr . log ( "%s" , answer . sdp ) ;
gr . end ( ) ;
2020-11-15 22:28:00 +00:00
}
2020-11-07 12:16:07 +00:00
answer . sdp = this . sdpProcessor . processOutgoingSdp ( answer . sdp , "answer" ) ;
2020-11-15 22:28:00 +00:00
2020-11-15 23:02:47 +00:00
await this . handle [ "peer" ] . setLocalDescription ( answer ) ;
return answer ;
} )
. then ( answer = > {
2020-11-07 12:16:07 +00:00
answer . sdp = SdpCompressor . compressSdp ( answer . sdp , kSdpCompressionMode ) ;
2020-11-15 22:28:00 +00:00
if ( RTCConnection . kEnableSdpTrace ) {
2020-12-16 21:06:46 +00:00
const gr = logGroupNative ( LogType . TRACE , LogCategory . WEBRTC , tra ( "Patched local SDP ({})" , answer . type as string ) ) ;
2020-12-12 13:18:50 +00:00
gr . collapsed ( true ) ;
gr . log ( "%s" , answer . sdp ) ;
gr . end ( ) ;
2020-11-15 22:28:00 +00:00
}
2020-11-07 12:16:07 +00:00
return this . connection . send_command ( "rtcsessiondescribe" , {
mode : "answer" ,
sdp : answer.sdp ,
compression : kSdpCompressionMode
} ) ;
} ) . catch ( error = > {
logError ( LogCategory . WEBRTC , tr ( "Failed to set the remote description and execute the renegotiation: %o" ) , error ) ;
2020-11-17 12:10:24 +00:00
this . handle [ "handleFatalError" ] ( tr ( "Failed to set the remote description (offer/renegotiation)" ) , true ) ;
2020-11-07 12:16:07 +00:00
} ) ;
} else {
logWarn ( LogCategory . NETWORKING , tr ( "Received invalid mode for rtc session description (%s)." ) , data . mode ) ;
}
return true ;
2020-12-05 21:21:14 +00:00
} else if ( command . command === "notifyrtcicecandidate" ) {
const candidate = command . arguments [ 0 ] [ "candidate" ] ;
const mediaLine = parseInt ( command . arguments [ 0 ] [ "medialine" ] ) ;
if ( Number . isNaN ( mediaLine ) ) {
logError ( LogCategory . WEBRTC , tr ( "Failed to parse ICE media line: %o" ) , command . arguments [ 0 ] [ "medialine" ] ) ;
return ;
}
if ( candidate ) {
const parsedCandidate = new RTCIceCandidate ( {
candidate : "candidate:" + candidate ,
sdpMLineIndex : mediaLine
} ) ;
this . handle . handleRemoteIceCandidate ( parsedCandidate , mediaLine ) ;
} else {
this . handle . handleRemoteIceCandidate ( undefined , mediaLine ) ;
}
2020-11-07 12:16:07 +00:00
} else if ( command . command === "notifyrtcstreamassignment" ) {
const data = command . arguments [ 0 ] ;
const ssrc = parseInt ( data [ "streamid" ] ) >>> 0 ;
if ( parseInt ( data [ "sclid" ] ) ) {
this . handle [ "doMapStream" ] ( ssrc , {
client_id : parseInt ( data [ "sclid" ] ) ,
client_database_id : parseInt ( data [ "scldbid" ] ) ,
client_name : data [ "sclname" ] ,
2020-11-22 18:08:19 +00:00
client_unique_id : data [ "scluid" ] ,
media : parseInt ( data [ "media" ] )
2020-11-07 12:16:07 +00:00
} ) ;
} else {
this . handle [ "doMapStream" ] ( ssrc , undefined ) ;
}
2020-11-22 18:08:19 +00:00
} else if ( command . command === "notifyrtcstreamstate" ) {
2020-11-07 12:16:07 +00:00
const data = command . arguments [ 0 ] ;
const state = parseInt ( data [ "state" ] ) ;
const ssrc = parseInt ( data [ "streamid" ] ) >>> 0 ;
if ( state === 0 ) {
/* stream stopped */
this . handle [ "handleStreamState" ] ( ssrc , 0 , undefined ) ;
} else if ( state === 1 ) {
this . handle [ "handleStreamState" ] ( ssrc , 1 , {
client_id : parseInt ( data [ "sclid" ] ) ,
client_database_id : parseInt ( data [ "scldbid" ] ) ,
client_name : data [ "sclname" ] ,
2020-11-22 18:08:19 +00:00
client_unique_id : data [ "scluid" ] ,
2020-11-07 12:16:07 +00:00
} ) ;
} else {
logWarn ( LogCategory . WEBRTC , tr ( "Received unknown/invalid rtc track state: %d" ) , state ) ;
}
}
return false ;
}
}
export enum RTPConnectionState {
DISCONNECTED ,
CONNECTING ,
CONNECTED ,
2020-11-17 13:27:46 +00:00
FAILED ,
NOT_SUPPORTED
2020-11-07 12:16:07 +00:00
}
class InternalRemoteRTPAudioTrack extends RemoteRTPAudioTrack {
private muteTimeout ;
constructor ( ssrc : number , transceiver : RTCRtpTransceiver ) {
super ( ssrc , transceiver ) ;
}
destroy() {
this . handleTrackEnded ( ) ;
super . destroy ( ) ;
}
handleAssignment ( info : TrackClientInfo | undefined ) {
if ( Object . isSimilar ( this . currentAssignment , info ) ) {
return ;
}
this . currentAssignment = info ;
if ( info ) {
logDebug ( LogCategory . WEBRTC , tr ( "Remote RTP audio track %d mounted to client %o" ) , this . getSsrc ( ) , info ) ;
this . setState ( RemoteRTPTrackState . Bound ) ;
} else {
logDebug ( LogCategory . WEBRTC , tr ( "Remote RTP audio track %d has been unmounted." ) , this . getSsrc ( ) ) ;
this . setState ( RemoteRTPTrackState . Unbound ) ;
}
}
handleStateNotify ( state : number , info : TrackClientInfo | undefined ) {
if ( ! this . currentAssignment ) {
logWarn ( LogCategory . WEBRTC , tr ( "Received stream state update for %d with miss info. Updating info." ) , this . getSsrc ( ) ) ;
}
const validateInfo = ( ) = > {
if ( info . client_id !== this . currentAssignment . client_id ) {
logWarn ( LogCategory . WEBRTC , tr ( "Received stream state update for %d with miss matching client info. Expected client %d but received %d. Updating stream assignment." ) ,
this . getSsrc ( ) , this . currentAssignment . client_id , info . client_id ) ;
this . currentAssignment = info ;
/* TODO: Update the assignment via doMapStream */
} else if ( info . client_unique_id !== this . currentAssignment . client_unique_id ) {
logWarn ( LogCategory . WEBRTC , tr ( "Received stream state update for %d with miss matching client info. Expected client %s but received %s. Updating stream assignment." ) ,
this . getSsrc ( ) , this . currentAssignment . client_id , info . client_id ) ;
this . currentAssignment = info ;
/* TODO: Update the assignment via doMapStream */
} else if ( this . currentAssignment . client_name !== info . client_name ) {
this . currentAssignment . client_name = info . client_name ;
/* TODO: Notify name update */
}
} ;
clearTimeout ( this . muteTimeout ) ;
this . muteTimeout = undefined ;
if ( state === 1 ) {
validateInfo ( ) ;
this . shouldReplay = true ;
2021-02-15 14:53:01 +00:00
this . updateGainNode ( ) ;
2020-11-07 12:16:07 +00:00
this . setState ( RemoteRTPTrackState . Started ) ;
} else {
/* There wil be no info present */
this . setState ( RemoteRTPTrackState . Bound ) ;
/* since we're might still having some jitter stuff */
this . muteTimeout = setTimeout ( ( ) = > {
this . shouldReplay = false ;
2021-02-15 14:53:01 +00:00
this . updateGainNode ( ) ;
2020-11-07 12:16:07 +00:00
} , 1000 ) ;
}
}
}
class InternalRemoteRTPVideoTrack extends RemoteRTPVideoTrack {
constructor ( ssrc : number , transceiver : RTCRtpTransceiver ) {
super ( ssrc , transceiver ) ;
}
destroy() {
this . handleTrackEnded ( ) ;
super . destroy ( ) ;
}
handleAssignment ( info : TrackClientInfo | undefined ) {
if ( Object . isSimilar ( this . currentAssignment , info ) ) {
return ;
}
this . currentAssignment = info ;
if ( info ) {
logDebug ( LogCategory . WEBRTC , tr ( "Remote RTP video track %d mounted to client %o" ) , this . getSsrc ( ) , info ) ;
this . setState ( RemoteRTPTrackState . Bound ) ;
} else {
logDebug ( LogCategory . WEBRTC , tr ( "Remote RTP video track %d has been unmounted." ) , this . getSsrc ( ) ) ;
this . setState ( RemoteRTPTrackState . Unbound ) ;
}
}
handleStateNotify ( state : number , info : TrackClientInfo | undefined ) {
if ( ! this . currentAssignment ) {
logWarn ( LogCategory . WEBRTC , tr ( "Received stream state update for %d with miss info. Updating info." ) , this . getSsrc ( ) ) ;
}
const validateInfo = ( ) = > {
if ( info . client_id !== this . currentAssignment . client_id ) {
logWarn ( LogCategory . WEBRTC , tr ( "Received stream state update for %d with miss matching client info. Expected client %d but received %d. Updating stream assignment." ) ,
this . getSsrc ( ) , this . currentAssignment . client_id , info . client_id ) ;
this . currentAssignment = info ;
/* TODO: Update the assignment via doMapStream */
} else if ( info . client_unique_id !== this . currentAssignment . client_unique_id ) {
logWarn ( LogCategory . WEBRTC , tr ( "Received stream state update for %d with miss matching client info. Expected client %s but received %s. Updating stream assignment." ) ,
this . getSsrc ( ) , this . currentAssignment . client_id , info . client_id ) ;
this . currentAssignment = info ;
/* TODO: Update the assignment via doMapStream */
} else if ( this . currentAssignment . client_name !== info . client_name ) {
this . currentAssignment . client_name = info . client_name ;
/* TODO: Notify name update */
}
} ;
if ( state === 1 ) {
validateInfo ( ) ;
this . setState ( RemoteRTPTrackState . Started ) ;
} else {
/* There wil be no info present */
this . setState ( RemoteRTPTrackState . Bound ) ;
}
}
}
export type RTCSourceTrackType = "audio" | "audio-whisper" | "video" | "video-screen" ;
export type RTCBroadcastableTrackType = Exclude < RTCSourceTrackType , " audio - whisper " > ;
const kRtcSourceTrackTypes : RTCSourceTrackType [ ] = [ "audio" , "audio-whisper" , "video" , "video-screen" ] ;
function broadcastableTrackTypeToNumber ( type : RTCBroadcastableTrackType ) : number {
switch ( type ) {
case "video-screen" :
return 3 ;
case "video" :
return 2 ;
case "audio" :
return 1 ;
default :
throw tr ( "invalid target type" ) ;
}
}
type TemporaryRtpStream = {
createTimestamp : number ,
timeoutId : number ,
ssrc : number ,
status : number | undefined ,
info : TrackClientInfo | undefined
}
2020-11-16 20:02:18 +00:00
export type RTCConnectionStatistics = {
videoBytesReceived : number ,
videoBytesSent : number ,
voiceBytesReceived : number ,
voiceBytesSent
}
2020-11-07 12:16:07 +00:00
export interface RTCConnectionEvents {
notify_state_changed : { oldState : RTPConnectionState , newState : RTPConnectionState } ,
notify_audio_assignment_changed : { track : RemoteRTPAudioTrack , info : TrackClientInfo | undefined } ,
notify_video_assignment_changed : { track : RemoteRTPVideoTrack , info : TrackClientInfo | undefined } ,
}
export class RTCConnection {
2020-11-15 22:28:00 +00:00
public static readonly kEnableSdpTrace = true ;
2020-11-28 18:33:54 +00:00
private readonly audioSupport : boolean ;
2020-11-07 12:16:07 +00:00
private readonly events : Registry < RTCConnectionEvents > ;
2020-11-29 13:42:02 +00:00
private readonly connection : AbstractServerConnection ;
2020-11-07 12:16:07 +00:00
private readonly commandHandler : CommandHandler ;
private readonly sdpProcessor : SdpProcessor ;
private connectionState : RTPConnectionState ;
2020-12-04 11:48:36 +00:00
private connectTimeout : number ;
2020-11-07 12:16:07 +00:00
private failedReason : string ;
2020-11-17 12:10:24 +00:00
private retryCalculator : RetryTimeCalculator ;
private retryTimestamp : number ;
2020-11-15 22:28:00 +00:00
private retryTimeout : number ;
2020-11-07 12:16:07 +00:00
private peer : RTCPeerConnection ;
private localCandidateCount : number ;
2020-12-05 21:21:14 +00:00
private peerRemoteDescriptionReceived : boolean ;
private cachedRemoteIceCandidates : { candidate : RTCIceCandidate , mediaLine : number } [ ] ;
2020-12-11 23:16:17 +00:00
private cachedRemoteSessionDescription : string ;
2020-11-07 12:16:07 +00:00
private currentTracks : { [ T in RTCSourceTrackType ] : MediaStreamTrack | undefined } = {
"audio-whisper" : undefined ,
"video-screen" : undefined ,
audio : undefined ,
video : undefined
} ;
private currentTransceiver : { [ T in RTCSourceTrackType ] : RTCRtpTransceiver | undefined } = {
"audio-whisper" : undefined ,
"video-screen" : undefined ,
audio : undefined ,
video : undefined
} ;
private remoteAudioTracks : { [ key : number ] : InternalRemoteRTPAudioTrack } ;
private remoteVideoTracks : { [ key : number ] : InternalRemoteRTPVideoTrack } ;
private temporaryStreams : { [ key : number ] : TemporaryRtpStream } = { } ;
2020-11-29 13:42:02 +00:00
constructor ( connection : AbstractServerConnection , audioSupport : boolean ) {
2020-11-07 12:16:07 +00:00
this . events = new Registry < RTCConnectionEvents > ( ) ;
this . connection = connection ;
this . sdpProcessor = new SdpProcessor ( ) ;
this . commandHandler = new CommandHandler ( connection , this , this . sdpProcessor ) ;
2020-11-17 12:10:24 +00:00
this . retryCalculator = new RetryTimeCalculator ( 5000 , 30000 , 10000 ) ;
2020-11-28 18:33:54 +00:00
this . audioSupport = audioSupport ;
2020-11-07 12:16:07 +00:00
this . connection . command_handler_boss ( ) . register_handler ( this . commandHandler ) ;
this . reset ( true ) ;
this . connection . events . on ( "notify_connection_state_changed" , event = > this . handleConnectionStateChanged ( event ) ) ;
2021-01-15 23:03:01 +00:00
( window as any ) . rtp = this ;
2020-11-07 12:16:07 +00:00
}
destroy() {
this . connection . command_handler_boss ( ) . unregister_handler ( this . commandHandler ) ;
}
2020-11-28 18:33:54 +00:00
isAudioEnabled ( ) : boolean {
return this . audioSupport ;
}
2020-11-29 13:42:02 +00:00
getConnection ( ) : AbstractServerConnection {
2020-11-07 12:16:07 +00:00
return this . connection ;
}
getEvents() {
return this . events ;
}
getConnectionState ( ) : RTPConnectionState {
return this . connectionState ;
}
getFailReason ( ) : string {
return this . failedReason ;
}
2020-11-17 12:10:24 +00:00
getRetryTimestamp ( ) : number | 0 {
return this . retryTimestamp ;
}
2020-11-25 22:41:32 +00:00
restartConnection() {
if ( this . connectionState === RTPConnectionState . DISCONNECTED ) {
/* We've been disconnected on purpose */
return ;
}
this . reset ( true ) ;
this . doInitialSetup ( ) ;
}
2020-11-07 12:16:07 +00:00
reset ( updateConnectionState : boolean ) {
2020-12-04 11:48:36 +00:00
logTrace ( LogCategory . WEBRTC , tr ( "Resetting the RTC connection (Updating connection state: %o)" ) , updateConnectionState ) ;
2020-11-07 12:16:07 +00:00
if ( this . peer ) {
if ( this . getConnection ( ) . connected ( ) ) {
this . getConnection ( ) . send_command ( "rtcsessionreset" ) . catch ( error = > {
logWarn ( LogCategory . WEBRTC , tr ( "Failed to signal RTC session reset to server: %o" ) , error ) ;
} ) ;
}
2020-12-04 11:48:36 +00:00
this . peer . onconnectionstatechange = undefined ;
this . peer . ondatachannel = undefined ;
this . peer . onicecandidate = undefined ;
this . peer . onicecandidateerror = undefined ;
this . peer . oniceconnectionstatechange = undefined ;
this . peer . onicegatheringstatechange = undefined ;
this . peer . onnegotiationneeded = undefined ;
this . peer . onsignalingstatechange = undefined ;
this . peer . onstatsended = undefined ;
this . peer . ontrack = undefined ;
2020-11-12 19:53:56 +00:00
2020-11-07 12:16:07 +00:00
this . peer . close ( ) ;
this . peer = undefined ;
}
2020-12-05 21:21:14 +00:00
this . peerRemoteDescriptionReceived = false ;
this . cachedRemoteIceCandidates = [ ] ;
2020-12-11 23:16:17 +00:00
this . cachedRemoteSessionDescription = undefined ;
2020-11-07 12:16:07 +00:00
2020-12-04 11:48:36 +00:00
clearTimeout ( this . connectTimeout ) ;
2020-11-07 12:16:07 +00:00
Object . keys ( this . currentTransceiver ) . forEach ( key = > this . currentTransceiver [ key ] = undefined ) ;
this . sdpProcessor . reset ( ) ;
if ( this . remoteAudioTracks ) {
Object . values ( this . remoteAudioTracks ) . forEach ( track = > track . destroy ( ) ) ;
}
this . remoteAudioTracks = { } ;
if ( this . remoteVideoTracks ) {
Object . values ( this . remoteVideoTracks ) . forEach ( track = > track . destroy ( ) ) ;
}
this . remoteVideoTracks = { } ;
this . temporaryStreams = { } ;
this . localCandidateCount = 0 ;
2020-11-15 22:28:00 +00:00
clearTimeout ( this . retryTimeout ) ;
this . retryTimeout = 0 ;
2020-11-17 12:10:24 +00:00
this . retryTimestamp = 0 ;
/ *
* We do not reset the retry timer here since we might get called when a fatal error occurs .
* Instead we 're resetting it every time we' ve changed the server connection state .
* /
/* this.retryCalculator.reset(); */
2020-11-15 22:28:00 +00:00
2020-11-07 12:16:07 +00:00
if ( updateConnectionState ) {
this . updateConnectionState ( RTPConnectionState . DISCONNECTED ) ;
}
}
2020-12-11 23:16:17 +00:00
async setTrackSource ( type : RTCSourceTrackType , source : MediaStreamTrack | null ) : Promise < MediaStreamTrack > {
2020-11-07 12:16:07 +00:00
switch ( type ) {
case "audio" :
case "audio-whisper" :
2020-11-28 18:33:54 +00:00
if ( ! this . audioSupport ) { throw tr ( "audio support isn't enabled" ) ; }
2020-11-07 12:16:07 +00:00
if ( source && source . kind !== "audio" ) { throw tr ( "invalid track type" ) ; }
break ;
case "video" :
case "video-screen" :
if ( source && source . kind !== "video" ) { throw tr ( "invalid track type" ) ; }
break ;
}
if ( this . currentTracks [ type ] === source ) {
return ;
}
2020-12-11 23:16:17 +00:00
const oldTrack = this . currentTracks [ type ] = source ;
2020-11-07 12:16:07 +00:00
await this . updateTracks ( ) ;
2020-12-11 23:16:17 +00:00
return oldTrack ;
2020-11-07 12:16:07 +00:00
}
2021-01-04 20:28:47 +00:00
public async startVideoBroadcast ( type : VideoBroadcastType , config : VideoBroadcastConfig ) {
let track : RTCBroadcastableTrackType ;
let broadcastType : number ;
switch ( type ) {
case "camera" :
broadcastType = 0 ;
track = "video" ;
break ;
case "screen" :
broadcastType = 1 ;
track = "video-screen" ;
break ;
default :
throw tr ( "invalid video broadcast type" ) ;
2020-11-07 12:16:07 +00:00
}
2021-01-04 20:28:47 +00:00
let payload = { } ;
payload [ "broadcast_keyframe_interval" ] = config . keyframeInterval ;
payload [ "broadcast_bitrate_max" ] = config . maxBandwidth ;
payload [ "ssrc" ] = this . sdpProcessor . getLocalSsrcFromFromMediaId ( this . currentTransceiver [ track ] . mid ) ;
payload [ "type" ] = broadcastType ;
try {
await this . connection . send_command ( "broadcastvideo" , payload ) ;
} catch ( error ) {
if ( error instanceof CommandResult ) {
if ( error . id === ErrorCode . SERVER_INSUFFICIENT_PERMISSIONS ) {
throw tr ( "failed on permission" ) + " " + this . connection . client . permissions . getFailedPermission ( error ) ;
2020-11-28 18:33:54 +00:00
}
2021-01-04 20:28:47 +00:00
error = error . formattedMessage ( ) ;
}
logError ( LogCategory . WEBRTC , tr ( "failed to start %s broadcast: %o" ) , type , error ) ;
throw tr ( "failed to signal broadcast start" ) ;
}
}
public async changeVideoBroadcastConfig ( type : VideoBroadcastType , config : VideoBroadcastConfig ) {
let track : RTCBroadcastableTrackType ;
let broadcastType : number ;
switch ( type ) {
case "camera" :
broadcastType = 0 ;
track = "video" ;
2020-11-28 18:33:54 +00:00
break ;
2021-01-04 20:28:47 +00:00
case "screen" :
broadcastType = 1 ;
track = "video-screen" ;
2020-11-28 18:33:54 +00:00
break ;
default :
2021-01-04 20:28:47 +00:00
throw tr ( "invalid video broadcast type" ) ;
}
let payload = { } ;
payload [ "broadcast_keyframe_interval" ] = config . keyframeInterval ;
payload [ "broadcast_bitrate_max" ] = config . maxBandwidth ;
payload [ "bt" ] = broadcastType ;
try {
await this . connection . send_command ( "broadcastvideoconfigure" , payload ) ;
} catch ( error ) {
if ( error instanceof CommandResult ) {
if ( error . id === ErrorCode . SERVER_INSUFFICIENT_PERMISSIONS ) {
throw tr ( "failed on permission" ) + " " + this . connection . client . permissions . getFailedPermission ( error ) ;
}
error = error . formattedMessage ( ) ;
}
logError ( LogCategory . WEBRTC , tr ( "failed to update %s broadcast: %o" ) , type , error ) ;
throw tr ( "failed to update broadcast config" ) ;
2020-11-28 18:33:54 +00:00
}
2021-01-04 20:28:47 +00:00
}
2020-11-28 18:33:54 +00:00
2021-01-04 20:28:47 +00:00
public async startAudioBroadcast() {
2020-11-07 12:16:07 +00:00
try {
2021-01-04 20:28:47 +00:00
await this . connection . send_command ( "broadcastaudio" , {
ssrc : this.sdpProcessor.getLocalSsrcFromFromMediaId ( this . currentTransceiver [ "audio" ] . mid )
2020-11-07 12:16:07 +00:00
} ) ;
} catch ( error ) {
2021-01-04 20:28:47 +00:00
logError ( LogCategory . WEBRTC , tr ( "failed to start %s broadcast: %o" ) , "audio" , error ) ;
2020-11-07 12:16:07 +00:00
throw tr ( "failed to signal broadcast start" ) ;
}
}
2020-11-28 11:58:22 +00:00
public async startWhisper ( target : WhisperTarget ) : Promise < void > {
2020-11-28 18:33:54 +00:00
if ( ! this . audioSupport ) {
throw tr ( "audio support isn't enabled" ) ;
}
2020-11-28 11:58:22 +00:00
const transceiver = this . currentTransceiver [ "audio-whisper" ] ;
if ( typeof transceiver === "undefined" ) {
throw tr ( "missing transceiver" ) ;
}
if ( target . target === "echo" ) {
await this . connection . send_command ( "whispersessioninitialize" , {
ssrc : this.sdpProcessor.getLocalSsrcFromFromMediaId ( transceiver . mid ) ,
type : 0x10 , /* self */
target : 0 ,
id : 0
} , { flagset : [ "new" ] } ) ;
} else if ( target . target === "channel-clients" ) {
throw "target not yet supported" ;
} else if ( target . target === "groups" ) {
throw "target not yet supported" ;
} else {
throw "target not yet supported" ;
}
}
2020-11-07 12:16:07 +00:00
public stopTrackBroadcast ( type : RTCBroadcastableTrackType ) {
2021-01-04 20:28:47 +00:00
let promise : Promise < any > ;
switch ( type ) {
case "audio" :
promise = this . connection . send_command ( "broadcastaudio" , {
ssrc : 0
} ) ;
break ;
case "video-screen" :
promise = this . connection . send_command ( "broadcastvideo" , {
type : 1 ,
ssrc : 0
} ) ;
break ;
case "video" :
promise = this . connection . send_command ( "broadcastvideo" , {
type : 0 ,
ssrc : 0
} ) ;
break ;
}
promise . catch ( error = > {
2020-11-07 12:16:07 +00:00
logWarn ( LogCategory . WEBRTC , tr ( "Failed to signal track broadcast stop: %o" ) , error ) ;
} ) ;
}
2020-11-17 13:27:46 +00:00
public setNotSupported() {
this . reset ( false ) ;
this . updateConnectionState ( RTPConnectionState . NOT_SUPPORTED ) ;
}
2020-11-07 12:16:07 +00:00
private updateConnectionState ( newState : RTPConnectionState ) {
if ( this . connectionState === newState ) { return ; }
const oldState = this . connectionState ;
this . connectionState = newState ;
this . events . fire ( "notify_state_changed" , { oldState : oldState , newState : newState } ) ;
}
2020-11-17 12:10:24 +00:00
private handleFatalError ( error : string , allowRetry : boolean ) {
2020-11-07 12:16:07 +00:00
this . reset ( false ) ;
this . failedReason = error ;
this . updateConnectionState ( RTPConnectionState . FAILED ) ;
2020-11-17 12:10:24 +00:00
const log = this . connection . client . log ;
if ( allowRetry ) {
const time = this . retryCalculator . calculateRetryTime ( ) ;
if ( time > 0 ) {
this . retryTimestamp = Date . now ( ) + time ;
this . retryTimeout = setTimeout ( ( ) = > {
this . doInitialSetup ( ) ;
} , time ) ;
log . log ( "webrtc.fatal.error" , {
message : error ,
retryTimeout : time
} ) ;
} else {
allowRetry = false ;
}
}
if ( ! allowRetry ) {
log . log ( "webrtc.fatal.error" , {
message : error ,
retryTimeout : 0
} ) ;
2020-11-07 12:16:07 +00:00
}
}
private static checkBrowserSupport() {
if ( ! window . RTCRtpSender || ! RTCRtpSender . prototype ) {
throw tr ( "Missing RTCRtpSender" ) ;
}
if ( ! RTCRtpSender . prototype . getParameters ) {
throw tr ( "RTCRtpSender.getParameters" ) ;
}
if ( ! RTCRtpSender . prototype . replaceTrack ) {
throw tr ( "RTCRtpSender.getParameters" ) ;
}
}
2020-11-17 13:27:46 +00:00
public doInitialSetup() {
2020-11-25 22:41:32 +00:00
if ( ! ( "RTCPeerConnection" in window ) ) {
2020-11-17 12:10:24 +00:00
this . handleFatalError ( tr ( "WebRTC has been disabled (RTCPeerConnection is not defined)" ) , false ) ;
2020-11-12 19:53:56 +00:00
return ;
}
2020-11-25 22:41:32 +00:00
if ( ! ( "addTransceiver" in RTCPeerConnection . prototype ) ) {
this . handleFatalError ( tr ( "WebRTC api incompatible (RTCPeerConnection.addTransceiver missing)" ) , false ) ;
return ;
}
2020-11-07 12:16:07 +00:00
this . peer = new RTCPeerConnection ( {
bundlePolicy : "max-bundle" ,
rtcpMuxPolicy : "require" ,
iceServers : [ { urls : [ "stun:stun.l.google.com:19302" , "stun:stun1.l.google.com:19302" ] } ]
} ) ;
2020-11-28 18:33:54 +00:00
if ( this . audioSupport ) {
this . currentTransceiver [ "audio" ] = this . peer . addTransceiver ( "audio" ) ;
this . currentTransceiver [ "audio-whisper" ] = this . peer . addTransceiver ( "audio" ) ;
2020-11-07 12:16:07 +00:00
2021-02-15 14:53:01 +00:00
if ( window . detectedBrowser . name === "firefox" ) {
/ *
* For some reason FF ( <= 85.0 ) does not replay any audio from extra added transceivers .
* On the other hand , if the server is creating that track or we ' re using it for sending audio as well
* it works . So we just wait for the server to come up with new streams ( even though we need to renegotiate . . . ) .
* For Chrome we only need to negotiate once in most cases .
* Side note : This does not apply to video channels !
* /
} else {
/* add some other transceivers for later use */
for ( let i = 0 ; i < settings . getValue ( Settings . KEY_RTC_EXTRA_AUDIO_CHANNELS ) ; i ++ ) {
this . peer . addTransceiver ( "audio" , { direction : "recvonly" } ) ;
}
2020-11-28 18:33:54 +00:00
}
}
2020-11-07 12:16:07 +00:00
this . currentTransceiver [ "video" ] = this . peer . addTransceiver ( "video" ) ;
this . currentTransceiver [ "video-screen" ] = this . peer . addTransceiver ( "video" ) ;
/* add some other transceivers for later use */
2021-02-15 14:53:01 +00:00
for ( let i = 0 ; i < settings . getValue ( Settings . KEY_RTC_EXTRA_VIDEO_CHANNELS ) ; i ++ ) {
this . peer . addTransceiver ( "video" , { direction : "recvonly" } ) ;
2020-11-07 12:16:07 +00:00
}
2020-12-05 21:21:14 +00:00
this . peer . onicecandidate = event = > this . handleLocalIceCandidate ( event . candidate ) ;
2020-11-07 12:16:07 +00:00
this . peer . onicecandidateerror = event = > this . handleIceCandidateError ( event ) ;
this . peer . oniceconnectionstatechange = ( ) = > this . handleIceConnectionStateChanged ( ) ;
this . peer . onicegatheringstatechange = ( ) = > this . handleIceGatheringStateChanged ( ) ;
this . peer . onsignalingstatechange = ( ) = > this . handleSignallingStateChanged ( ) ;
this . peer . onconnectionstatechange = ( ) = > this . handlePeerConnectionStateChanged ( ) ;
this . peer . ondatachannel = event = > this . handleDataChannel ( event . channel ) ;
this . peer . ontrack = event = > this . handleTrack ( event ) ;
this . updateConnectionState ( RTPConnectionState . CONNECTING ) ;
this . doInitialSetup0 ( ) . catch ( error = > {
2020-11-17 12:10:24 +00:00
this . handleFatalError ( tr ( "initial setup failed" ) , true ) ;
2020-11-07 12:16:07 +00:00
logError ( LogCategory . WEBRTC , tr ( "Connection setup failed: %o" ) , error ) ;
} ) ;
}
private async updateTracks() {
for ( const type of kRtcSourceTrackTypes ) {
2020-11-28 14:41:44 +00:00
if ( ! this . currentTransceiver [ type ] ? . sender ) {
continue ;
}
2020-11-15 22:28:00 +00:00
let fallback ;
switch ( type ) {
case "audio" :
case "audio-whisper" :
fallback = getIdleTrack ( "audio" ) ;
break ;
case "video" :
case "video-screen" :
fallback = getIdleTrack ( "video" ) ;
break ;
}
2020-11-28 14:41:44 +00:00
let target = this . currentTracks [ type ] || fallback ;
if ( this . currentTransceiver [ type ] . sender . track === target ) {
continue ;
}
await this . currentTransceiver [ type ] . sender . replaceTrack ( target ) ;
2020-12-16 21:06:46 +00:00
/* Firefox has some crazy issues */
if ( window . detectedBrowser . name !== "firefox" ) {
if ( target ) {
2021-01-10 16:36:57 +00:00
logTrace ( LogCategory . NETWORKING , "Setting sendrecv from %o" , this . currentTransceiver [ type ] . direction , this . currentTransceiver [ type ] . currentDirection ) ;
2020-12-16 21:06:46 +00:00
this . currentTransceiver [ type ] . direction = "sendrecv" ;
} else if ( type === "video" || type === "video-screen" ) {
/ *
* We don 't need to stop & start the audio transceivers every time we' re toggling the stream state .
* This would be a much overall cost than just keeping it going .
*
* The video streams instead are not toggling that much and since they split up the bandwidth between them ,
* we 've to shut them down if they' re no needed . This not only allows the one stream to take full advantage
* of the bandwidth it also reduces resource usage .
* /
//this.currentTransceiver[type].direction = "recvonly";
}
2020-12-11 23:16:17 +00:00
}
2020-11-28 18:33:54 +00:00
logTrace ( LogCategory . WEBRTC , "Replaced track for %o (Fallback: %o)" , type , target === fallback ) ;
2020-11-07 12:16:07 +00:00
}
}
private async doInitialSetup0() {
RTCConnection . checkBrowserSupport ( ) ;
const peer = this . peer ;
await this . updateTracks ( ) ;
2020-11-15 22:28:00 +00:00
2020-11-28 18:33:54 +00:00
const offer = await peer . createOffer ( { iceRestart : false , offerToReceiveAudio : this.audioSupport , offerToReceiveVideo : true } ) ;
2020-11-07 12:16:07 +00:00
if ( offer . type !== "offer" ) { throw tr ( "created ofer isn't of type offer" ) ; }
if ( this . peer !== peer ) { return ; }
if ( RTCConnection . kEnableSdpTrace ) {
2020-12-12 13:18:50 +00:00
const gr = logGroupNative ( LogType . TRACE , LogCategory . WEBRTC , tra ( "Original initial local SDP (offer)" ) ) ;
gr . collapsed ( true ) ;
gr . log ( "%s" , offer . sdp ) ;
gr . end ( ) ;
2020-11-07 12:16:07 +00:00
}
try {
offer . sdp = this . sdpProcessor . processOutgoingSdp ( offer . sdp , "offer" ) ;
2020-12-12 13:18:50 +00:00
const gr = logGroupNative ( LogType . TRACE , LogCategory . WEBRTC , tra ( "Patched initial local SDP (offer)" ) ) ;
gr . collapsed ( true ) ;
gr . log ( "%s" , offer . sdp ) ;
gr . end ( ) ;
2020-11-07 12:16:07 +00:00
} catch ( error ) {
logError ( LogCategory . WEBRTC , tr ( "Failed to preprocess outgoing initial offer: %o" ) , error ) ;
2020-11-17 12:10:24 +00:00
this . handleFatalError ( tr ( "Failed to preprocess outgoing initial offer" ) , true ) ;
2020-11-07 12:16:07 +00:00
return ;
}
await peer . setLocalDescription ( offer ) ;
if ( this . peer !== peer ) { return ; }
try {
await this . connection . send_command ( "rtcsessiondescribe" , {
mode : "offer" ,
sdp : offer.sdp
} ) ;
} catch ( error ) {
if ( this . peer !== peer ) { return ; }
if ( error instanceof CommandResult ) {
2020-11-17 13:27:46 +00:00
if ( error . id === ErrorCode . COMMAND_NOT_FOUND ) {
this . setNotSupported ( ) ;
return ;
}
2020-11-07 12:16:07 +00:00
error = error . formattedMessage ( ) ;
}
logWarn ( LogCategory . VOICE , tr ( "Failed to initialize RTP connection: %o" ) , error ) ;
throw tr ( "server failed to accept our offer" ) ;
}
if ( this . peer !== peer ) { return ; }
this . peer . onnegotiationneeded = ( ) = > this . handleNegotiationNeeded ( ) ;
2020-12-04 11:48:36 +00:00
this . connectTimeout = setTimeout ( ( ) = > {
this . handleFatalError ( "Connection initialize timeout" , true ) ;
} , 10 _000 ) ;
2020-11-07 12:16:07 +00:00
/* Nothing left to do. Server should send a notifyrtcsessiondescription with mode answer */
}
private handleConnectionStateChanged ( event : ServerConnectionEvents [ "notify_connection_state_changed" ] ) {
if ( event . newState === ConnectionState . CONNECTED ) {
2020-11-17 13:27:46 +00:00
/* will be called by the server connection handler */
2020-11-07 12:16:07 +00:00
} else {
this . reset ( true ) ;
2020-12-05 21:59:46 +00:00
this . retryCalculator . reset ( ) ;
2020-11-07 12:16:07 +00:00
}
}
2020-12-05 21:21:14 +00:00
private handleLocalIceCandidate ( candidate : RTCIceCandidate | undefined ) {
2020-11-07 12:16:07 +00:00
if ( candidate ) {
2020-11-15 22:28:00 +00:00
if ( candidate . address ? . endsWith ( ".local" ) ) {
logTrace ( LogCategory . WEBRTC , tr ( "Skipping local fqdn ICE candidate %s" ) , candidate . toJSON ( ) . candidate ) ;
return ;
}
2020-11-17 12:29:36 +00:00
this . localCandidateCount ++ ;
2020-11-07 12:16:07 +00:00
const json = candidate . toJSON ( ) ;
2020-11-15 22:28:00 +00:00
logTrace ( LogCategory . WEBRTC , tr ( "Received local ICE candidate %s" ) , json . candidate ) ;
2020-11-07 12:16:07 +00:00
this . connection . send_command ( "rtcicecandidate" , {
media_line : json.sdpMLineIndex ,
candidate : json.candidate
} ) . catch ( error = > {
logWarn ( LogCategory . WEBRTC , tr ( "Failed to transmit local ICE candidate to server: %o" ) , error ) ;
} ) ;
} else {
if ( this . localCandidateCount === 0 ) {
2020-11-17 12:10:24 +00:00
logError ( LogCategory . WEBRTC , tr ( "Received local ICE candidate finish, without having any candidates" ) ) ;
this . handleFatalError ( tr ( "Failed to gather any local ICE candidates." ) , false ) ;
2020-11-07 12:16:07 +00:00
return ;
} else {
2020-11-15 22:28:00 +00:00
logTrace ( LogCategory . WEBRTC , tr ( "Received local ICE candidate finish" ) ) ;
2020-11-07 12:16:07 +00:00
}
this . connection . send_command ( "rtcicecandidate" , { } ) . catch ( error = > {
logWarn ( LogCategory . WEBRTC , tr ( "Failed to transmit local ICE candidate finish to server: %o" ) , error ) ;
} ) ;
}
}
2020-12-05 21:21:14 +00:00
public handleRemoteIceCandidate ( candidate : RTCIceCandidate | undefined , mediaLine : number ) {
if ( ! this . peer ) {
logWarn ( LogCategory . WEBRTC , tr ( "Received remote ICE candidate without an active peer. Dropping candidate." ) ) ;
return ;
}
if ( ! this . peerRemoteDescriptionReceived ) {
logTrace ( LogCategory . WEBRTC , tr ( "Received remote ICE candidate but haven't yet received a remote description. Caching the candidate." ) ) ;
this . cachedRemoteIceCandidates . push ( { mediaLine : mediaLine , candidate : candidate } ) ;
return ;
}
if ( ! candidate ) {
/* candidates finished */
} else {
this . peer . addIceCandidate ( candidate ) . then ( ( ) = > {
logTrace ( LogCategory . WEBRTC , tr ( "Successfully added a remote ice candidate for media line %d: %s" ) , mediaLine , candidate . candidate ) ;
} ) . catch ( error = > {
logWarn ( LogCategory . WEBRTC , tr ( "Failed to add a remote ice candidate for media line %d: %o (Candidate: %s)" ) , mediaLine , error , candidate . candidate ) ;
} ) ;
}
}
public applyCachedRemoteIceCandidates() {
for ( const { candidate , mediaLine } of this . cachedRemoteIceCandidates ) {
this . handleRemoteIceCandidate ( candidate , mediaLine ) ;
}
2020-12-12 12:19:04 +00:00
this . handleRemoteIceCandidate ( undefined , 0 ) ;
2020-12-05 21:21:14 +00:00
this . cachedRemoteIceCandidates = [ ] ;
}
2020-11-07 12:16:07 +00:00
private handleIceCandidateError ( event : RTCPeerConnectionIceErrorEvent ) {
if ( this . peer . iceGatheringState === "gathering" ) {
2021-01-10 16:36:57 +00:00
logWarn ( LogCategory . WEBRTC , tr ( "Received error while gathering the ice candidates: %d/%s for %s (url: %s)" ) ,
2020-11-07 12:16:07 +00:00
event . errorCode , event . errorText , event . hostCandidate , event . url ) ;
} else {
2021-01-10 16:36:57 +00:00
logTrace ( LogCategory . WEBRTC , tr ( "Ice candidate %s (%s) errored: %d/%s" ) ,
2020-11-07 12:16:07 +00:00
event . url , event . hostCandidate , event . errorCode , event . errorText ) ;
}
}
private handleIceConnectionStateChanged() {
2021-01-10 16:36:57 +00:00
logTrace ( LogCategory . WEBRTC , tr ( "ICE connection state changed to %s" ) , this . peer . iceConnectionState ) ;
2020-11-07 12:16:07 +00:00
}
private handleIceGatheringStateChanged() {
2021-01-10 16:36:57 +00:00
logTrace ( LogCategory . WEBRTC , tr ( "ICE gathering state changed to %s" ) , this . peer . iceGatheringState ) ;
2020-11-07 12:16:07 +00:00
}
private handleSignallingStateChanged() {
logTrace ( LogCategory . WEBRTC , tr ( "Peer signalling state changed to %s" ) , this . peer . signalingState ) ;
}
private handleNegotiationNeeded() {
2020-12-11 23:16:17 +00:00
logWarn ( LogCategory . WEBRTC , tr ( "Local peer needs negotiation, but that's not supported that." ) ) ;
2020-11-07 12:16:07 +00:00
}
private handlePeerConnectionStateChanged() {
logTrace ( LogCategory . WEBRTC , tr ( "Peer connection state changed to %s" ) , this . peer . connectionState ) ;
switch ( this . peer . connectionState ) {
case "connecting" :
2020-11-17 12:10:24 +00:00
this . updateConnectionState ( RTPConnectionState . CONNECTING ) ;
2020-11-07 12:16:07 +00:00
break ;
case "connected" :
2020-12-04 11:48:36 +00:00
clearTimeout ( this . connectTimeout ) ;
2020-11-17 12:10:24 +00:00
this . retryCalculator . reset ( ) ;
2020-11-07 12:16:07 +00:00
this . updateConnectionState ( RTPConnectionState . CONNECTED ) ;
break ;
case "failed" :
2020-11-16 20:02:18 +00:00
if ( this . connectionState !== RTPConnectionState . FAILED ) {
2020-11-17 12:10:24 +00:00
this . handleFatalError ( tr ( "peer connection failed" ) , true ) ;
2020-11-16 20:02:18 +00:00
}
break ;
2020-11-07 12:16:07 +00:00
case "closed" :
case "disconnected" :
case "new" :
if ( this . connectionState !== RTPConnectionState . FAILED ) {
this . updateConnectionState ( RTPConnectionState . DISCONNECTED ) ;
}
break ;
}
}
private handleDataChannel ( _channel : RTCDataChannel ) {
/* We're not doing anything with data channels */
}
private releaseTemporaryStream ( ssrc : number ) : TemporaryRtpStream | undefined {
if ( this . temporaryStreams [ ssrc ] ) {
const stream = this . temporaryStreams [ ssrc ] ;
clearTimeout ( stream . timeoutId ) ;
stream . timeoutId = 0 ;
delete this . temporaryStreams [ ssrc ] ;
return stream ;
}
return undefined ;
}
private handleTrack ( event : RTCTrackEvent ) {
const ssrc = this . sdpProcessor . getRemoteSsrcFromFromMediaId ( event . transceiver . mid ) ;
if ( typeof ssrc !== "number" ) {
logError ( LogCategory . WEBRTC , tr ( "Received track without knowing its ssrc. Ignoring track..." ) ) ;
return ;
}
const tempInfo = this . releaseTemporaryStream ( ssrc ) ;
if ( event . track . kind === "audio" ) {
2020-11-28 18:33:54 +00:00
if ( ! this . audioSupport ) {
logWarn ( LogCategory . WEBRTC , tr ( "Received remote audio track %d but audio has been disabled. Dropping track." ) , ssrc ) ;
return ;
}
2020-12-16 21:06:46 +00:00
2020-11-07 12:16:07 +00:00
const track = new InternalRemoteRTPAudioTrack ( ssrc , event . transceiver ) ;
2020-12-16 21:06:46 +00:00
logDebug ( LogCategory . WEBRTC , tr ( "Received remote audio track on ssrc %o" ) , ssrc ) ;
2020-11-07 12:16:07 +00:00
if ( tempInfo ? . info !== undefined ) {
track . handleAssignment ( tempInfo . info ) ;
this . events . fire ( "notify_audio_assignment_changed" , {
info : tempInfo.info ,
track : track
} ) ;
}
if ( tempInfo ? . status !== undefined ) {
track . handleStateNotify ( tempInfo . status , tempInfo . info ) ;
}
this . remoteAudioTracks [ ssrc ] = track ;
} else if ( event . track . kind === "video" ) {
const track = new InternalRemoteRTPVideoTrack ( ssrc , event . transceiver ) ;
2020-12-16 21:06:46 +00:00
logDebug ( LogCategory . WEBRTC , tr ( "Received remote video track on ssrc %o" ) , ssrc ) ;
2020-11-07 12:16:07 +00:00
if ( tempInfo ? . info !== undefined ) {
track . handleAssignment ( tempInfo . info ) ;
this . events . fire ( "notify_video_assignment_changed" , {
info : tempInfo.info ,
track : track
} ) ;
}
if ( tempInfo ? . status !== undefined ) {
track . handleStateNotify ( tempInfo . status , tempInfo . info ) ;
}
this . remoteVideoTracks [ ssrc ] = track ;
} else {
logWarn ( LogCategory . WEBRTC , tr ( "Received track with unknown kind '%s'." ) , event . track . kind ) ;
}
}
private getOrCreateTempStream ( ssrc : number ) : TemporaryRtpStream {
if ( this . temporaryStreams [ ssrc ] ) {
return this . temporaryStreams [ ssrc ] ;
}
const tempStream = this . temporaryStreams [ ssrc ] = {
ssrc : ssrc ,
timeoutId : 0 ,
createTimestamp : Date.now ( ) ,
info : undefined ,
status : undefined
} ;
tempStream . timeoutId = setTimeout ( ( ) = > {
logWarn ( LogCategory . WEBRTC , tr ( "Received stream mapping for invalid stream which hasn't been signalled after 5 seconds (ssrc: %o)." ) , ssrc ) ;
delete this . temporaryStreams [ ssrc ] ;
} , 5000 ) ;
return tempStream ;
}
private doMapStream ( ssrc : number , target : TrackClientInfo | undefined ) {
if ( this . remoteAudioTracks [ ssrc ] ) {
const track = this . remoteAudioTracks [ ssrc ] ;
track . handleAssignment ( target ) ;
this . events . fire ( "notify_audio_assignment_changed" , {
info : target ,
track : track
} ) ;
} else if ( this . remoteVideoTracks [ ssrc ] ) {
const track = this . remoteVideoTracks [ ssrc ] ;
track . handleAssignment ( target ) ;
this . events . fire ( "notify_video_assignment_changed" , {
info : target ,
track : track
} ) ;
} else {
let tempStream = this . getOrCreateTempStream ( ssrc ) ;
tempStream . info = target ;
}
}
private handleStreamState ( ssrc : number , state : number , info : TrackClientInfo | undefined ) {
if ( this . remoteAudioTracks [ ssrc ] ) {
const track = this . remoteAudioTracks [ ssrc ] ;
track . handleStateNotify ( state , info ) ;
} else if ( this . remoteVideoTracks [ ssrc ] ) {
const track = this . remoteVideoTracks [ ssrc ] ;
track . handleStateNotify ( state , info ) ;
} else {
let tempStream = this . getOrCreateTempStream ( ssrc ) ;
2020-11-22 18:08:19 +00:00
if ( typeof info . media === "undefined" ) {
/* the media will only be send on stream assignments, not on stream state changes */
info . media = tempStream . info ? . media ;
}
2020-11-07 12:16:07 +00:00
tempStream . info = info ;
tempStream . status = state ;
}
}
2020-11-16 20:02:18 +00:00
async getConnectionStatistics ( ) : Promise < RTCConnectionStatistics > {
try {
if ( ! this . peer ) {
throw "missing peer" ;
}
const statisticsInfo = await this . peer . getStats ( ) ;
const statistics = [ . . . statisticsInfo . entries ( ) ] . map ( e = > e [ 1 ] ) as RTCStats [ ] ;
const inboundStreams = statistics . filter ( e = > e . type . replace ( /-/ , "" ) === "inboundrtp" && 'bytesReceived' in e ) as any [ ] ;
const outboundStreams = statistics . filter ( e = > e . type . replace ( /-/ , "" ) === "outboundrtp" && 'bytesSent' in e ) as any [ ] ;
return {
voiceBytesSent : outboundStreams.filter ( e = > e . mediaType === "audio" ) . reduce ( ( a , b ) = > a + b . bytesSent , 0 ) ,
voiceBytesReceived : inboundStreams.filter ( e = > e . mediaType === "audio" ) . reduce ( ( a , b ) = > a + b . bytesReceived , 0 ) ,
videoBytesSent : outboundStreams.filter ( e = > e . mediaType === "video" ) . reduce ( ( a , b ) = > a + b . bytesSent , 0 ) ,
videoBytesReceived : inboundStreams.filter ( e = > e . mediaType === "video" ) . reduce ( ( a , b ) = > a + b . bytesReceived , 0 )
}
} catch ( error ) {
logWarn ( LogCategory . WEBRTC , tr ( "Failed to calculate connection statistics: %o" ) , error ) ;
return {
videoBytesReceived : 0 ,
videoBytesSent : 0 ,
voiceBytesReceived : 0 ,
voiceBytesSent : 0
} ;
}
}
2020-11-22 12:48:15 +00:00
async getVideoBroadcastStatistics ( type : RTCBroadcastableTrackType ) : Promise < RtcVideoBroadcastStatistics | undefined > {
if ( ! this . currentTransceiver [ type ] ? . sender ) { return undefined ; }
const senderStatistics = new RTCStatsWrapper ( ( ) = > this . currentTransceiver [ type ] . sender . getStats ( ) ) ;
await senderStatistics . initialize ( ) ;
if ( senderStatistics . getValues ( ) . length === 0 ) { return undefined ; }
const trackSettings = this . currentTransceiver [ type ] . sender . track ? . getSettings ( ) || { } ;
const result = { } as RtcVideoBroadcastStatistics ;
const outboundStream = senderStatistics . getStatisticByType ( "outboundrtp" ) ;
/* only available in chrome */
if ( "codecId" in outboundStream ) {
if ( typeof outboundStream . codecId !== "string" ) { throw tr ( "invalid codec id type" ) ; }
if ( senderStatistics [ outboundStream . codecId ] ? . type !== "codec" ) { throw tra ( "invalid/missing codec statistic for codec {}" , outboundStream . codecId ) ; }
const codecInfo = senderStatistics [ outboundStream . codecId ] ;
if ( typeof codecInfo . mimeType !== "string" ) { throw tr ( "codec statistic missing mine type" ) ; }
if ( typeof codecInfo . payloadType !== "number" ) { throw tr ( "codec statistic has invalid payloadType type" ) ; }
result . codec = {
name : codecInfo.mimeType.startsWith ( "video/" ) ? codecInfo . mimeType . substr ( 6 ) : codecInfo . mimeType || tr ( "unknown" ) ,
payloadType : codecInfo.payloadType
} ;
} else {
/* TODO: Get the only one video type from the sdp */
}
if ( "frameWidth" in outboundStream && "frameHeight" in outboundStream ) {
if ( typeof outboundStream . frameWidth !== "number" ) { throw tr ( "invalid frameWidth attribute of outboundrtp statistic" ) ; }
if ( typeof outboundStream . frameHeight !== "number" ) { throw tr ( "invalid frameHeight attribute of outboundrtp statistic" ) ; }
result . dimensions = {
width : outboundStream.frameWidth ,
height : outboundStream.frameHeight
} ;
} else if ( "height" in trackSettings && "width" in trackSettings ) {
result . dimensions = {
height : trackSettings.height ,
width : trackSettings.width
} ;
} else {
result . dimensions = {
width : 0 ,
height : 0
} ;
}
if ( "framesPerSecond" in outboundStream ) {
if ( typeof outboundStream . framesPerSecond !== "number" ) { throw tr ( "invalid framesPerSecond attribute of outboundrtp statistic" ) ; }
result . frameRate = outboundStream . framesPerSecond ;
} else if ( "frameRate" in trackSettings ) {
result . frameRate = trackSettings . frameRate ;
} else {
result . frameRate = 0 ;
}
if ( "qualityLimitationReason" in outboundStream ) {
/* TODO: verify the value? */
if ( typeof outboundStream . qualityLimitationReason !== "string" ) { throw tr ( "invalid qualityLimitationReason attribute of outboundrtp statistic" ) ; }
result . qualityLimitation = outboundStream . qualityLimitationReason as any ;
} else {
result . qualityLimitation = "none" ;
}
if ( "mediaSourceId" in outboundStream ) {
if ( typeof outboundStream . mediaSourceId !== "string" ) { throw tr ( "invalid media source type" ) ; }
if ( senderStatistics [ outboundStream . mediaSourceId ] ? . type !== "media-source" ) { throw tra ( "invalid/missing media source statistic for source {}" , outboundStream . mediaSourceId ) ; }
const source = senderStatistics [ outboundStream . mediaSourceId ] ;
if ( typeof source . width !== "number" ) { throw tr ( "invalid width attribute of media-source statistic" ) ; }
if ( typeof source . height !== "number" ) { throw tr ( "invalid height attribute of media-source statistic" ) ; }
if ( typeof source . framesPerSecond !== "number" ) { throw tr ( "invalid framesPerSecond attribute of media-source statistic" ) ; }
result . source = {
dimensions : { height : source.height , width : source.width } ,
frameRate : source.framesPerSecond
} ;
} else {
result . source = {
dimensions : { width : 0 , height : 0 } ,
frameRate : 0
} ;
if ( "height" in trackSettings && "width" in trackSettings ) {
result . source . dimensions = {
height : trackSettings.height ,
width : trackSettings.width
} ;
}
if ( "frameRate" in trackSettings ) {
result . source . frameRate = trackSettings . frameRate ;
}
}
return result ;
}
2020-11-07 12:16:07 +00:00
}