kod-temp
Version:
Workano Communications SDK
715 lines (714 loc) • 30.3 kB
JavaScript
import sdpParser from 'sdp-transform';
import getApiClient from '../../service/getApiClient';
import Emitter from '../../utils/Emitter';
import Workano from '../index';
import RemoteParticipant from './RemoteParticipant';
import IssueReporter from '../../service/IssueReporter';
export const SIGNAL_TYPE_PARTICIPANT_UPDATE = 'signal/PARTICIPANT_UPDATE';
export const SIGNAL_TYPE_PARTICIPANT_REQUEST = 'signal/PARTICIPANT_REQUEST';
const logger = IssueReporter.loggerFor('sdk-room');
class Room extends Emitter {
callSession;
name;
extension;
sourceId;
meetingUuid;
participants;
callId;
connected;
localParticipant;
_callIdStreamIdMap;
_unassociatedVideoStreams;
_unassociatedParticipants;
_boundOnParticipantJoined;
_boundOnParticipantLeft;
_boundOnMessage;
_boundOnChat;
_boundOnSignal;
_boundSaveLocalVideoStream;
_boundOnReinvite;
audioStream;
extra;
// video tag representing the room audio stream
roomAudioElement;
CONFERENCE_USER_PARTICIPANT_JOINED;
CONFERENCE_USER_PARTICIPANT_LEFT;
MEETING_USER_PARTICIPANT_JOINED;
MEETING_USER_PARTICIPANT_LEFT;
ON_SHARE_SCREEN_ENDED;
ON_MESSAGE;
ON_CHAT;
ON_SIGNAL;
ON_AUDIO_STREAM;
ON_VIDEO_STREAM;
ON_REMOVE_STREAM;
ON_DISCONNECTED;
ON_JOINED;
ON_VIDEO_INPUT_CHANGE;
/**
*
* @param callSession CallSession
* @param extension string
* @param sourceId number
* @param callId string
* @param meetingUuid string
* @param extra Object
*/
constructor(callSession, extension, sourceId, callId, meetingUuid, extra = {}) {
super();
logger.info('room initialized', {
callId,
extension,
sourceId,
meetingUuid,
});
// Represents the room callSession
this.callSession = callSession;
this.extension = extension;
this.sourceId = sourceId;
this.meetingUuid = meetingUuid;
this.callId = callId;
this.participants = [];
this.connected = false;
this.localParticipant = null;
// [callId]: streamId
this._callIdStreamIdMap = {};
// Stream not yet associated to a participant, [streamId]: stream
this._unassociatedVideoStreams = {};
// Participant not yet associated to a stream, [participant.callId = label in setDescription]: Participant
this._unassociatedParticipants = {};
// The shared audio stream of the room
this.audioStream = null;
// Extra values passed to local participant
this.extra = extra;
// Sugar syntax for `room.EVENT_NAME`
this.CONFERENCE_USER_PARTICIPANT_JOINED = Workano.Websocket.CONFERENCE_USER_PARTICIPANT_JOINED;
this.CONFERENCE_USER_PARTICIPANT_LEFT = Workano.Websocket.CONFERENCE_USER_PARTICIPANT_LEFT;
this.MEETING_USER_PARTICIPANT_JOINED = Workano.Websocket.MEETING_USER_PARTICIPANT_JOINED;
this.MEETING_USER_PARTICIPANT_LEFT = Workano.Websocket.MEETING_USER_PARTICIPANT_LEFT;
this.ON_SHARE_SCREEN_ENDED = Workano.Phone.ON_SHARE_SCREEN_ENDED;
this.ON_MESSAGE = Workano.Phone.ON_MESSAGE;
this.ON_CHAT = Workano.Phone.ON_CHAT;
this.ON_SIGNAL = Workano.Phone.ON_SIGNAL;
this.ON_AUDIO_STREAM = Workano.Phone.ON_AUDIO_STREAM;
this.ON_VIDEO_STREAM = Workano.Phone.ON_VIDEO_STREAM;
this.ON_REMOVE_STREAM = Workano.Phone.ON_REMOVE_STREAM;
this.ON_VIDEO_INPUT_CHANGE = Workano.Phone.ON_VIDEO_INPUT_CHANGE;
this.ON_DISCONNECTED = 'room/ON_DISCONNECTED';
this.ON_JOINED = 'room/ON_JOINED';
this._boundOnParticipantJoined = this._onParticipantJoined.bind(this);
this._boundOnParticipantLeft = this._onParticipantLeft.bind(this);
this._boundOnMessage = this._onMessage.bind(this);
this._boundOnChat = this._onChat.bind(this);
this._boundOnSignal = this._onSignal.bind(this);
this._boundSaveLocalVideoStream = this._saveLocalVideoStream.bind(this);
this._boundOnReinvite = this._onReinvite.bind(this);
this.unbind();
this._bindEvents();
this._transferEvents();
}
/**
*
* @param extension string
* @param constraints string
* @param audioOnly boolean
* @param extra Object
* @param room ?Room
* @param meeting ?Meeting
* @returns {Promise<Room>}
*/
static async connect({ extension, constraints, audioOnly = false, extra, room, meeting, }) {
logger.info('connecting to room', {
extension,
audioOnly,
room: !!room,
});
if (!room) {
await Workano.Phone.connect({
// @ts-ignore: Property 'audio' is missing
media: constraints,
});
const withCamera = constraints && !!constraints.video;
if (withCamera) {
Workano.Phone.checkSfu();
}
// Call_created is triggered before call_accepted, so we have to listen for it here.
Workano.Websocket.once(Workano.Websocket.CALL_CREATED, ({ data, }) => {
logger.info('room call received via WS', {
callId: data.call_id,
});
if (room) {
room.setCallId(data.call_id);
}
});
const callSession = await Workano.Phone.call(extension, withCamera, null, audioOnly, true);
// @ts-fix: heads up: added an extra `null` here
// eslint-disable-next-line no-param-reassign
room = new Room(callSession, extension, null, null, null, extra);
// Wait for the call to be accepted
await new Promise((resolve, reject) => {
Workano.Phone.once(Workano.Phone.ON_CALL_ACCEPTED, resolve);
Workano.Phone.once(Workano.Phone.ON_CALL_FAILED, reject);
});
}
if (room && room.callSession && room.callSession.call) {
room.setCallId(room.callSession.call.id);
}
if (!meeting) {
// Fetch conference source
const sources = await getApiClient().dird.fetchConferenceSource('default');
// Retrieve conference sources
const contacts = await getApiClient().dird.fetchConferenceContacts(sources.items[0]);
// Retrieve conference
const conference = contacts?.find(contact => contact.numbers?.find(number => String(number.number) === String(extension)));
logger.info('connected to room', {
sourceId: conference ? conference.sourceId : null,
name: conference ? conference.name : null,
});
if (conference) {
room.setSourceId(parseInt(`${conference.sourceId}`, 10));
room.setName(`${conference.name}`);
}
}
else if (meeting) {
logger.info('Already connected to meeting', {
uuid: meeting.uuid,
name: meeting.name,
});
room.setMeetingUuid(meeting.uuid);
room.setName(meeting.name);
}
return room;
}
static disconnect() {
logger.info('static disconnection to room');
Workano.Phone.disconnect();
}
async disconnect() {
logger.info('disconnection to room called');
await Workano.Phone.hangup(this.callSession);
this.callSession = undefined;
this.eventEmitter.emit(this.ON_DISCONNECTED, this);
this.connected = false;
this.unbind();
Workano.Phone.off(this.ON_MESSAGE, this._boundOnMessage);
Workano.Phone.off(this.ON_CHAT, this._boundOnChat);
Workano.Phone.off(this.ON_SIGNAL, this._boundOnSignal);
Workano.Phone.off(this.ON_VIDEO_INPUT_CHANGE, this._boundSaveLocalVideoStream);
Workano.Phone.phone?.off(Workano.Phone.phone.client.ON_REINVITE, this._boundOnReinvite);
Workano.Websocket.off(this.CONFERENCE_USER_PARTICIPANT_JOINED, this._boundOnParticipantJoined);
Workano.Websocket.off(this.CONFERENCE_USER_PARTICIPANT_LEFT, this._boundOnParticipantLeft);
Workano.Websocket.off(this.MEETING_USER_PARTICIPANT_JOINED, this._boundOnParticipantJoined);
Workano.Websocket.off(this.MEETING_USER_PARTICIPANT_LEFT, this._boundOnParticipantLeft);
}
setSourceId(sourceId) {
logger.info('set room source id', {
sourceId,
});
this.sourceId = sourceId;
}
setMeetingUuid(meetingUuid) {
logger.info('set meeting uuid', {
meetingUuid,
});
this.meetingUuid = meetingUuid;
}
setCallId(callId) {
logger.info('set room call id', {
callId,
});
if (callId) {
this.callId = callId;
}
}
setName(name) {
logger.info('set room name', {
name,
});
this.name = name;
}
// @TODO: change sipSession to callSession
sendMessage(body, sipSession = null) {
return Workano.Phone.sendMessage(body, sipSession);
}
sendChat(content) {
return Workano.Phone.sendChat(content);
}
sendSignal(content) {
return Workano.Phone.sendSignal(content);
}
async startScreenSharing(constraints) {
logger.info('start room screen sharing', {
constraints,
});
const screensharingStream = await Workano.Phone.startScreenSharing(constraints, this.callSession);
if (!screensharingStream) {
console.warn('screensharing stream is null (likely due to user cancellation)');
return null;
}
this._onScreenSharing();
return screensharingStream;
}
async stopScreenSharing(restoreLocalStream = true) {
logger.info('stop room screen sharing');
await Workano.Phone.stopScreenSharing(this.callSession, restoreLocalStream);
if (this.localParticipant) {
this._updateLocalParticipantStream();
this.localParticipant.onStopScreensharing();
}
}
turnCameraOff() {
logger.info('turn room camera off');
Workano.Phone.turnCameraOff(this.callSession);
if (this.localParticipant) {
this.localParticipant.onVideoMuted();
}
}
turnCameraOn() {
logger.info('turn room camera on');
Workano.Phone.turnCameraOn(this.callSession);
if (this.localParticipant) {
this.localParticipant.onVideoUnMuted();
}
}
mute() {
logger.info('mute room');
Workano.Phone.mute(this.callSession);
this.sendMuteStatus();
}
unmute() {
logger.info('unmute room');
Workano.Phone.unmute(this.callSession);
this.sendUnMuteStatus();
}
sendMuteStatus() {
if (this.localParticipant) {
this.localParticipant.onAudioMuted();
}
}
sendUnMuteStatus() {
if (this.localParticipant) {
this.localParticipant.onAudioUnMuted();
}
}
hold() {
logger.info('hold room');
Workano.Phone.hold(this.callSession);
if (this.localParticipant) {
this.localParticipant.onHold();
}
}
async resume() {
logger.info('resume room');
const newStream = await Workano.Phone.resume(this.callSession);
if (this.localParticipant) {
// Update local participant stream (useful when resuming a shreenshared conference)
this._updateLocalParticipantStream();
this.localParticipant.onResume();
if (!newStream && this.localParticipant.screensharing) {
this.localParticipant.onStopScreensharing();
}
}
}
_updateLocalParticipantStream() {
const localStream = Workano.Phone.getLocalStream(this.callSession);
if (this.localParticipant && localStream) {
const localWorkanoStream = new Workano.Stream(localStream);
this.localParticipant.resetStreams([localWorkanoStream]);
}
}
sendDTMF(tone) {
logger.info('send room DTMF', {
tone,
});
Workano.Phone.sendDTMF(tone, this.callSession);
}
async sendReinvite(newConstraints = null) {
logger.info('send room reinvite', {
callId: this.callSession ? this.callSession.getId() : null,
newConstraints,
});
const wasScreensharing = this.localParticipant && this.localParticipant.screensharing;
Workano.Phone.on(Workano.Phone.ON_SHARE_SCREEN_STARTED, () => {
if (Workano.Phone.phone && Workano.Phone.phone.currentScreenShare) {
this._onScreenSharing();
}
});
const response = await Workano.Phone.phone?.sendReinvite(this.callSession, newConstraints, true);
if (this.localParticipant && newConstraints && newConstraints.video) {
const localVideoStream = Workano.Phone.phone?.getLocalVideoStream(this.callSession);
if (localVideoStream) {
this._associateStreamTo(localVideoStream, this.localParticipant);
}
}
else if (this.localParticipant && wasScreensharing && newConstraints && !newConstraints.video) {
// Downgrade from screenshare to audio
this.localParticipant.onStopScreensharing();
}
return response;
}
hasALocalVideoTrack() {
return Workano.Phone.hasALocalVideoTrack(this.callSession);
}
getLocalStream() {
return Workano.Phone.getLocalStream(this.callSession);
}
getRemoteStream() {
return Workano.Phone.getRemoteStream(this.callSession);
}
getRemoteVideoStream() {
return Workano.Phone.getRemoteVideoStream(this.callSession);
}
_bindEvents() {
if (!Workano.Phone.phone || !Workano.Phone.phone.currentSipSession) {
return;
}
// Retrieve mapping
Workano.Phone.phone.currentSipSession.sessionDescriptionHandler?.on('setDescription', ({ type, sdp: rawSdp, }) => {
if (type !== 'offer') {
return;
}
this._mapMsid(rawSdp);
});
// Listen to REINVITE to ba able to map msid after upgrading to video in a audio only conference
// This allow to map msid with the non parsed (eg without the `stripVideo` modifier) SDP
Workano.Phone.phone.on(Workano.Phone.phone.client.ON_REINVITE, this._boundOnReinvite);
this.on(this.ON_AUDIO_STREAM, async (stream) => {
logger.info('on room audio stream');
this.audioStream = stream;
if (!this.roomAudioElement) {
const sessionId = Workano.Phone.phone?.getSipSessionId(Workano.Phone.phone?.currentSipSession);
this.roomAudioElement = await Workano.Phone.phone?.createAudioElementFor(sessionId);
this.roomAudioElement.srcObject = stream;
}
else {
this.roomAudioElement.srcObject = stream;
}
});
this.on(this.ON_VIDEO_STREAM, (stream, streamId, event, sipSession) => {
logger.info('on room video stream', {
streamId,
});
this._mapMsid(sipSession.body.body);
// ON_VIDEO_STREAM is called before PARTICIPANT_JOINED, so we have to keep stream in `_unassociatedVideoStreams`.
this._unassociatedVideoStreams[streamId] = stream;
const callId = this._getCallIdFromTrackId(streamId);
const participant = callId ? this._getParticipantFromCallId(callId) : null;
if (participant) {
this.__associateStreams(participant);
}
});
this.on(this.ON_REMOVE_STREAM, stream => {
logger.info('on room remove stream');
const participant = this.participants.find(someParticipant => someParticipant.streams.find(someStream => someStream && someStream.id === stream.id));
if (!participant) {
return;
}
participant.videoStreams = participant.videoStreams.filter(someStream => someStream.id !== stream.id);
participant.streams = participant.streams.filter(someStream => someStream.id !== stream.id);
participant.onStreamUnSubscribed(stream);
});
}
_onScreenSharing() {
if (this.localParticipant) {
this.localParticipant.onScreensharing();
}
}
_onReinvite(session, inviteRequest) {
const body = inviteRequest.body || inviteRequest.message.body;
if (body) {
this._mapMsid(body);
// Re-associate video streams
this.participants.forEach(participant => {
this.__associateStreams(participant);
});
}
}
_mapMsid(rawSdp) {
const sdp = sdpParser.parse(rawSdp);
// @ts-ignore: Property 'label' does not exist
const labelMsidArray = sdp.media.filter((media) => !!media.label).map(({ label, msid }) => ({
label: String(label),
streamId: msid?.split(' ')[0],
trackId: msid?.split(' ')[1],
}));
labelMsidArray.forEach(({ label, streamId, trackId, }) => {
this._callIdStreamIdMap[String(label)] = {
streamId,
trackId,
};
const callId = String(label);
const participant = this._unassociatedParticipants[callId] || this._getParticipantFromCallId(callId);
if (participant) {
this.__associateStreams(participant);
}
});
}
_transferEvents() {
Workano.Websocket.on(this.CONFERENCE_USER_PARTICIPANT_JOINED, this._boundOnParticipantJoined);
Workano.Websocket.on(this.CONFERENCE_USER_PARTICIPANT_LEFT, this._boundOnParticipantLeft);
Workano.Websocket.on(this.MEETING_USER_PARTICIPANT_JOINED, this._boundOnParticipantJoined);
Workano.Websocket.on(this.MEETING_USER_PARTICIPANT_LEFT, this._boundOnParticipantLeft);
// Phone events
Workano.Phone.on(this.ON_MESSAGE, this._boundOnMessage);
Workano.Phone.on(this.ON_CHAT, this._boundOnChat);
Workano.Phone.on(this.ON_SIGNAL, this._boundOnSignal);
Workano.Phone.on(this.ON_VIDEO_INPUT_CHANGE, this._boundSaveLocalVideoStream);
[this.ON_AUDIO_STREAM, this.ON_VIDEO_STREAM, this.ON_REMOVE_STREAM].forEach(event => Workano.Phone.on(event, (...args) => this.eventEmitter.emit.apply(this.eventEmitter, [event, ...args])));
}
_onMessage(message) {
if (message.method !== 'MESSAGE') {
return null;
}
let body;
try {
body = JSON.parse(message.body);
}
catch (e) {
return null;
}
switch (body.type) {
case 'ConfbridgeTalking':
{
// Update participant
const channel = body.channels[0];
const { id: callId, talking_status: talkingStatus, } = channel;
const isTalking = talkingStatus === 'on';
const participantIdx = this.participants.findIndex(participant => participant.callId === callId);
if (participantIdx === -1) {
return;
}
this.participants[participantIdx].onTalking(isTalking);
break;
}
default:
}
this.eventEmitter.emit(this.ON_MESSAGE, body);
return body;
}
_onChat(content) {
this.eventEmitter.emit(this.ON_CHAT, content);
}
_onSignal(content) {
const { type, } = content;
switch (type) {
// we're receiving a external update
case SIGNAL_TYPE_PARTICIPANT_UPDATE:
{
const { status, } = content;
const participant = this._getParticipantFromCallId(status.callId);
if (participant) {
// we're receiving, so no need to broadcast
participant.updateStatus(status, false);
}
break;
}
// this is a request to broadcast our current status
case SIGNAL_TYPE_PARTICIPANT_REQUEST:
{
const { callId, origin, } = content;
// callId is null, someone's requesting everyone's state;
// or callId is set and matches ours;
if (this.localParticipant && (!callId || callId === this.localParticipant.callId)) {
this.localParticipant.broadcastStatus(null, true);
}
// might as well update the requester's status
const requester = this._getParticipantFromCallId(origin.callId);
if (requester) {
// @FIXME?: when need to trigger an update on join-in; this is a bit of a hack
logger.info('trigger room requester status', {
origin,
});
requester.triggerUpdate('REQUESTER_UPDATE');
}
break;
}
case Workano.Phone.ON_MESSAGE_TRACK_UPDATED:
{
const { callId, update, } = content;
const participantIdx = this.participants.findIndex(p => p.callId === callId);
if (participantIdx !== -1) {
this.participants[participantIdx] = this._onParticipantTrackUpdate(this.participants[participantIdx], update);
}
break;
}
default:
{
console.warn('uncaught signal', content);
}
}
this.eventEmitter.emit(Workano.Phone.ON_SIGNAL, content);
}
async _onParticipantJoined(payload) {
const participant = payload.data;
const session = Workano.Auth.getSession();
let participants = [];
// When we join the room, we can call `getConferenceParticipantsAsUser`, not before.
if (participant.user_uuid === session?.uuid) {
logger.info('room current user joined');
// Retrieve participants via an API calls
const conferenceId = this.sourceId || payload.data.conference_id;
let response;
try {
if (this.meetingUuid) {
logger.info('fetching meeting participants', {
meetingUuid: this.meetingUuid,
});
response = await getApiClient().calld.getMeetingParticipantsAsUser(this.meetingUuid);
}
else {
logger.info('fetching conference participants', {
conferenceId,
});
response = await getApiClient().calld.getConferenceParticipantsAsUser(conferenceId);
}
}
catch (e) {
logger.error('room participants fetching, error', e);
}
if (response) {
logger.info('conference participants fetched', {
nb: response.items.length,
});
participants = response.items.map((item) => {
const isMe = item.call_id === this.callId;
return isMe && item.call_id ? new Workano.LocalParticipant(this, item, this.extra) : new Workano.RemoteParticipant(this, item);
});
this.participants = participants;
const localParticipant = participants.find((someParticipant) => someParticipant instanceof Workano.LocalParticipant);
if (!this.localParticipant && localParticipant) {
this._onLocalParticipantJoined(localParticipant);
}
participants.forEach((someParticipant) => this._isParticipantJoining(someParticipant));
this.eventEmitter.emit(this.ON_JOINED, localParticipant, participants);
}
return this.participants;
}
const remoteParticipant = !this.participants.some(p => p.callId === participant.call_id) ? new Workano.RemoteParticipant(this, participant) : null;
logger.info('other room user joined', {
callId: participant.call_id,
remoteParticipant: !!remoteParticipant,
});
if (remoteParticipant) {
this.participants.push(remoteParticipant);
this._isParticipantJoining(remoteParticipant);
}
return remoteParticipant;
}
_onLocalParticipantJoined(localParticipant) {
this.localParticipant = localParticipant;
const localVideoStream = this._getLocalVideoStream();
if (localVideoStream) {
this._saveLocalVideoStream(localVideoStream);
}
this.connected = true;
localParticipant.broadcastStatus(null, true);
// we're in the room, now let's request everyone's status
if (this.localParticipant) {
this.sendSignal({
type: SIGNAL_TYPE_PARTICIPANT_REQUEST,
origin: this.localParticipant.getStatus(),
});
}
}
_isParticipantJoining(participant) {
this.__associateStreams(participant);
// @VALIDATE: no need to publicize ourselves, no?
if (participant instanceof RemoteParticipant) {
this.eventEmitter.emit(this.CONFERENCE_USER_PARTICIPANT_JOINED, participant);
}
}
_saveLocalVideoStream(stream) {
const { localParticipant, } = this;
if (!localParticipant) {
return;
}
const videoStream = new Workano.Stream(stream, localParticipant);
if (videoStream) {
localParticipant.resetStreams([videoStream]);
localParticipant.onStreamSubscribed(videoStream);
}
return videoStream;
}
_onParticipantLeft(payload) {
const leftParticipant = this.participants.find(participant => participant && participant.callId === payload.data.call_id);
// Trigger Participant.ON_DISCONNECT event
if (leftParticipant) {
leftParticipant.onDisconnect();
}
this.participants = this.participants.filter(participant => participant && participant.callId !== payload.data.call_id);
this.eventEmitter.emit(this.CONFERENCE_USER_PARTICIPANT_LEFT, leftParticipant);
}
_onParticipantTrackUpdate(oldParticipant, update) {
const newParticipant = oldParticipant;
const { trackId, streamId, } = this._callIdStreamIdMap[newParticipant.callId] || {};
const pc = Workano.Phone.phone?.currentSipSession?.sessionDescriptionHandler?.peerConnection;
// Can't use `getReceivers` here because on FF we make the mapping based on the streamId
const stream = pc.getRemoteStreams().find(someStream => someStream.id === streamId || someStream.getTracks().some(track => track.id === trackId));
if (update === 'downgrade') {
newParticipant.resetStreams([]);
newParticipant.onStreamUnSubscribed(stream);
return newParticipant;
}
// Upgrade
if (stream) {
this._associateStreamTo(stream, newParticipant);
}
return newParticipant;
}
// Associate audio/video streams to the participant and triggers events on it
__associateStreams(participant) {
const { trackId, } = this._callIdStreamIdMap[participant.callId] || {};
if (!trackId) {
this._unassociatedParticipants[participant.callId] = participant;
return;
}
if (!trackId || !participant || !this.localParticipant || participant.callId === this.localParticipant.callId) {
return;
}
const streamId = this._getStreamIdFrTrackId(trackId);
const key = this._getUnassociatedMapIdFromTrackIdOrStreamId(trackId, streamId);
const stream = key ? this._unassociatedVideoStreams[key] : null;
if (stream) {
// Try to associate stream
this._associateStreamTo(stream, participant);
if (key) {
delete this._unassociatedVideoStreams[key];
}
delete this._unassociatedParticipants[participant.callId];
}
}
_getUnassociatedMapIdFromTrackIdOrStreamId(trackId, streamId) {
// Find by trackId
if (trackId in this._unassociatedVideoStreams) {
return trackId;
}
// Find by streamId
if (streamId && streamId in this._unassociatedVideoStreams) {
return streamId;
}
// Find in all the streams by streamId (used on FF where we can't map by trackId)
const idx = Object.values(this._unassociatedVideoStreams).findIndex((stream) => stream && stream.id === streamId);
return idx === -1 ? null : Object.keys(this._unassociatedVideoStreams)[idx];
}
_getStreamIdFrTrackId(trackId) {
const mapping = Object.values(this._callIdStreamIdMap).find((map) => map.trackId === trackId);
return mapping ? mapping.streamId : null;
}
_associateStreamTo(rawStream, participant) {
const stream = new Workano.Stream(rawStream, participant);
participant.streams.push(stream);
participant.videoStreams.push(stream);
participant.onStreamSubscribed(stream);
}
_getCallIdFromTrackId(trackId) {
return Object.keys(this._callIdStreamIdMap).find(key => this._callIdStreamIdMap[key].trackId === trackId);
}
_getParticipantFromCallId(callId) {
return this.participants.find(participant => participant.callId === callId);
}
_getLocalVideoStream() {
return Workano.Phone.getLocalVideoStream(this.callSession);
}
}
export default Room;