UNPKG

@100mslive/hms-video-store

Version:

@100mslive Core SDK which abstracts the complexities of webRTC while providing a reactive store for data management with a unidirectional data flow

215 lines (187 loc) • 7.98 kB
import IConnectionObserver, { RTCIceCandidatePair } from './IConnectionObserver'; import { HMSConnectionRole } from './model'; import { ErrorFactory } from '../error/ErrorFactory'; import { HMSAction } from '../error/HMSAction'; import { HMSAudioTrackSettings, HMSVideoTrackSettings } from '../media/settings'; import { HMSLocalTrack, HMSLocalVideoTrack } from '../media/tracks'; import { TrackState } from '../notification-manager'; import JsonRpcSignal from '../signal/jsonrpc'; import HMSLogger from '../utils/logger'; import { enableOpusDtx, fixMsid } from '../utils/session-description'; const TAG = '[HMSConnection]'; export default abstract class HMSConnection { readonly role: HMSConnectionRole; protected readonly signal: JsonRpcSignal; protected abstract readonly observer: IConnectionObserver; abstract readonly nativeConnection: RTCPeerConnection; /** * We keep a list of pending IceCandidates received * from the signalling server. When the peer-connection * is initialized we call [addIceCandidate] for each. * * WARN: * - [HMSPublishConnection] keeps the complete list of candidates (for * ice-connection failed/disconnect) forever. * - [HMSSubscribeConnection] clears this list as soon as we call [addIceCandidate] */ readonly candidates = new Array<RTCIceCandidateInit>(); // @ts-ignore sfuNodeId?: string; selectedCandidatePair?: RTCIceCandidatePair; protected constructor(role: HMSConnectionRole, signal: JsonRpcSignal) { this.role = role; this.signal = signal; } public get iceConnectionState(): RTCIceConnectionState { return this.nativeConnection.iceConnectionState; } public get connectionState(): RTCPeerConnectionState { return this.nativeConnection.connectionState; } private get action() { return this.role === HMSConnectionRole.Publish ? HMSAction.PUBLISH : HMSAction.SUBSCRIBE; } setSfuNodeId(nodeId?: string) { this.sfuNodeId = nodeId; } addTransceiver(track: MediaStreamTrack, init: RTCRtpTransceiverInit): RTCRtpTransceiver { return this.nativeConnection.addTransceiver(track, init); } async createOffer(tracks?: Map<string, TrackState>, options?: RTCOfferOptions): Promise<RTCSessionDescriptionInit> { try { const offer = await this.nativeConnection.createOffer(options); HMSLogger.d(TAG, `[role=${this.role}] createOffer offer=${JSON.stringify(offer, null, 1)}`); return enableOpusDtx(fixMsid(offer, tracks)); } catch (error) { throw ErrorFactory.WebrtcErrors.CreateOfferFailed(this.action, (error as Error).message); } } async createAnswer(options: RTCOfferOptions | undefined = undefined): Promise<RTCSessionDescriptionInit> { try { const answer = await this.nativeConnection.createAnswer(options); HMSLogger.d(TAG, `[role=${this.role}] createAnswer answer=${JSON.stringify(answer, null, 1)}`); return answer; } catch (error) { throw ErrorFactory.WebrtcErrors.CreateAnswerFailed(this.action, (error as Error).message); } } async setLocalDescription(description: RTCSessionDescriptionInit): Promise<void> { try { HMSLogger.d(TAG, `[role=${this.role}] setLocalDescription description=${JSON.stringify(description, null, 1)}`); await this.nativeConnection.setLocalDescription(description); } catch (error) { throw ErrorFactory.WebrtcErrors.SetLocalDescriptionFailed(this.action, (error as Error).message); } } async setRemoteDescription(description: RTCSessionDescriptionInit): Promise<void> { try { HMSLogger.d(TAG, `[role=${this.role}] setRemoteDescription description=${JSON.stringify(description, null, 1)}`); await this.nativeConnection.setRemoteDescription(description); } catch (error) { throw ErrorFactory.WebrtcErrors.SetRemoteDescriptionFailed(this.action, (error as Error).message); } } async addIceCandidate(candidate: RTCIceCandidateInit): Promise<void> { if (this.nativeConnection.signalingState === 'closed') { HMSLogger.d(TAG, `[role=${this.role}] addIceCandidate signalling state closed`); return; } HMSLogger.d(TAG, `[role=${this.role}] addIceCandidate candidate=${JSON.stringify(candidate, null, 1)}`); await this.nativeConnection.addIceCandidate(candidate); } public get remoteDescription(): RTCSessionDescription | null { return this.nativeConnection.remoteDescription; } getSenders(): Array<RTCRtpSender> { return this.nativeConnection.getSenders(); } handleSelectedIceCandidatePairs() { /** * for the very first peer in the room we don't have any subscribe ice candidates * because the peer hasn't subscribed to anything. * * For all peers joining after this peer, we have published and subscribed at the time of join itself * so we're able to log both publish and subscribe ice candidates. * Added try catch for the whole section as the getSenders and getReceivers is throwing errors in load test */ try { const transmitters = this.role === HMSConnectionRole.Publish ? this.getSenders() : this.getReceivers(); transmitters.forEach(transmitter => { const kindOfTrack = transmitter.track?.kind; if (transmitter.transport) { const iceTransport = transmitter.transport.iceTransport; const handleSelectedCandidate = () => { // @ts-expect-error if (typeof iceTransport.getSelectedCandidatePair === 'function') { // @ts-expect-error this.selectedCandidatePair = iceTransport.getSelectedCandidatePair(); if (this.selectedCandidatePair) { this.observer.onSelectedCandidatePairChange(this.selectedCandidatePair); HMSLogger.d( TAG, `${HMSConnectionRole[this.role]} connection`, `selected ${kindOfTrack || 'unknown'} candidate pair`, JSON.stringify(this.selectedCandidatePair, null, 2), ); } } }; // @ts-expect-error if (typeof iceTransport.onselectedcandidatepairchange === 'function') { // @ts-expect-error iceTransport.onselectedcandidatepairchange = handleSelectedCandidate; } handleSelectedCandidate(); } }); } catch (error) { HMSLogger.w( TAG, `Error in logging selected ice candidate pair for ${HMSConnectionRole[this.role]} connection`, error, ); } } removeTrack(sender: RTCRtpSender) { if (this.nativeConnection.signalingState !== 'closed') { this.nativeConnection.removeTrack(sender); } } // eslint-disable-next-line async setMaxBitrateAndFramerate( track: HMSLocalTrack, updatedSettings?: HMSAudioTrackSettings | HMSVideoTrackSettings, ) { const maxBitrate = updatedSettings?.maxBitrate || track.settings.maxBitrate; const maxFramerate = track instanceof HMSLocalVideoTrack && track.settings.maxFramerate; const sender = this.getSenders().find(s => s?.track?.id === track.getTrackIDBeingSent()); if (sender) { const params = sender.getParameters(); // modify only for non-simulcast encodings if (params.encodings.length === 1) { if (maxBitrate) { params.encodings[0].maxBitrate = maxBitrate * 1000; } if (maxFramerate) { // @ts-ignore params.encodings[0].maxFramerate = maxFramerate; } } await sender.setParameters(params); } else { HMSLogger.w( TAG, `no sender found to setMaxBitrate for track - ${track.trackId}, sentTrackId - ${track.getTrackIDBeingSent()}`, ); } } async getStats() { return await this.nativeConnection.getStats(); } close() { this.nativeConnection.close(); } private getReceivers() { return this.nativeConnection.getReceivers(); } }