From 693adf3f3732d7a7298fa8f588118ff4a3d7cd59 Mon Sep 17 00:00:00 2001 From: Drew Ewing Date: Thu, 5 May 2022 11:11:16 -0700 Subject: [PATCH] feat(calls): cleanup & comments --- libraries/WebRTC/Call.ts | 195 ++++++++++++++++++++++++++++++++-- store/conversation/actions.ts | 31 ++++++ store/webrtc/actions.ts | 81 +++++++------- 3 files changed, 260 insertions(+), 47 deletions(-) diff --git a/libraries/WebRTC/Call.ts b/libraries/WebRTC/Call.ts index 6588ffc4de..251d9970ba 100644 --- a/libraries/WebRTC/Call.ts +++ b/libraries/WebRTC/Call.ts @@ -1,4 +1,3 @@ -import { Duplex } from 'stream' import Peer, { SignalData } from 'simple-peer' import PeerId from 'peer-id' import Emitter from './Emitter' @@ -78,10 +77,21 @@ export class Call extends Emitter { this._bindBusListeners() } + /** + * @method isDirectCall + * @description Returns true if the call is direct, false otherwise + * @returns {boolean} + * @example + * const isDirectCall = call.isDirectCall() + */ isDirectCall() { return this.peerIds.length <= 1 } + /** + * @method requestPeerCalls + * @description Send a libp2p message requesting for the given peerIds to initiate a call + */ async requestPeerCalls() { await Promise.all( this.peerIds.map(async (peerId) => { @@ -137,6 +147,15 @@ export class Call extends Emitter { await this.start() } + /** + * @method initiateCall + * @description locally initiates a call to the given peer + * @param peerId + * @param asCaller boolean indicating if the local peer is the caller or not + * @returns {Promise} + * @example + * await call.initiateCall(peerId, true) + */ async initiateCall(peerId: string, asCaller: boolean = false) { if (!this.peers[peerId]) { const peer = new CallPeer( @@ -170,6 +189,14 @@ export class Call extends Emitter { } } + /** + * @method sendPeerCallRequest + * @description Send a call request to the given peer + * @param peerId + * @returns {Promise} + * @example + * await call.sendPeerCallRequest(peerId) + */ async sendPeerCallRequest(peerId: string) { if (this.peerConnected[peerId] || this.peerDialingDisabled[peerId]) return await this.p2p.sendMessage( @@ -188,16 +215,24 @@ export class Call extends Emitter { ) } + /** + * @method setCallId + * @description Sets the callId of the current call + * @param callId + * @returns {void} + * @example + * call.setCallId(callId) + */ setCallId(callId: string) { this.callId = callId } /** * @method createLocalTracks - * @description Generate local tracks for audio/video - * @param kinds Array of track kind you want to create + * @description Generate local streams for audio/video/screen + * @param kinds Array of track kinds you want to create * @param constraints Media stream constraints to apply - * @returns The created local tracks + * @returns The created streams * @example */ async createLocalTracks( @@ -224,6 +259,14 @@ export class Call extends Emitter { return this.streams } + /** + * @method createAudioStream + * @description Create local audio stream and send it to remote peers + * @param constraints Media stream constraints to apply + * @returns {Promise} + * @example + * await call.createAudioStream() + */ async createAudioStream( constraints: MediaTrackConstraints | boolean | undefined, ) { @@ -258,6 +301,14 @@ export class Call extends Emitter { ) } + /** + * @method createVideoStream + * @description Create local video stream and send it to remote peers + * @param constraints Media stream constraints to apply + * @returns {Promise} + * @example + * await call.createVideoStream() + */ async createVideoStream( constraints: MediaTrackConstraints | boolean | undefined, ) { @@ -292,6 +343,13 @@ export class Call extends Emitter { ) } + /** + * @method createDisplayStream + * @description Create local screen stream and send it to remote peers + * @returns {Promise} + * @example + * await call.createDisplayStream() + */ async createDisplayStream() { const screenStream = await navigator.mediaDevices.getDisplayMedia() if (!this.streams[this.localId]) { @@ -390,6 +448,12 @@ export class Call extends Emitter { ) } + /** + * @method getTrack + * @description Get the given track from the local or remote stream + * @param trackId + * @returns {MediaStreamTrack} the track + */ getTrack(trackId: string) { Object.values(this.tracks).forEach((tracks) => { tracks.forEach((track) => { @@ -401,42 +465,110 @@ export class Call extends Emitter { return null } + /** + * @method allRemoteStreams + * @description get all remote streams + * @returns {{ [peerId: string]: { [kind: string]: MediaStream } }} + * @example + * const remoteStreams = call.allRemoteStreams() + */ allRemoteStreams() { return Object.keys(this.streams) .filter((peerId) => peerId !== this.localId) .map((peerId) => this.streams[peerId]) } + /** + * @method getRemoteStreams + * @description get remote streams of a given type + * @param kind + * @returns {{ [peerId: string]: MediaStream }} + * @example + * const remoteStreams = call.getRemoteStreams('video') + */ getRemoteStreams(kind: string) { return Object.keys(this.streams) .filter((peerId) => peerId !== this.localId) .map((peerId) => this.streams[peerId]?.[kind]) } + /** + * @method getLocalStreams + * @description get all local streams + * @returns {{ [kind: string]: MediaStream }} + * @example + * const localStreams = call.getLocalStreams() + */ getLocalStreams() { return this.streams[this.localId] } + /** + * @method getLocalStream + * @description get local stream of a given type + * @param kind + * @returns {MediaStream} + * @example + * const localStream = call.getLocalStream('video') + */ getLocalStream(kind: string) { return this.streams[this.localId]?.[kind] } + /** + * @method getPeerStreams + * @description get all streams of a given peer + * @param peerId + * @returns {{ [kind: string]: MediaStream }} + * @example + * const peerStreams = call.getPeerStreams(peerId) + */ getPeerStreams(peerId: string) { return this.streams[peerId] } + /** + * @method getPeerStream + * @description get stream of a given peer and type + * @param peerId + * @param kind + * @returns {MediaStream} + * @example + * const peerStream = call.getPeerStream(peerId, 'video') + */ getPeerStream(peerId: string, kind: string) { return this.streams[peerId]?.[kind] } + /** + * @method hasLocalStream + * @description check if any local stream is available + * @returns {boolean} + * @example + * const hasLocalStream = call.hasLocalStream() + */ hasLocalStream() { return Object.entries(this.streams[this.localId] || {}).length > 0 } + /** + * @method hasRemoteStream + * @description check if any remote stream is available + * @returns {boolean} + * @example + * const hasRemoteStream = call.hasRemoteStream() + */ hasRemoteStream() { return this.allRemoteStreams().length > 0 } + /** + * @method hasRemoteTracks + * @description check if any remote track is available + * @returns {boolean} + * @example + * const hasRemoteTracks = call.hasRemoteTracks() + */ hasRemoteTracks() { return this.getRemoteTracks().length > 0 } @@ -665,6 +797,12 @@ export class Call extends Emitter { this.p2p?.on('peer:destroy', this._onBusDestroy.bind(this)) } + /** + * @method _unbindBusListeners + * @description Internal function to unbind listeners to the communiciationBus events + * @example + * this._unbindBusListeners(); + */ protected _unbindBusListeners() { this.p2p?.off('peer:signal', this._onBusSignal.bind(this)) this.p2p?.off('peer:refuse', this._onBusRefuse.bind(this)) @@ -690,6 +828,12 @@ export class Call extends Emitter { peer.on('close', this._onClose.bind(this, peer)) } + /** + * @method _unbindPeerListeners + * @description Internal function to unbind listeners to the main peer events + * @example + * this._unbindPeerListeners() + */ protected _unbindPeerListeners(peer: CallPeer) { peer.removeAllListeners() } @@ -726,12 +870,25 @@ export class Call extends Emitter { ) } + /** + * @method send + * @description send some data to connected peers + * @param type + * @param data + * @returns {void} + * @example + * await call.send('foo', 'test') + */ async send(type: string, data: any) { - Object.values(this.peers).forEach((peer) => { - peer.send({ peerId: this.localId, type, ...data }) + Object.values(this.peers).map(async (peer) => { + await peer.send({ peerId: this.localId, type, ...data }) }) } + /** + * @method _onConnect + * @description Callback for the Simple Peer connect event + */ protected _onConnect(peer: CallPeer) { this.emit('CONNECTED', { callId: this.callId, @@ -842,6 +999,14 @@ export class Call extends Emitter { } } + /** + * @method destroyPeer + * @description Destroy a peer + * @param peerId + * @returns {void} + * @example + * call.destroyPeer('peerId') + */ destroyPeer(peerId: string) { if (this.streams[peerId]) { Object.values(this.streams[peerId]).forEach((stream) => { @@ -886,6 +1051,12 @@ export class Call extends Emitter { this.destroyPeer(peerId.toB58String()) } + /** + * @method _onBusScreenshare + * @description Callback for the on screenshare event + * @param peerId + * @param payload + */ protected _onBusScreenshare({ peerId, payload: { streamId }, @@ -904,6 +1075,10 @@ export class Call extends Emitter { } } + /** + * @method _onBusMute + * @description Callback for the on mute event + */ protected _onBusMute({ peerId, payload: { kind, trackId }, @@ -926,6 +1101,10 @@ export class Call extends Emitter { ) } + /** + * @method _onBusMute + * @description Callback for the on unmute event + */ protected _onBusUnmute({ peerId, payload: { kind, trackId }, @@ -948,6 +1127,10 @@ export class Call extends Emitter { ) } + /** + * @method _onBusMute + * @description Callback for the on destroy event + */ protected _onBusDestroy({ peerId }: { peerId: PeerId }) { this.destroyPeer(peerId.toB58String()) } diff --git a/store/conversation/actions.ts b/store/conversation/actions.ts index 8d9800938c..321a8a8a13 100644 --- a/store/conversation/actions.ts +++ b/store/conversation/actions.ts @@ -5,6 +5,12 @@ import { ConversationParticipant, ConversationState } from './types' import { ActionsArguments } from '~/types/store/store' const actions = { + /** + * @method intialize + * @description Initialize the conversation store + * @example + * store.dispatch('conversation/initialize'); + */ initialize(state: ActionsArguments) { state.commit('setConversation', { id: '', @@ -13,6 +19,12 @@ const actions = { participants: [], }) }, + /** + * @method setConversation + * @description Set the current conversation + * @example + * store.dispatch('conversation/setConversation', conversation); + */ setConversation( state: ActionsArguments, payload: { @@ -24,9 +36,21 @@ const actions = { ) { state.commit('setConversation', payload) }, + /** + * @method setCalling + * @description Set the calling state of the conversation + * @example + * store.dispatch('conversation/setCalling', true); + */ setCalling(state: ActionsArguments, calling: boolean) { state.commit('setCalling', calling) }, + /** + * @method addParticipant + * @description Add a participant to the conversation + * @example + * store.dispatch('conversation/addParticipant', participant); + */ async addParticipant( { commit, state, rootState }: ActionsArguments, address: string, @@ -53,6 +77,13 @@ const actions = { profilePicture: participant?.profilePicture, }) }, + /** + * @method addParticipants + * @description Add multiple participants to the conversation + * @param participants + * @example + * store.dispatch('conversation/addParticipants', participants); + */ addParticipants( action: ActionsArguments, participants: string[], diff --git a/store/webrtc/actions.ts b/store/webrtc/actions.ts index 700475c2ca..f6cab458e1 100644 --- a/store/webrtc/actions.ts +++ b/store/webrtc/actions.ts @@ -1,6 +1,5 @@ import Vue from 'vue' import type { SignalData } from 'simple-peer' -import PeerId from 'peer-id' import { WebRTCState } from './types' import { ActionsArguments } from '~/types/store/store' @@ -190,6 +189,12 @@ const webRTCActions = { commit('setInitialized', true) }, + /** + * @method toggleMute + * @description - Turn on/off mute for the given stream in the active call + * @param peerId Peer ID of the owner of the stream + * @param kind Kind of the stream (audio/video/screen) + */ toggleMute( { state }: ActionsArguments, { peerId, kind }: { peerId: string; kind: 'audio' | 'video' | 'screen' }, @@ -209,10 +214,10 @@ const webRTCActions = { }, /** - * @method getActivePeers - * @description returns an array of the user id's that have an open/active webrtc call + * @method getActiveCalls + * @description returns an array containing the callId for each active webrtc call * @example - * this.$store.dispatch('webrtc/getActivePeers') // ['userid1', 'userid2'] + * this.$store.dispatch('webrtc/getActiveCalls') // ['userid1', 'groupId2'] */ async getActiveCalls() { return Object.keys($WebRTC.calls) @@ -220,10 +225,14 @@ const webRTCActions = { /** * @method createCall - * @description Connects to a secret channel and waits the peer connection to happen - * @param identifier Address of the current user + * @description creates a webrtc call connection to the provided peer(s) + * @param callID - the identifier of the group or peer + * @param peerIds - an array of peerIds that are part of the call + * @param signal - a pre-provided peer signal for dropping into ongoing calls (optional) + * @param peerId - the peerId of the user that initiated the call * @example - * this.$store.dispatch('webrtc/initialize') + * this.$store.dispatch('webrtc/initialize', { callId: 'userid1', peerId: 'userid2' }) + * this.$store.dispatch('webrtc/initialize', { callId: 'groupId1', peerIds: ['userid1', 'userid2'] }) */ async createCall( { commit, state }: ActionsArguments, @@ -362,12 +371,24 @@ const webRTCActions = { $WebRTC.calls.delete(callId) }) }, - denyCall({ state, commit }: ActionsArguments) { + /** + * @method deny + * @description denies an incoming call + * @example + * this.$store.dispatch('webrtc/deny') + */ + denyCall({ state }: ActionsArguments) { if (state.activeCall) $WebRTC.getCall(state.activeCall.callId)?.deny() if (state.incomingCall) { $WebRTC.getCall(state.incomingCall.callId)?.deny() } }, + /** + * @method hangUp + * @description hangs up the active call + * @example + * this.$store.dispatch('webrtc/hangUp') + */ hangUp({ state, commit }: ActionsArguments) { if (state.activeCall) { $WebRTC.getCall(state.activeCall.callId)?.destroy() @@ -375,11 +396,19 @@ const webRTCActions = { commit('setActiveCall', undefined) commit('setIncomingCall', undefined) }, + /** + * @method call + * @description dials an outbound call to the active peer(s) (rootState.conversation.id) + * @param kinds - an array of media kinds to be sent (audio, video, screen) + * @example + * this.$store.dispatch('webrtc/call') + */ async call( { dispatch, commit, rootState }: ActionsArguments, { kinds }: { kinds: TrackKind[] }, ) { const $Logger: Logger = Vue.prototype.$Logger + const $Peer2Peer: Peer2Peer = Peer2Peer.getInstance() const activeConversation = rootState.conversation if (!activeConversation.id) { @@ -398,35 +427,7 @@ const webRTCActions = { return } - dispatch('initializeCall', { - initiate: true, - callId: activeConversation.id, - peerIds, - kinds, - }) - commit('setIncomingCall', undefined) - }, - async initializeCall( - { commit, dispatch }: ActionsArguments, - { - initiate = false, - callId, - peerIds, - peerId, - signal, - kinds, - }: { - initiate: boolean - callId: string - peerId: string - peerIds?: string[] - signal?: SignalData - kinds: TrackKind[] - }, - ) { - const $Logger: Logger = Vue.prototype.$Logger - const $Peer2Peer: Peer2Peer = Peer2Peer.getInstance() - + const callId = activeConversation.id if (!callId) { $Logger.log('webrtc', `call - callId not found: ${callId}`) return @@ -437,9 +438,7 @@ const webRTCActions = { await dispatch('createCall', { callId, peerIds, - initiate, - peerId, - signal, + initiate: true, }) } @@ -456,7 +455,7 @@ const webRTCActions = { screen: !kinds.includes('screen'), }) commit('setIncomingCall', undefined) - commit('setActiveCall', { callId, peerId }) + commit('setActiveCall', { callId }) commit('updateCreatedAt', Date.now()) await call.createLocalTracks(kinds) await call.start()