You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2672 lines
57 KiB
2672 lines
57 KiB
import protooClient from 'protoo-client';
|
|
import * as mediasoupClient from 'mediasoup-client';
|
|
import Logger from './Logger';
|
|
import { getProtooUrl } from './urlFactory';
|
|
import * as cookiesManager from './cookiesManager';
|
|
import * as requestActions from './redux/requestActions';
|
|
import * as stateActions from './redux/stateActions';
|
|
import * as e2e from './e2e';
|
|
|
|
const VIDEO_CONSTRAINS =
|
|
{
|
|
qvga : { width: { ideal: 320 }, height: { ideal: 240 } },
|
|
vga : { width: { ideal: 640 }, height: { ideal: 480 } },
|
|
hd : { width: { ideal: 1280 }, height: { ideal: 720 } }
|
|
};
|
|
|
|
const PC_PROPRIETARY_CONSTRAINTS =
|
|
{
|
|
// optional : [ { googDscp: true } ]
|
|
};
|
|
|
|
const EXTERNAL_VIDEO_SRC = '/resources/videos/video-audio-stereo.mp4';
|
|
|
|
const logger = new Logger('RoomClient');
|
|
|
|
let store;
|
|
|
|
export default class RoomClient
|
|
{
|
|
/**
|
|
* @param {Object} data
|
|
* @param {Object} data.store - The Redux store.
|
|
*/
|
|
static init(data)
|
|
{
|
|
store = data.store;
|
|
}
|
|
|
|
constructor(
|
|
{
|
|
roomId,
|
|
peerId,
|
|
displayName,
|
|
device,
|
|
handlerName,
|
|
forceTcp,
|
|
produce,
|
|
consume,
|
|
datachannel,
|
|
enableWebcamLayers,
|
|
enableSharingLayers,
|
|
webcamScalabilityMode,
|
|
sharingScalabilityMode,
|
|
numSimulcastStreams,
|
|
forceVP8,
|
|
forceH264,
|
|
forceVP9,
|
|
externalVideo,
|
|
e2eKey,
|
|
consumerReplicas
|
|
}
|
|
)
|
|
{
|
|
logger.debug(
|
|
'constructor() [roomId:"%s", peerId:"%s", displayName:"%s", device:%s]',
|
|
roomId, peerId, displayName, device.flag);
|
|
|
|
// Closed flag.
|
|
// @type {Boolean}
|
|
this._closed = false;
|
|
|
|
// Display name.
|
|
// @type {String}
|
|
this._displayName = displayName;
|
|
|
|
// Device info.
|
|
// @type {Object}
|
|
this._device = device;
|
|
|
|
// Custom mediasoup-client handler name (to override default browser
|
|
// detection if desired).
|
|
// @type {String}
|
|
this._handlerName = handlerName;
|
|
|
|
// Whether we want to force RTC over TCP.
|
|
// @type {Boolean}
|
|
this._forceTcp = forceTcp;
|
|
|
|
// Whether we want to produce audio/video.
|
|
// @type {Boolean}
|
|
this._produce = produce;
|
|
|
|
// Whether we should consume.
|
|
// @type {Boolean}
|
|
this._consume = consume;
|
|
|
|
// Whether we want DataChannels.
|
|
// @type {Boolean}
|
|
this._useDataChannel = Boolean(datachannel);
|
|
|
|
// Force VP8 codec for sending.
|
|
// @type {Boolean}
|
|
this._forceVP8 = Boolean(forceVP8);
|
|
|
|
// Force H264 codec for sending.
|
|
// @type {Boolean}
|
|
this._forceH264 = Boolean(forceH264);
|
|
|
|
// Force VP9 codec for sending.
|
|
// @type {Boolean}
|
|
this._forceVP9 = Boolean(forceVP9);
|
|
|
|
// Whether simulcast or SVC should be used for webcam.
|
|
// @type {Boolean}
|
|
this._enableWebcamLayers = Boolean(enableWebcamLayers);
|
|
|
|
// Whether simulcast or SVC should be used in desktop sharing.
|
|
// @type {Boolean}
|
|
this._enableSharingLayers = Boolean(enableSharingLayers);
|
|
|
|
// Scalability mode for webcam.
|
|
// @type {String}
|
|
this._webcamScalabilityMode = webcamScalabilityMode;
|
|
|
|
// Scalability mode for sharing.
|
|
// @type {String}
|
|
this._sharingScalabilityMode = sharingScalabilityMode;
|
|
|
|
// Number of simuclast streams for webcam and sharing.
|
|
// @type {Number}
|
|
this._numSimulcastStreams = numSimulcastStreams;
|
|
|
|
// External video.
|
|
// @type {HTMLVideoElement}
|
|
this._externalVideo = null;
|
|
|
|
// Enabled end-to-end encryption.
|
|
this._e2eKey = e2eKey;
|
|
|
|
// MediaStream of the external video.
|
|
// @type {MediaStream}
|
|
this._externalVideoStream = null;
|
|
|
|
// Next expected dataChannel test number.
|
|
// @type {Number}
|
|
this._nextDataChannelTestNumber = 0;
|
|
|
|
if (externalVideo)
|
|
{
|
|
this._externalVideo = document.createElement('video');
|
|
|
|
this._externalVideo.controls = true;
|
|
this._externalVideo.muted = true;
|
|
this._externalVideo.loop = true;
|
|
this._externalVideo.setAttribute('playsinline', '');
|
|
this._externalVideo.src = EXTERNAL_VIDEO_SRC;
|
|
|
|
this._externalVideo.play()
|
|
.catch((error) => logger.warn('externalVideo.play() failed:%o', error));
|
|
}
|
|
|
|
// Protoo URL.
|
|
// @type {String}
|
|
this._protooUrl = getProtooUrl({ roomId, peerId, consumerReplicas });
|
|
|
|
// protoo-client Peer instance.
|
|
// @type {protooClient.Peer}
|
|
this._protoo = null;
|
|
|
|
// mediasoup-client Device instance.
|
|
// @type {mediasoupClient.Device}
|
|
this._mediasoupDevice = null;
|
|
|
|
// mediasoup Transport for sending.
|
|
// @type {mediasoupClient.Transport}
|
|
this._sendTransport = null;
|
|
|
|
// mediasoup Transport for receiving.
|
|
// @type {mediasoupClient.Transport}
|
|
this._recvTransport = null;
|
|
|
|
// Local mic mediasoup Producer.
|
|
// @type {mediasoupClient.Producer}
|
|
this._micProducer = null;
|
|
|
|
// Local webcam mediasoup Producer.
|
|
// @type {mediasoupClient.Producer}
|
|
this._webcamProducer = null;
|
|
|
|
// Local share mediasoup Producer.
|
|
// @type {mediasoupClient.Producer}
|
|
this._shareProducer = null;
|
|
|
|
// Local chat DataProducer.
|
|
// @type {mediasoupClient.DataProducer}
|
|
this._chatDataProducer = null;
|
|
|
|
// Local bot DataProducer.
|
|
// @type {mediasoupClient.DataProducer}
|
|
this._botDataProducer = null;
|
|
|
|
// mediasoup Consumers.
|
|
// @type {Map<String, mediasoupClient.Consumer>}
|
|
this._consumers = new Map();
|
|
|
|
// mediasoup DataConsumers.
|
|
// @type {Map<String, mediasoupClient.DataConsumer>}
|
|
this._dataConsumers = new Map();
|
|
|
|
// Map of webcam MediaDeviceInfos indexed by deviceId.
|
|
// @type {Map<String, MediaDeviceInfos>}
|
|
this._webcams = new Map();
|
|
|
|
// Local Webcam.
|
|
// @type {Object} with:
|
|
// - {MediaDeviceInfo} [device]
|
|
// - {String} [resolution] - 'qvga' / 'vga' / 'hd'.
|
|
this._webcam =
|
|
{
|
|
device : null,
|
|
resolution : 'hd'
|
|
};
|
|
|
|
if (this._e2eKey && e2e.isSupported())
|
|
{
|
|
e2e.setCryptoKey('setCryptoKey', this._e2eKey, true);
|
|
}
|
|
}
|
|
|
|
close()
|
|
{
|
|
if (this._closed)
|
|
return;
|
|
|
|
this._closed = true;
|
|
|
|
logger.debug('close()');
|
|
|
|
// Close protoo Peer
|
|
this._protoo.close();
|
|
|
|
// Close mediasoup Transports.
|
|
if (this._sendTransport)
|
|
this._sendTransport.close();
|
|
|
|
if (this._recvTransport)
|
|
this._recvTransport.close();
|
|
|
|
store.dispatch(
|
|
stateActions.setRoomState('closed'));
|
|
}
|
|
|
|
async join()
|
|
{
|
|
store.dispatch(
|
|
stateActions.setMediasoupClientVersion(mediasoupClient.version));
|
|
|
|
const protooTransport = new protooClient.WebSocketTransport(this._protooUrl);
|
|
|
|
this._protoo = new protooClient.Peer(protooTransport);
|
|
|
|
store.dispatch(
|
|
stateActions.setRoomState('connecting'));
|
|
|
|
this._protoo.on('open', () => this._joinRoom());
|
|
|
|
this._protoo.on('failed', () =>
|
|
{
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : 'WebSocket连接失败'
|
|
}));
|
|
});
|
|
|
|
this._protoo.on('disconnected', () =>
|
|
{
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : 'WebSocket已断开连接'
|
|
}));
|
|
|
|
// Close mediasoup Transports.
|
|
if (this._sendTransport)
|
|
{
|
|
this._sendTransport.close();
|
|
this._sendTransport = null;
|
|
}
|
|
|
|
if (this._recvTransport)
|
|
{
|
|
this._recvTransport.close();
|
|
this._recvTransport = null;
|
|
}
|
|
|
|
store.dispatch(
|
|
stateActions.setRoomState('closed'));
|
|
});
|
|
|
|
this._protoo.on('close', () =>
|
|
{
|
|
if (this._closed)
|
|
return;
|
|
|
|
this.close();
|
|
});
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
this._protoo.on('request', async (request, accept, reject) =>
|
|
{
|
|
logger.debug(
|
|
'proto "request" event [method:%s, data:%o]',
|
|
request.method, request.data);
|
|
|
|
switch (request.method)
|
|
{
|
|
case 'newConsumer':
|
|
{
|
|
if (!this._consume)
|
|
{
|
|
reject(403, 'I do not want to consume');
|
|
|
|
break;
|
|
}
|
|
|
|
const {
|
|
peerId,
|
|
producerId,
|
|
id,
|
|
kind,
|
|
rtpParameters,
|
|
type,
|
|
appData,
|
|
producerPaused
|
|
} = request.data;
|
|
|
|
try
|
|
{
|
|
const consumer = await this._recvTransport.consume(
|
|
{
|
|
id,
|
|
producerId,
|
|
kind,
|
|
rtpParameters,
|
|
// NOTE: Force streamId to be same in mic and webcam and different
|
|
// in screen sharing so libwebrtc will just try to sync mic and
|
|
// webcam streams from the same remote peer.
|
|
streamId : `${peerId}-${appData.share ? 'share' : 'mic-webcam'}`,
|
|
appData : { ...appData, peerId } // Trick.
|
|
});
|
|
|
|
if (this._e2eKey && e2e.isSupported())
|
|
{
|
|
e2e.setupReceiverTransform(consumer.rtpReceiver);
|
|
}
|
|
|
|
// Store in the map.
|
|
this._consumers.set(consumer.id, consumer);
|
|
|
|
consumer.on('transportclose', () =>
|
|
{
|
|
this._consumers.delete(consumer.id);
|
|
});
|
|
|
|
const { spatialLayers, temporalLayers } =
|
|
mediasoupClient.parseScalabilityMode(
|
|
consumer.rtpParameters.encodings[0].scalabilityMode);
|
|
|
|
store.dispatch(stateActions.addConsumer(
|
|
{
|
|
id : consumer.id,
|
|
type : type,
|
|
locallyPaused : false,
|
|
remotelyPaused : producerPaused,
|
|
rtpParameters : consumer.rtpParameters,
|
|
spatialLayers : spatialLayers,
|
|
temporalLayers : temporalLayers,
|
|
preferredSpatialLayer : spatialLayers - 1,
|
|
preferredTemporalLayer : temporalLayers - 1,
|
|
priority : 1,
|
|
codec : consumer.rtpParameters.codecs[0].mimeType.split('/')[1],
|
|
track : consumer.track
|
|
},
|
|
peerId));
|
|
|
|
// We are ready. Answer the protoo request so the server will
|
|
// resume this Consumer (which was paused for now if video).
|
|
accept();
|
|
|
|
// If audio-only mode is enabled, pause it.
|
|
if (consumer.kind === 'video' && store.getState().me.audioOnly)
|
|
this._pauseConsumer(consumer);
|
|
}
|
|
catch (error)
|
|
{
|
|
logger.error('"newConsumer" request failed:%o', error);
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `创建用户时出错: ${error}`
|
|
}));
|
|
|
|
throw error;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case 'newDataConsumer':
|
|
{
|
|
if (!this._consume)
|
|
{
|
|
reject(403, 'I do not want to data consume');
|
|
|
|
break;
|
|
}
|
|
|
|
if (!this._useDataChannel)
|
|
{
|
|
reject(403, 'I do not want DataChannels');
|
|
|
|
break;
|
|
}
|
|
|
|
const {
|
|
peerId, // NOTE: Null if bot.
|
|
dataProducerId,
|
|
id,
|
|
sctpStreamParameters,
|
|
label,
|
|
protocol,
|
|
appData
|
|
} = request.data;
|
|
|
|
try
|
|
{
|
|
const dataConsumer = await this._recvTransport.consumeData(
|
|
{
|
|
id,
|
|
dataProducerId,
|
|
sctpStreamParameters,
|
|
label,
|
|
protocol,
|
|
appData : { ...appData, peerId } // Trick.
|
|
});
|
|
|
|
// Store in the map.
|
|
this._dataConsumers.set(dataConsumer.id, dataConsumer);
|
|
|
|
dataConsumer.on('transportclose', () =>
|
|
{
|
|
this._dataConsumers.delete(dataConsumer.id);
|
|
});
|
|
|
|
dataConsumer.on('open', () =>
|
|
{
|
|
logger.debug('DataConsumer "open" event');
|
|
});
|
|
|
|
dataConsumer.on('close', () =>
|
|
{
|
|
logger.warn('DataConsumer "close" event');
|
|
|
|
this._dataConsumers.delete(dataConsumer.id);
|
|
|
|
// store.dispatch(requestActions.notify(
|
|
// {
|
|
// type : 'error',
|
|
// text : `${peerId} 退出房间`
|
|
// }));
|
|
});
|
|
|
|
dataConsumer.on('error', (error) =>
|
|
{
|
|
logger.error('DataConsumer "error" event:%o', error);
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `DataConsumer 错误: ${error}`
|
|
}));
|
|
});
|
|
|
|
dataConsumer.on('message', (message) =>
|
|
{
|
|
logger.debug(
|
|
'DataConsumer "message" event [streamId:%d]',
|
|
dataConsumer.sctpStreamParameters.streamId);
|
|
|
|
// TODO: For debugging.
|
|
window.DC_MESSAGE = message;
|
|
|
|
if (message instanceof ArrayBuffer)
|
|
{
|
|
const view = new DataView(message);
|
|
const number = view.getUint32();
|
|
|
|
if (number == Math.pow(2, 32) - 1)
|
|
{
|
|
logger.warn('dataChannelTest finished!');
|
|
|
|
this._nextDataChannelTestNumber = 0;
|
|
|
|
return;
|
|
}
|
|
|
|
if (number > this._nextDataChannelTestNumber)
|
|
{
|
|
logger.warn(
|
|
'dataChannelTest: %s packets missing',
|
|
number - this._nextDataChannelTestNumber);
|
|
}
|
|
|
|
this._nextDataChannelTestNumber = number + 1;
|
|
|
|
return;
|
|
}
|
|
else if (typeof message !== 'string')
|
|
{
|
|
logger.warn('ignoring DataConsumer "message" (not a string)');
|
|
|
|
return;
|
|
}
|
|
|
|
switch (dataConsumer.label)
|
|
{
|
|
case 'chat':
|
|
{
|
|
const { peers } = store.getState();
|
|
const peersArray = Object.keys(peers)
|
|
.map((_peerId) => peers[_peerId]);
|
|
const sendingPeer = peersArray
|
|
.find((peer) => peer.dataConsumers.includes(dataConsumer.id));
|
|
|
|
if (!sendingPeer)
|
|
{
|
|
logger.warn('DataConsumer "message" from unknown peer');
|
|
|
|
break;
|
|
}
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'userMessage',
|
|
title : `${sendingPeer.displayName}`,
|
|
text : message,
|
|
timeout : 5000
|
|
}));
|
|
|
|
break;
|
|
}
|
|
|
|
case 'bot':
|
|
{
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
title : 'Message from Bot:',
|
|
text : message,
|
|
timeout : 5000
|
|
}));
|
|
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
// TODO: REMOVE
|
|
window.DC = dataConsumer;
|
|
|
|
store.dispatch(stateActions.addDataConsumer(
|
|
{
|
|
id : dataConsumer.id,
|
|
sctpStreamParameters : dataConsumer.sctpStreamParameters,
|
|
label : dataConsumer.label,
|
|
protocol : dataConsumer.protocol
|
|
},
|
|
peerId));
|
|
|
|
// We are ready. Answer the protoo request.
|
|
accept();
|
|
}
|
|
catch (error)
|
|
{
|
|
logger.error('"newDataConsumer" request failed:%o', error);
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `创建DataConsumer时出错: ${error}`
|
|
}));
|
|
|
|
throw error;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
this._protoo.on('notification', (notification) =>
|
|
{
|
|
logger.debug(
|
|
'proto "notification" event [method:%s, data:%o]',
|
|
notification.method, notification.data);
|
|
|
|
switch (notification.method)
|
|
{
|
|
case 'mediasoup-version':
|
|
{
|
|
const { version } = notification.data;
|
|
|
|
store.dispatch(
|
|
stateActions.setMediasoupVersion(version));
|
|
|
|
break;
|
|
}
|
|
|
|
case 'producerScore':
|
|
{
|
|
const { producerId, score } = notification.data;
|
|
|
|
store.dispatch(
|
|
stateActions.setProducerScore(producerId, score));
|
|
|
|
break;
|
|
}
|
|
|
|
case 'newPeer':
|
|
{
|
|
const peer = notification.data;
|
|
|
|
store.dispatch(
|
|
stateActions.addPeer(
|
|
{ ...peer, consumers: [], dataConsumers: [] }));
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
text : `${peer.displayName} 加入房间`
|
|
}));
|
|
|
|
break;
|
|
}
|
|
|
|
case 'peerClosed':
|
|
{
|
|
const { peerId } = notification.data;
|
|
|
|
store.dispatch(
|
|
stateActions.removePeer(peerId));
|
|
|
|
break;
|
|
}
|
|
|
|
case 'peerDisplayNameChanged':
|
|
{
|
|
const { peerId, displayName, oldDisplayName } = notification.data;
|
|
|
|
store.dispatch(
|
|
stateActions.setPeerDisplayName(displayName, peerId));
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
text : `${oldDisplayName} is now ${displayName}`
|
|
}));
|
|
|
|
break;
|
|
}
|
|
|
|
case 'downlinkBwe':
|
|
{
|
|
logger.debug('\'downlinkBwe\' event:%o', notification.data);
|
|
|
|
break;
|
|
}
|
|
|
|
case 'consumerClosed':
|
|
{
|
|
const { consumerId } = notification.data;
|
|
const consumer = this._consumers.get(consumerId);
|
|
|
|
if (!consumer)
|
|
break;
|
|
|
|
consumer.close();
|
|
this._consumers.delete(consumerId);
|
|
|
|
const { peerId } = consumer.appData;
|
|
|
|
store.dispatch(
|
|
stateActions.removeConsumer(consumerId, peerId));
|
|
|
|
break;
|
|
}
|
|
|
|
case 'consumerPaused':
|
|
{
|
|
const { consumerId } = notification.data;
|
|
const consumer = this._consumers.get(consumerId);
|
|
|
|
if (!consumer)
|
|
break;
|
|
|
|
consumer.pause();
|
|
|
|
store.dispatch(
|
|
stateActions.setConsumerPaused(consumerId, 'remote'));
|
|
|
|
break;
|
|
}
|
|
|
|
case 'consumerResumed':
|
|
{
|
|
const { consumerId } = notification.data;
|
|
const consumer = this._consumers.get(consumerId);
|
|
|
|
if (!consumer)
|
|
break;
|
|
|
|
consumer.resume();
|
|
|
|
store.dispatch(
|
|
stateActions.setConsumerResumed(consumerId, 'remote'));
|
|
|
|
break;
|
|
}
|
|
|
|
case 'consumerLayersChanged':
|
|
{
|
|
const { consumerId, spatialLayer, temporalLayer } = notification.data;
|
|
const consumer = this._consumers.get(consumerId);
|
|
|
|
if (!consumer)
|
|
break;
|
|
|
|
store.dispatch(stateActions.setConsumerCurrentLayers(
|
|
consumerId, spatialLayer, temporalLayer));
|
|
|
|
break;
|
|
}
|
|
|
|
case 'consumerScore':
|
|
{
|
|
const { consumerId, score } = notification.data;
|
|
|
|
store.dispatch(
|
|
stateActions.setConsumerScore(consumerId, score));
|
|
|
|
break;
|
|
}
|
|
|
|
case 'dataConsumerClosed':
|
|
{
|
|
const { dataConsumerId } = notification.data;
|
|
const dataConsumer = this._dataConsumers.get(dataConsumerId);
|
|
|
|
if (!dataConsumer)
|
|
break;
|
|
|
|
dataConsumer.close();
|
|
this._dataConsumers.delete(dataConsumerId);
|
|
|
|
const { peerId } = dataConsumer.appData;
|
|
|
|
store.dispatch(
|
|
stateActions.removeDataConsumer(dataConsumerId, peerId));
|
|
|
|
break;
|
|
}
|
|
|
|
case 'activeSpeaker':
|
|
{
|
|
const { peerId } = notification.data;
|
|
|
|
store.dispatch(
|
|
stateActions.setRoomActiveSpeaker(peerId));
|
|
|
|
break;
|
|
}
|
|
|
|
default:
|
|
{
|
|
logger.error(
|
|
'unknown protoo notification.method "%s"', notification.method);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
async enableMic()
|
|
{
|
|
logger.debug('enableMic()');
|
|
|
|
if (this._micProducer)
|
|
return;
|
|
|
|
if (!this._mediasoupDevice.canProduce('audio'))
|
|
{
|
|
logger.error('enableMic() | cannot produce audio');
|
|
|
|
return;
|
|
}
|
|
|
|
let track;
|
|
|
|
try
|
|
{
|
|
if (!this._externalVideo)
|
|
{
|
|
logger.debug('enableMic() | calling getUserMedia()');
|
|
|
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
|
|
track = stream.getAudioTracks()[0];
|
|
}
|
|
else
|
|
{
|
|
const stream = await this._getExternalVideoStream();
|
|
|
|
track = stream.getAudioTracks()[0].clone();
|
|
}
|
|
|
|
this._micProducer = await this._sendTransport.produce(
|
|
{
|
|
track,
|
|
codecOptions :
|
|
{
|
|
opusStereo : true,
|
|
opusDtx : true,
|
|
opusFec : true,
|
|
opusNack : true
|
|
}
|
|
// NOTE: for testing codec selection.
|
|
// codec : this._mediasoupDevice.rtpCapabilities.codecs
|
|
// .find((codec) => codec.mimeType.toLowerCase() === 'audio/pcma')
|
|
});
|
|
|
|
if (this._e2eKey && e2e.isSupported())
|
|
{
|
|
e2e.setupSenderTransform(this._micProducer.rtpSender);
|
|
}
|
|
|
|
store.dispatch(stateActions.addProducer(
|
|
{
|
|
id : this._micProducer.id,
|
|
paused : this._micProducer.paused,
|
|
track : this._micProducer.track,
|
|
rtpParameters : this._micProducer.rtpParameters,
|
|
codec : this._micProducer.rtpParameters.codecs[0].mimeType.split('/')[1]
|
|
}));
|
|
|
|
this._micProducer.on('transportclose', () =>
|
|
{
|
|
this._micProducer = null;
|
|
});
|
|
|
|
this._micProducer.on('trackended', () =>
|
|
{
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : '麦克风断开!'
|
|
}));
|
|
|
|
this.disableMic()
|
|
.catch(() => {});
|
|
});
|
|
}
|
|
catch (error)
|
|
{
|
|
logger.error('enableMic() | failed:%o', error);
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `启用麦克风时出错: ${error}`
|
|
}));
|
|
|
|
if (track)
|
|
track.stop();
|
|
}
|
|
}
|
|
|
|
async disableMic()
|
|
{
|
|
logger.debug('disableMic()');
|
|
|
|
if (!this._micProducer)
|
|
return;
|
|
|
|
this._micProducer.close();
|
|
|
|
store.dispatch(
|
|
stateActions.removeProducer(this._micProducer.id));
|
|
|
|
try
|
|
{
|
|
await this._protoo.request(
|
|
'closeProducer', { producerId: this._micProducer.id });
|
|
}
|
|
catch (error)
|
|
{
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `关闭服务器端麦克风生成器时出错: ${error}`
|
|
}));
|
|
}
|
|
|
|
this._micProducer = null;
|
|
}
|
|
|
|
async muteMic()
|
|
{
|
|
logger.debug('muteMic()');
|
|
|
|
this._micProducer.pause();
|
|
|
|
try
|
|
{
|
|
await this._protoo.request(
|
|
'pauseProducer', { producerId: this._micProducer.id });
|
|
|
|
store.dispatch(
|
|
stateActions.setProducerPaused(this._micProducer.id));
|
|
}
|
|
catch (error)
|
|
{
|
|
logger.error('muteMic() | failed: %o', error);
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `暂停服务器端麦克风生成器时出错: ${error}`
|
|
}));
|
|
}
|
|
}
|
|
|
|
async unmuteMic()
|
|
{
|
|
logger.debug('unmuteMic()');
|
|
|
|
this._micProducer.resume();
|
|
|
|
try
|
|
{
|
|
await this._protoo.request(
|
|
'resumeProducer', { producerId: this._micProducer.id });
|
|
|
|
store.dispatch(
|
|
stateActions.setProducerResumed(this._micProducer.id));
|
|
}
|
|
catch (error)
|
|
{
|
|
logger.error('unmuteMic() | failed: %o', error);
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `恢复服务器端麦克风生成器时出错: ${error}`
|
|
}));
|
|
}
|
|
}
|
|
|
|
async enableWebcam()
|
|
{
|
|
logger.debug('enableWebcam()');
|
|
|
|
if (this._webcamProducer)
|
|
return;
|
|
else if (this._shareProducer)
|
|
await this.disableShare();
|
|
|
|
if (!this._mediasoupDevice.canProduce('video'))
|
|
{
|
|
logger.error('enableWebcam() | cannot produce video');
|
|
|
|
return;
|
|
}
|
|
|
|
let track;
|
|
let device;
|
|
|
|
store.dispatch(
|
|
stateActions.setWebcamInProgress(true));
|
|
|
|
try
|
|
{
|
|
if (!this._externalVideo)
|
|
{
|
|
await this._updateWebcams();
|
|
device = this._webcam.device;
|
|
|
|
const { resolution } = this._webcam;
|
|
|
|
if (!device)
|
|
throw new Error('no webcam devices');
|
|
|
|
logger.debug('enableWebcam() | calling getUserMedia()');
|
|
|
|
const stream = await navigator.mediaDevices.getUserMedia(
|
|
{
|
|
video :
|
|
{
|
|
deviceId : { ideal: device.deviceId },
|
|
...VIDEO_CONSTRAINS[resolution]
|
|
}
|
|
});
|
|
|
|
track = stream.getVideoTracks()[0];
|
|
}
|
|
else
|
|
{
|
|
device = { label: 'external video' };
|
|
|
|
const stream = await this._getExternalVideoStream();
|
|
|
|
track = stream.getVideoTracks()[0].clone();
|
|
}
|
|
|
|
let encodings;
|
|
let codec;
|
|
const codecOptions =
|
|
{
|
|
videoGoogleStartBitrate : 1000
|
|
};
|
|
|
|
if (this._forceVP8)
|
|
{
|
|
codec = this._mediasoupDevice.rtpCapabilities.codecs
|
|
.find((c) => c.mimeType.toLowerCase() === 'video/vp8');
|
|
|
|
if (!codec)
|
|
{
|
|
throw new Error('desired VP8 codec+configuration is not supported');
|
|
}
|
|
}
|
|
else if (this._forceH264)
|
|
{
|
|
codec = this._mediasoupDevice.rtpCapabilities.codecs
|
|
.find((c) => c.mimeType.toLowerCase() === 'video/h264');
|
|
|
|
if (!codec)
|
|
{
|
|
throw new Error('desired H264 codec+configuration is not supported');
|
|
}
|
|
}
|
|
else if (this._forceVP9)
|
|
{
|
|
codec = this._mediasoupDevice.rtpCapabilities.codecs
|
|
.find((c) => c.mimeType.toLowerCase() === 'video/vp9');
|
|
|
|
if (!codec)
|
|
{
|
|
throw new Error('desired VP9 codec+configuration is not supported');
|
|
}
|
|
}
|
|
|
|
if (this._enableWebcamLayers)
|
|
{
|
|
// If VP9 is the only available video codec then use SVC.
|
|
const firstVideoCodec = this._mediasoupDevice
|
|
.rtpCapabilities
|
|
.codecs
|
|
.find((c) => c.kind === 'video');
|
|
|
|
// VP9 with SVC.
|
|
if (
|
|
(this._forceVP9 && codec) ||
|
|
firstVideoCodec.mimeType.toLowerCase() === 'video/vp9'
|
|
)
|
|
{
|
|
encodings =
|
|
[
|
|
{
|
|
maxBitrate : 5000000,
|
|
scalabilityMode : this._webcamScalabilityMode || 'L3T3_KEY'
|
|
}
|
|
];
|
|
}
|
|
// VP8 or H264 with simulcast.
|
|
else
|
|
{
|
|
encodings =
|
|
[
|
|
{
|
|
scaleResolutionDownBy : 1,
|
|
maxBitrate : 5000000,
|
|
scalabilityMode : this._webcamScalabilityMode || 'L1T3'
|
|
}
|
|
];
|
|
|
|
if (this._numSimulcastStreams > 1)
|
|
{
|
|
encodings.unshift(
|
|
{
|
|
scaleResolutionDownBy : 2,
|
|
maxBitrate : 1000000,
|
|
scalabilityMode : this._webcamScalabilityMode || 'L1T3'
|
|
}
|
|
);
|
|
}
|
|
|
|
if (this._numSimulcastStreams > 2)
|
|
{
|
|
encodings.unshift(
|
|
{
|
|
scaleResolutionDownBy : 4,
|
|
maxBitrate : 500000,
|
|
scalabilityMode : this._webcamScalabilityMode || 'L1T3'
|
|
}
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
this._webcamProducer = await this._sendTransport.produce(
|
|
{
|
|
track,
|
|
encodings,
|
|
codecOptions,
|
|
codec
|
|
});
|
|
|
|
if (this._e2eKey && e2e.isSupported())
|
|
{
|
|
e2e.setupSenderTransform(this._webcamProducer.rtpSender);
|
|
}
|
|
|
|
store.dispatch(stateActions.addProducer(
|
|
{
|
|
id : this._webcamProducer.id,
|
|
deviceLabel : device.label,
|
|
type : this._getWebcamType(device),
|
|
paused : this._webcamProducer.paused,
|
|
track : this._webcamProducer.track,
|
|
rtpParameters : this._webcamProducer.rtpParameters,
|
|
codec : this._webcamProducer.rtpParameters.codecs[0].mimeType.split('/')[1]
|
|
}));
|
|
|
|
this._webcamProducer.on('transportclose', () =>
|
|
{
|
|
this._webcamProducer = null;
|
|
});
|
|
|
|
this._webcamProducer.on('trackended', () =>
|
|
{
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : '网络摄像头已断开连接!'
|
|
}));
|
|
|
|
this.disableWebcam()
|
|
.catch(() => {});
|
|
});
|
|
}
|
|
catch (error)
|
|
{
|
|
logger.error('enableWebcam() | failed:%o', error);
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `启用网络摄像头时出错: ${error}`
|
|
}));
|
|
|
|
if (track)
|
|
track.stop();
|
|
}
|
|
|
|
store.dispatch(
|
|
stateActions.setWebcamInProgress(false));
|
|
}
|
|
|
|
async disableWebcam()
|
|
{
|
|
logger.debug('disableWebcam()');
|
|
|
|
if (!this._webcamProducer)
|
|
return;
|
|
|
|
this._webcamProducer.close();
|
|
|
|
store.dispatch(
|
|
stateActions.removeProducer(this._webcamProducer.id));
|
|
|
|
try
|
|
{
|
|
await this._protoo.request(
|
|
'closeProducer', { producerId: this._webcamProducer.id });
|
|
}
|
|
catch (error)
|
|
{
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `关闭服务器端网络摄像头生成器时出错: ${error}`
|
|
}));
|
|
}
|
|
|
|
this._webcamProducer = null;
|
|
}
|
|
|
|
async changeWebcam()
|
|
{
|
|
logger.debug('changeWebcam()');
|
|
|
|
store.dispatch(
|
|
stateActions.setWebcamInProgress(true));
|
|
|
|
try
|
|
{
|
|
await this._updateWebcams();
|
|
|
|
const array = Array.from(this._webcams.keys());
|
|
const len = array.length;
|
|
const deviceId =
|
|
this._webcam.device ? this._webcam.device.deviceId : undefined;
|
|
let idx = array.indexOf(deviceId);
|
|
|
|
if (idx < len - 1)
|
|
idx++;
|
|
else
|
|
idx = 0;
|
|
|
|
this._webcam.device = this._webcams.get(array[idx]);
|
|
|
|
logger.debug(
|
|
'changeWebcam() | new selected webcam [device:%o]',
|
|
this._webcam.device);
|
|
|
|
// Reset video resolution to HD.
|
|
this._webcam.resolution = 'hd';
|
|
|
|
if (!this._webcam.device)
|
|
throw new Error('no webcam devices');
|
|
|
|
// Closing the current video track before asking for a new one (mobiles do not like
|
|
// having both front/back cameras open at the same time).
|
|
this._webcamProducer.track.stop();
|
|
|
|
logger.debug('changeWebcam() | calling getUserMedia()');
|
|
|
|
const stream = await navigator.mediaDevices.getUserMedia(
|
|
{
|
|
video :
|
|
{
|
|
deviceId : { exact: this._webcam.device.deviceId },
|
|
...VIDEO_CONSTRAINS[this._webcam.resolution]
|
|
}
|
|
});
|
|
|
|
const track = stream.getVideoTracks()[0];
|
|
|
|
await this._webcamProducer.replaceTrack({ track });
|
|
|
|
store.dispatch(
|
|
stateActions.setProducerTrack(this._webcamProducer.id, track));
|
|
}
|
|
catch (error)
|
|
{
|
|
logger.error('changeWebcam() | failed: %o', error);
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `无法更改网络摄像头: ${error}`
|
|
}));
|
|
}
|
|
|
|
store.dispatch(
|
|
stateActions.setWebcamInProgress(false));
|
|
}
|
|
|
|
async changeWebcamResolution()
|
|
{
|
|
logger.debug('changeWebcamResolution()');
|
|
|
|
store.dispatch(
|
|
stateActions.setWebcamInProgress(true));
|
|
|
|
try
|
|
{
|
|
switch (this._webcam.resolution)
|
|
{
|
|
case 'qvga':
|
|
this._webcam.resolution = 'vga';
|
|
break;
|
|
case 'vga':
|
|
this._webcam.resolution = 'hd';
|
|
break;
|
|
case 'hd':
|
|
this._webcam.resolution = 'qvga';
|
|
break;
|
|
default:
|
|
this._webcam.resolution = 'hd';
|
|
}
|
|
|
|
logger.debug('changeWebcamResolution() | calling getUserMedia()');
|
|
|
|
const stream = await navigator.mediaDevices.getUserMedia(
|
|
{
|
|
video :
|
|
{
|
|
deviceId : { exact: this._webcam.device.deviceId },
|
|
...VIDEO_CONSTRAINS[this._webcam.resolution]
|
|
}
|
|
});
|
|
|
|
const track = stream.getVideoTracks()[0];
|
|
|
|
await this._webcamProducer.replaceTrack({ track });
|
|
|
|
store.dispatch(
|
|
stateActions.setProducerTrack(this._webcamProducer.id, track));
|
|
}
|
|
catch (error)
|
|
{
|
|
logger.error('changeWebcamResolution() | failed: %o', error);
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `无法更改网络摄像头分辨率: ${error}`
|
|
}));
|
|
}
|
|
|
|
store.dispatch(
|
|
stateActions.setWebcamInProgress(false));
|
|
}
|
|
|
|
async enableShare()
|
|
{
|
|
logger.debug('enableShare()');
|
|
|
|
if (this._shareProducer)
|
|
return;
|
|
else if (this._webcamProducer)
|
|
await this.disableWebcam();
|
|
|
|
if (!this._mediasoupDevice.canProduce('video'))
|
|
{
|
|
logger.error('enableShare() | cannot produce video');
|
|
|
|
return;
|
|
}
|
|
|
|
let track;
|
|
|
|
store.dispatch(
|
|
stateActions.setShareInProgress(true));
|
|
|
|
try
|
|
{
|
|
logger.debug('enableShare() | calling getUserMedia()');
|
|
|
|
const stream = await navigator.mediaDevices.getDisplayMedia(
|
|
{
|
|
audio : false,
|
|
video :
|
|
{
|
|
displaySurface : 'monitor',
|
|
logicalSurface : true,
|
|
cursor : true,
|
|
width : { max: 1920 },
|
|
height : { max: 1080 },
|
|
frameRate : { max: 30 }
|
|
}
|
|
});
|
|
|
|
// May mean cancelled (in some implementations).
|
|
if (!stream)
|
|
{
|
|
store.dispatch(
|
|
stateActions.setShareInProgress(true));
|
|
|
|
return;
|
|
}
|
|
|
|
track = stream.getVideoTracks()[0];
|
|
|
|
let encodings;
|
|
let codec;
|
|
const codecOptions =
|
|
{
|
|
videoGoogleStartBitrate : 1000
|
|
};
|
|
|
|
if (this._forceVP8)
|
|
{
|
|
codec = this._mediasoupDevice.rtpCapabilities.codecs
|
|
.find((c) => c.mimeType.toLowerCase() === 'video/vp8');
|
|
|
|
if (!codec)
|
|
{
|
|
throw new Error('desired VP8 codec+configuration is not supported');
|
|
}
|
|
}
|
|
else if (this._forceH264)
|
|
{
|
|
codec = this._mediasoupDevice.rtpCapabilities.codecs
|
|
.find((c) => c.mimeType.toLowerCase() === 'video/h264');
|
|
|
|
if (!codec)
|
|
{
|
|
throw new Error('desired H264 codec+configuration is not supported');
|
|
}
|
|
}
|
|
else if (this._forceVP9)
|
|
{
|
|
codec = this._mediasoupDevice.rtpCapabilities.codecs
|
|
.find((c) => c.mimeType.toLowerCase() === 'video/vp9');
|
|
|
|
if (!codec)
|
|
{
|
|
throw new Error('desired VP9 codec+configuration is not supported');
|
|
}
|
|
}
|
|
|
|
if (this._enableSharingLayers)
|
|
{
|
|
// If VP9 is the only available video codec then use SVC.
|
|
const firstVideoCodec = this._mediasoupDevice
|
|
.rtpCapabilities
|
|
.codecs
|
|
.find((c) => c.kind === 'video');
|
|
|
|
// VP9 with SVC.
|
|
if (
|
|
(this._forceVP9 && codec) ||
|
|
firstVideoCodec.mimeType.toLowerCase() === 'video/vp9'
|
|
)
|
|
{
|
|
encodings =
|
|
[
|
|
{
|
|
maxBitrate : 5000000,
|
|
scalabilityMode : this._sharingScalabilityMode || 'L3T3',
|
|
dtx : true
|
|
}
|
|
];
|
|
}
|
|
// VP8 or H264 with simulcast.
|
|
else
|
|
{
|
|
encodings =
|
|
[
|
|
{
|
|
scaleResolutionDownBy : 1,
|
|
maxBitrate : 5000000,
|
|
scalabilityMode : this._sharingScalabilityMode || 'L1T3',
|
|
dtx : true
|
|
}
|
|
];
|
|
|
|
if (this._numSimulcastStreams > 1)
|
|
{
|
|
encodings.unshift(
|
|
{
|
|
scaleResolutionDownBy : 2,
|
|
maxBitrate : 1000000,
|
|
scalabilityMode : this._sharingScalabilityMode || 'L1T3',
|
|
dtx : true
|
|
}
|
|
);
|
|
}
|
|
|
|
if (this._numSimulcastStreams > 2)
|
|
{
|
|
encodings.unshift(
|
|
{
|
|
scaleResolutionDownBy : 4,
|
|
maxBitrate : 500000,
|
|
scalabilityMode : this._sharingScalabilityMode || 'L1T3',
|
|
dtx : true
|
|
}
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
this._shareProducer = await this._sendTransport.produce(
|
|
{
|
|
track,
|
|
encodings,
|
|
codecOptions,
|
|
codec,
|
|
appData :
|
|
{
|
|
share : true
|
|
}
|
|
});
|
|
|
|
if (this._e2eKey && e2e.isSupported())
|
|
{
|
|
e2e.setupSenderTransform(this._shareProducer.rtpSender);
|
|
}
|
|
|
|
store.dispatch(stateActions.addProducer(
|
|
{
|
|
id : this._shareProducer.id,
|
|
type : 'share',
|
|
paused : this._shareProducer.paused,
|
|
track : this._shareProducer.track,
|
|
rtpParameters : this._shareProducer.rtpParameters,
|
|
codec : this._shareProducer.rtpParameters.codecs[0].mimeType.split('/')[1]
|
|
}));
|
|
|
|
this._shareProducer.on('transportclose', () =>
|
|
{
|
|
this._shareProducer = null;
|
|
});
|
|
|
|
this._shareProducer.on('trackended', () =>
|
|
{
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : '共享已断开连接!'
|
|
}));
|
|
|
|
this.disableShare()
|
|
.catch(() => {});
|
|
});
|
|
}
|
|
catch (error)
|
|
{
|
|
logger.error('enableShare() | failed:%o', error);
|
|
|
|
if (error.name !== 'NotAllowedError')
|
|
{
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `共享错误: ${error}`
|
|
}));
|
|
}
|
|
|
|
if (track)
|
|
track.stop();
|
|
}
|
|
|
|
store.dispatch(
|
|
stateActions.setShareInProgress(false));
|
|
}
|
|
|
|
async disableShare()
|
|
{
|
|
logger.debug('disableShare()');
|
|
|
|
if (!this._shareProducer)
|
|
return;
|
|
|
|
this._shareProducer.close();
|
|
|
|
store.dispatch(
|
|
stateActions.removeProducer(this._shareProducer.id));
|
|
|
|
try
|
|
{
|
|
await this._protoo.request(
|
|
'closeProducer', { producerId: this._shareProducer.id });
|
|
}
|
|
catch (error)
|
|
{
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `关闭服务器端共享生成器时出错: ${error}`
|
|
}));
|
|
}
|
|
|
|
this._shareProducer = null;
|
|
}
|
|
|
|
async enableAudioOnly()
|
|
{
|
|
logger.debug('enableAudioOnly()');
|
|
|
|
store.dispatch(
|
|
stateActions.setAudioOnlyInProgress(true));
|
|
|
|
this.disableWebcam();
|
|
|
|
for (const consumer of this._consumers.values())
|
|
{
|
|
if (consumer.kind !== 'video')
|
|
continue;
|
|
|
|
this._pauseConsumer(consumer);
|
|
}
|
|
|
|
store.dispatch(
|
|
stateActions.setAudioOnlyState(true));
|
|
|
|
store.dispatch(
|
|
stateActions.setAudioOnlyInProgress(false));
|
|
}
|
|
|
|
async disableAudioOnly()
|
|
{
|
|
logger.debug('disableAudioOnly()');
|
|
|
|
store.dispatch(
|
|
stateActions.setAudioOnlyInProgress(true));
|
|
|
|
if (
|
|
!this._webcamProducer &&
|
|
this._produce &&
|
|
(cookiesManager.getDevices() || {}).webcamEnabled
|
|
)
|
|
{
|
|
this.enableWebcam();
|
|
}
|
|
|
|
for (const consumer of this._consumers.values())
|
|
{
|
|
if (consumer.kind !== 'video')
|
|
continue;
|
|
|
|
this._resumeConsumer(consumer);
|
|
}
|
|
|
|
store.dispatch(
|
|
stateActions.setAudioOnlyState(false));
|
|
|
|
store.dispatch(
|
|
stateActions.setAudioOnlyInProgress(false));
|
|
}
|
|
|
|
async muteAudio()
|
|
{
|
|
logger.debug('muteAudio()');
|
|
|
|
store.dispatch(
|
|
stateActions.setAudioMutedState(true));
|
|
}
|
|
|
|
async unmuteAudio()
|
|
{
|
|
logger.debug('unmuteAudio()');
|
|
|
|
store.dispatch(
|
|
stateActions.setAudioMutedState(false));
|
|
}
|
|
|
|
async restartIce()
|
|
{
|
|
logger.debug('restartIce()');
|
|
|
|
store.dispatch(
|
|
stateActions.setRestartIceInProgress(true));
|
|
|
|
try
|
|
{
|
|
if (this._sendTransport)
|
|
{
|
|
const iceParameters = await this._protoo.request(
|
|
'restartIce',
|
|
{ transportId: this._sendTransport.id });
|
|
|
|
await this._sendTransport.restartIce({ iceParameters });
|
|
}
|
|
|
|
if (this._recvTransport)
|
|
{
|
|
const iceParameters = await this._protoo.request(
|
|
'restartIce',
|
|
{ transportId: this._recvTransport.id });
|
|
|
|
await this._recvTransport.restartIce({ iceParameters });
|
|
}
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
text : 'ICE 重新启动'
|
|
}));
|
|
}
|
|
catch (error)
|
|
{
|
|
logger.error('restartIce() | failed:%o', error);
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `ICE 重启失败: ${error}`
|
|
}));
|
|
}
|
|
|
|
store.dispatch(
|
|
stateActions.setRestartIceInProgress(false));
|
|
}
|
|
|
|
async setMaxSendingSpatialLayer(spatialLayer)
|
|
{
|
|
logger.debug('setMaxSendingSpatialLayer() [spatialLayer:%s]', spatialLayer);
|
|
|
|
try
|
|
{
|
|
if (this._webcamProducer)
|
|
await this._webcamProducer.setMaxSpatialLayer(spatialLayer);
|
|
else if (this._shareProducer)
|
|
await this._shareProducer.setMaxSpatialLayer(spatialLayer);
|
|
}
|
|
catch (error)
|
|
{
|
|
logger.error('setMaxSendingSpatialLayer() | failed:%o', error);
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `设置最大发送视频空间层时出错: ${error}`
|
|
}));
|
|
}
|
|
}
|
|
|
|
async setConsumerPreferredLayers(consumerId, spatialLayer, temporalLayer)
|
|
{
|
|
logger.debug(
|
|
'setConsumerPreferredLayers() [consumerId:%s, spatialLayer:%s, temporalLayer:%s]',
|
|
consumerId, spatialLayer, temporalLayer);
|
|
|
|
try
|
|
{
|
|
await this._protoo.request(
|
|
'setConsumerPreferredLayers', { consumerId, spatialLayer, temporalLayer });
|
|
|
|
store.dispatch(stateActions.setConsumerPreferredLayers(
|
|
consumerId, spatialLayer, temporalLayer));
|
|
}
|
|
catch (error)
|
|
{
|
|
logger.error('setConsumerPreferredLayers() | failed:%o', error);
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `设置用户首选图层时出错: ${error}`
|
|
}));
|
|
}
|
|
}
|
|
|
|
async setConsumerPriority(consumerId, priority)
|
|
{
|
|
logger.debug(
|
|
'setConsumerPriority() [consumerId:%s, priority:%d]',
|
|
consumerId, priority);
|
|
|
|
try
|
|
{
|
|
await this._protoo.request('setConsumerPriority', { consumerId, priority });
|
|
|
|
store.dispatch(stateActions.setConsumerPriority(consumerId, priority));
|
|
}
|
|
catch (error)
|
|
{
|
|
logger.error('setConsumerPriority() | failed:%o', error);
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `设置用户优先级时出错: ${error}`
|
|
}));
|
|
}
|
|
}
|
|
|
|
async requestConsumerKeyFrame(consumerId)
|
|
{
|
|
logger.debug('requestConsumerKeyFrame() [consumerId:%s]', consumerId);
|
|
|
|
try
|
|
{
|
|
await this._protoo.request('requestConsumerKeyFrame', { consumerId });
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
text : '为视频用户请求关键帧'
|
|
}));
|
|
}
|
|
catch (error)
|
|
{
|
|
logger.error('requestConsumerKeyFrame() | failed:%o', error);
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `为用户请求关键帧时出错: ${error}`
|
|
}));
|
|
}
|
|
}
|
|
|
|
async enableChatDataProducer()
|
|
{
|
|
logger.debug('enableChatDataProducer()');
|
|
|
|
if (!this._useDataChannel)
|
|
return;
|
|
|
|
// NOTE: Should enable this code but it's useful for testing.
|
|
// if (this._chatDataProducer)
|
|
// return;
|
|
|
|
try
|
|
{
|
|
// Create chat DataProducer.
|
|
this._chatDataProducer = await this._sendTransport.produceData(
|
|
{
|
|
ordered : false,
|
|
maxRetransmits : 1,
|
|
label : 'chat',
|
|
priority : 'medium',
|
|
appData : { info: 'my-chat-DataProducer' }
|
|
});
|
|
|
|
store.dispatch(stateActions.addDataProducer(
|
|
{
|
|
id : this._chatDataProducer.id,
|
|
sctpStreamParameters : this._chatDataProducer.sctpStreamParameters,
|
|
label : this._chatDataProducer.label,
|
|
protocol : this._chatDataProducer.protocol
|
|
}));
|
|
|
|
this._chatDataProducer.on('transportclose', () =>
|
|
{
|
|
this._chatDataProducer = null;
|
|
});
|
|
|
|
this._chatDataProducer.on('open', () =>
|
|
{
|
|
logger.debug('chat DataProducer "open" event');
|
|
});
|
|
|
|
this._chatDataProducer.on('close', () =>
|
|
{
|
|
logger.error('chat DataProducer "close" event');
|
|
|
|
this._chatDataProducer = null;
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : '聊天数据生成器已关闭'
|
|
}));
|
|
});
|
|
|
|
this._chatDataProducer.on('error', (error) =>
|
|
{
|
|
logger.error('chat DataProducer "error" event:%o', error);
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `聊天数据生成器错误: ${error}`
|
|
}));
|
|
});
|
|
|
|
this._chatDataProducer.on('bufferedamountlow', () =>
|
|
{
|
|
logger.debug('chat DataProducer "bufferedamountlow" event');
|
|
});
|
|
}
|
|
catch (error)
|
|
{
|
|
logger.error('enableChatDataProducer() | failed:%o', error);
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `启用聊天数据生成器时出错: ${error}`
|
|
}));
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async enableBotDataProducer()
|
|
{
|
|
logger.debug('enableBotDataProducer()');
|
|
|
|
if (!this._useDataChannel)
|
|
return;
|
|
|
|
// NOTE: Should enable this code but it's useful for testing.
|
|
// if (this._botDataProducer)
|
|
// return;
|
|
|
|
try
|
|
{
|
|
// Create chat DataProducer.
|
|
this._botDataProducer = await this._sendTransport.produceData(
|
|
{
|
|
ordered : false,
|
|
maxPacketLifeTime : 2000,
|
|
label : 'bot',
|
|
priority : 'medium',
|
|
appData : { info: 'my-bot-DataProducer' }
|
|
});
|
|
|
|
store.dispatch(stateActions.addDataProducer(
|
|
{
|
|
id : this._botDataProducer.id,
|
|
sctpStreamParameters : this._botDataProducer.sctpStreamParameters,
|
|
label : this._botDataProducer.label,
|
|
protocol : this._botDataProducer.protocol
|
|
}));
|
|
|
|
this._botDataProducer.on('transportclose', () =>
|
|
{
|
|
this._botDataProducer = null;
|
|
});
|
|
|
|
this._botDataProducer.on('open', () =>
|
|
{
|
|
logger.debug('bot DataProducer "open" event');
|
|
});
|
|
|
|
this._botDataProducer.on('close', () =>
|
|
{
|
|
logger.error('bot DataProducer "close" event');
|
|
|
|
this._botDataProducer = null;
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : 'Bot DataProducer closed'
|
|
}));
|
|
});
|
|
|
|
this._botDataProducer.on('error', (error) =>
|
|
{
|
|
logger.error('bot DataProducer "error" event:%o', error);
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `Bot DataProducer 错误: ${error}`
|
|
}));
|
|
});
|
|
|
|
this._botDataProducer.on('bufferedamountlow', () =>
|
|
{
|
|
logger.debug('bot DataProducer "bufferedamountlow" event');
|
|
});
|
|
}
|
|
catch (error)
|
|
{
|
|
logger.error('enableBotDataProducer() | failed:%o', error);
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `启用bot DataProducter时出错: ${error}`
|
|
}));
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async sendChatMessage(text)
|
|
{
|
|
logger.debug('sendChatMessage() [text:"%s]', text);
|
|
|
|
if (!this._chatDataProducer)
|
|
{
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : '无聊天 DataProducter'
|
|
}));
|
|
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
this._chatDataProducer.send(text);
|
|
}
|
|
catch (error)
|
|
{
|
|
logger.error('chat DataProducer.send() failed:%o', error);
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `聊天DataProducter.send()失败: ${error}`
|
|
}));
|
|
}
|
|
}
|
|
|
|
async sendBotMessage(text)
|
|
{
|
|
logger.debug('sendBotMessage() [text:"%s]', text);
|
|
|
|
if (!this._botDataProducer)
|
|
{
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : 'No bot DataProducer'
|
|
}));
|
|
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
this._botDataProducer.send(text);
|
|
}
|
|
catch (error)
|
|
{
|
|
logger.error('bot DataProducer.send() failed:%o', error);
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `bot DataProducer.send() failed: ${error}`
|
|
}));
|
|
}
|
|
}
|
|
|
|
async changeDisplayName(displayName)
|
|
{
|
|
logger.debug('changeDisplayName() [displayName:"%s"]', displayName);
|
|
|
|
// Store in cookie.
|
|
cookiesManager.setUser({ displayName });
|
|
|
|
try
|
|
{
|
|
await this._protoo.request('changeDisplayName', { displayName });
|
|
|
|
this._displayName = displayName;
|
|
|
|
store.dispatch(
|
|
stateActions.setDisplayName(displayName));
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
text : '显示名称已更改'
|
|
}));
|
|
}
|
|
catch (error)
|
|
{
|
|
logger.error('changeDisplayName() | failed: %o', error);
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `无法更改显示名称: ${error}`
|
|
}));
|
|
|
|
// We need to refresh the component for it to render the previous
|
|
// displayName again.
|
|
store.dispatch(
|
|
stateActions.setDisplayName());
|
|
}
|
|
}
|
|
|
|
async getSendTransportRemoteStats()
|
|
{
|
|
logger.debug('getSendTransportRemoteStats()');
|
|
|
|
if (!this._sendTransport)
|
|
return;
|
|
|
|
return this._protoo.request(
|
|
'getTransportStats', { transportId: this._sendTransport.id });
|
|
}
|
|
|
|
async getRecvTransportRemoteStats()
|
|
{
|
|
logger.debug('getRecvTransportRemoteStats()');
|
|
|
|
if (!this._recvTransport)
|
|
return;
|
|
|
|
return this._protoo.request(
|
|
'getTransportStats', { transportId: this._recvTransport.id });
|
|
}
|
|
|
|
async getAudioRemoteStats()
|
|
{
|
|
logger.debug('getAudioRemoteStats()');
|
|
|
|
if (!this._micProducer)
|
|
return;
|
|
|
|
return this._protoo.request(
|
|
'getProducerStats', { producerId: this._micProducer.id });
|
|
}
|
|
|
|
async getVideoRemoteStats()
|
|
{
|
|
logger.debug('getVideoRemoteStats()');
|
|
|
|
const producer = this._webcamProducer || this._shareProducer;
|
|
|
|
if (!producer)
|
|
return;
|
|
|
|
return this._protoo.request(
|
|
'getProducerStats', { producerId: producer.id });
|
|
}
|
|
|
|
async getConsumerRemoteStats(consumerId)
|
|
{
|
|
logger.debug('getConsumerRemoteStats()');
|
|
|
|
const consumer = this._consumers.get(consumerId);
|
|
|
|
if (!consumer)
|
|
return;
|
|
|
|
return this._protoo.request('getConsumerStats', { consumerId });
|
|
}
|
|
|
|
async getChatDataProducerRemoteStats()
|
|
{
|
|
logger.debug('getChatDataProducerRemoteStats()');
|
|
|
|
const dataProducer = this._chatDataProducer;
|
|
|
|
if (!dataProducer)
|
|
return;
|
|
|
|
return this._protoo.request(
|
|
'getDataProducerStats', { dataProducerId: dataProducer.id });
|
|
}
|
|
|
|
async getBotDataProducerRemoteStats()
|
|
{
|
|
logger.debug('getBotDataProducerRemoteStats()');
|
|
|
|
const dataProducer = this._botDataProducer;
|
|
|
|
if (!dataProducer)
|
|
return;
|
|
|
|
return this._protoo.request(
|
|
'getDataProducerStats', { dataProducerId: dataProducer.id });
|
|
}
|
|
|
|
async getDataConsumerRemoteStats(dataConsumerId)
|
|
{
|
|
logger.debug('getDataConsumerRemoteStats()');
|
|
|
|
const dataConsumer = this._dataConsumers.get(dataConsumerId);
|
|
|
|
if (!dataConsumer)
|
|
return;
|
|
|
|
return this._protoo.request('getDataConsumerStats', { dataConsumerId });
|
|
}
|
|
|
|
async getSendTransportLocalStats()
|
|
{
|
|
logger.debug('getSendTransportLocalStats()');
|
|
|
|
if (!this._sendTransport)
|
|
return;
|
|
|
|
return this._sendTransport.getStats();
|
|
}
|
|
|
|
async getRecvTransportLocalStats()
|
|
{
|
|
logger.debug('getRecvTransportLocalStats()');
|
|
|
|
if (!this._recvTransport)
|
|
return;
|
|
|
|
return this._recvTransport.getStats();
|
|
}
|
|
|
|
async getAudioLocalStats()
|
|
{
|
|
logger.debug('getAudioLocalStats()');
|
|
|
|
if (!this._micProducer)
|
|
return;
|
|
|
|
return this._micProducer.getStats();
|
|
}
|
|
|
|
async getVideoLocalStats()
|
|
{
|
|
logger.debug('getVideoLocalStats()');
|
|
|
|
const producer = this._webcamProducer || this._shareProducer;
|
|
|
|
if (!producer)
|
|
return;
|
|
|
|
return producer.getStats();
|
|
}
|
|
|
|
async getConsumerLocalStats(consumerId)
|
|
{
|
|
const consumer = this._consumers.get(consumerId);
|
|
|
|
if (!consumer)
|
|
return;
|
|
|
|
return consumer.getStats();
|
|
}
|
|
|
|
async applyNetworkThrottle({ uplink, downlink, rtt, secret, packetLoss })
|
|
{
|
|
logger.debug(
|
|
'applyNetworkThrottle() [uplink:%s, downlink:%s, rtt:%s, packetLoss:%s]',
|
|
uplink, downlink, rtt, packetLoss);
|
|
|
|
try
|
|
{
|
|
await this._protoo.request(
|
|
'applyNetworkThrottle',
|
|
{ secret, uplink, downlink, rtt, packetLoss });
|
|
}
|
|
catch (error)
|
|
{
|
|
logger.error('applyNetworkThrottle() | failed:%o', error);
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `应用网络节流时出错: ${error}`
|
|
}));
|
|
}
|
|
}
|
|
|
|
async resetNetworkThrottle({ silent = false, secret })
|
|
{
|
|
logger.debug('resetNetworkThrottle()');
|
|
|
|
try
|
|
{
|
|
await this._protoo.request('resetNetworkThrottle', { secret });
|
|
}
|
|
catch (error)
|
|
{
|
|
if (!silent)
|
|
{
|
|
logger.error('resetNetworkThrottle() | failed:%o', error);
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `重置网络节流阀时出错: ${error}`
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
|
|
async _joinRoom()
|
|
{
|
|
logger.debug('_joinRoom()');
|
|
|
|
try
|
|
{
|
|
this._mediasoupDevice = new mediasoupClient.Device(
|
|
{
|
|
handlerName : this._handlerName
|
|
});
|
|
|
|
store.dispatch(stateActions.setRoomMediasoupClientHandler(
|
|
this._mediasoupDevice.handlerName
|
|
));
|
|
|
|
const routerRtpCapabilities =
|
|
await this._protoo.request('getRouterRtpCapabilities');
|
|
|
|
await this._mediasoupDevice.load({ routerRtpCapabilities });
|
|
|
|
// NOTE: Stuff to play remote audios due to browsers' new autoplay policy.
|
|
//
|
|
// Just get access to the mic and DO NOT close the mic track for a while.
|
|
// Super hack!
|
|
{
|
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
const audioTrack = stream.getAudioTracks()[0];
|
|
|
|
audioTrack.enabled = false;
|
|
|
|
setTimeout(() => audioTrack.stop(), 120000);
|
|
}
|
|
// Create mediasoup Transport for sending (unless we don't want to produce).
|
|
if (this._produce)
|
|
{
|
|
const transportInfo = await this._protoo.request(
|
|
'createWebRtcTransport',
|
|
{
|
|
forceTcp : this._forceTcp,
|
|
producing : true,
|
|
consuming : false,
|
|
sctpCapabilities : this._useDataChannel
|
|
? this._mediasoupDevice.sctpCapabilities
|
|
: undefined
|
|
});
|
|
|
|
const {
|
|
id,
|
|
iceParameters,
|
|
iceCandidates,
|
|
dtlsParameters,
|
|
sctpParameters
|
|
} = transportInfo;
|
|
|
|
this._sendTransport = this._mediasoupDevice.createSendTransport(
|
|
{
|
|
id,
|
|
iceParameters,
|
|
iceCandidates,
|
|
dtlsParameters :
|
|
{
|
|
...dtlsParameters,
|
|
// Remote DTLS role. We know it's always 'auto' by default so, if
|
|
// we want, we can force local WebRTC transport to be 'client' by
|
|
// indicating 'server' here and vice-versa.
|
|
role : 'auto'
|
|
},
|
|
sctpParameters,
|
|
iceServers : [],
|
|
proprietaryConstraints : PC_PROPRIETARY_CONSTRAINTS,
|
|
additionalSettings :
|
|
{ encodedInsertableStreams: this._e2eKey && e2e.isSupported() }
|
|
});
|
|
|
|
this._sendTransport.on(
|
|
'connect', ({ iceParameters, dtlsParameters }, callback, errback) => // eslint-disable-line no-shadow
|
|
{
|
|
this._protoo.request(
|
|
'connectWebRtcTransport',
|
|
{
|
|
transportId : this._sendTransport.id,
|
|
iceParameters,
|
|
dtlsParameters
|
|
})
|
|
.then(callback)
|
|
.catch(errback);
|
|
});
|
|
|
|
this._sendTransport.on(
|
|
'produce', async ({ kind, rtpParameters, appData }, callback, errback) =>
|
|
{
|
|
try
|
|
{
|
|
// eslint-disable-next-line no-shadow
|
|
const { id } = await this._protoo.request(
|
|
'produce',
|
|
{
|
|
transportId : this._sendTransport.id,
|
|
kind,
|
|
rtpParameters,
|
|
appData
|
|
});
|
|
|
|
callback({ id });
|
|
}
|
|
catch (error)
|
|
{
|
|
errback(error);
|
|
}
|
|
});
|
|
|
|
this._sendTransport.on('producedata', async (
|
|
{
|
|
sctpStreamParameters,
|
|
label,
|
|
protocol,
|
|
appData
|
|
},
|
|
callback,
|
|
errback
|
|
) =>
|
|
{
|
|
logger.debug(
|
|
'"producedata" event: [sctpStreamParameters:%o, appData:%o]',
|
|
sctpStreamParameters, appData);
|
|
|
|
try
|
|
{
|
|
// eslint-disable-next-line no-shadow
|
|
const { id } = await this._protoo.request(
|
|
'produceData',
|
|
{
|
|
transportId : this._sendTransport.id,
|
|
sctpStreamParameters,
|
|
label,
|
|
protocol,
|
|
appData
|
|
});
|
|
|
|
callback({ id });
|
|
}
|
|
catch (error)
|
|
{
|
|
errback(error);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Create mediasoup Transport for receiving (unless we don't want to consume).
|
|
if (this._consume)
|
|
{
|
|
const transportInfo = await this._protoo.request(
|
|
'createWebRtcTransport',
|
|
{
|
|
forceTcp : this._forceTcp,
|
|
producing : false,
|
|
consuming : true,
|
|
sctpCapabilities : this._useDataChannel
|
|
? this._mediasoupDevice.sctpCapabilities
|
|
: undefined
|
|
});
|
|
|
|
const {
|
|
id,
|
|
iceParameters,
|
|
iceCandidates,
|
|
dtlsParameters,
|
|
sctpParameters
|
|
} = transportInfo;
|
|
|
|
this._recvTransport = this._mediasoupDevice.createRecvTransport(
|
|
{
|
|
id,
|
|
iceParameters,
|
|
iceCandidates,
|
|
dtlsParameters :
|
|
{
|
|
...dtlsParameters,
|
|
// Remote DTLS role. We know it's always 'auto' by default so, if
|
|
// we want, we can force local WebRTC transport to be 'client' by
|
|
// indicating 'server' here and vice-versa.
|
|
role : 'auto'
|
|
},
|
|
sctpParameters,
|
|
iceServers : [],
|
|
additionalSettings :
|
|
{ encodedInsertableStreams: this._e2eKey && e2e.isSupported() }
|
|
});
|
|
|
|
this._recvTransport.on(
|
|
'connect', ({ iceParameters, dtlsParameters }, callback, errback) => // eslint-disable-line no-shadow
|
|
{
|
|
this._protoo.request(
|
|
'connectWebRtcTransport',
|
|
{
|
|
transportId : this._recvTransport.id,
|
|
iceParameters,
|
|
dtlsParameters
|
|
})
|
|
.then(callback)
|
|
.catch(errback);
|
|
});
|
|
}
|
|
|
|
// Join now into the room.
|
|
// NOTE: Don't send our RTP capabilities if we don't want to consume.
|
|
const { peers } = await this._protoo.request(
|
|
'join',
|
|
{
|
|
displayName : this._displayName,
|
|
device : this._device,
|
|
rtpCapabilities : this._consume
|
|
? this._mediasoupDevice.rtpCapabilities
|
|
: undefined,
|
|
sctpCapabilities : this._useDataChannel && this._consume
|
|
? this._mediasoupDevice.sctpCapabilities
|
|
: undefined
|
|
});
|
|
|
|
store.dispatch(
|
|
stateActions.setRoomState('connected'));
|
|
|
|
// Clean all the existing notifcations.
|
|
store.dispatch(
|
|
stateActions.removeAllNotifications());
|
|
|
|
// store.dispatch(requestActions.notify(
|
|
// {
|
|
// isMe : true,
|
|
// text : '你已成功加入房间',
|
|
// timeout : 3000
|
|
// }));
|
|
|
|
for (const peer of peers)
|
|
{
|
|
store.dispatch(
|
|
stateActions.addPeer(
|
|
{ ...peer, consumers: [], dataConsumers: [] }));
|
|
}
|
|
|
|
// Enable mic/webcam.
|
|
if (this._produce)
|
|
{
|
|
// Set our media capabilities.
|
|
store.dispatch(stateActions.setMediaCapabilities(
|
|
{
|
|
canSendMic : this._mediasoupDevice.canProduce('audio'),
|
|
canSendWebcam : this._mediasoupDevice.canProduce('video')
|
|
}));
|
|
|
|
this.enableMic();
|
|
|
|
const devicesCookie = cookiesManager.getDevices();
|
|
|
|
if (!devicesCookie || devicesCookie.webcamEnabled || this._externalVideo)
|
|
this.enableWebcam();
|
|
|
|
this._sendTransport.on('connectionstatechange', (connectionState) =>
|
|
{
|
|
if (connectionState === 'connected')
|
|
{
|
|
this.enableChatDataProducer();
|
|
this.enableBotDataProducer();
|
|
}
|
|
});
|
|
}
|
|
|
|
// NOTE: For testing.
|
|
if (window.SHOW_INFO)
|
|
{
|
|
const { me } = store.getState();
|
|
|
|
store.dispatch(
|
|
stateActions.setRoomStatsPeerId(me.id));
|
|
}
|
|
}
|
|
catch (error)
|
|
{
|
|
logger.error('_joinRoom() failed:%o', error);
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `无法加入房间: ${error}`
|
|
}));
|
|
|
|
this.close();
|
|
}
|
|
}
|
|
|
|
async _updateWebcams()
|
|
{
|
|
logger.debug('_updateWebcams()');
|
|
|
|
// Reset the list.
|
|
this._webcams = new Map();
|
|
|
|
logger.debug('_updateWebcams() | calling enumerateDevices()');
|
|
|
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
|
|
for (const device of devices)
|
|
{
|
|
if (device.kind !== 'videoinput')
|
|
continue;
|
|
|
|
this._webcams.set(device.deviceId, device);
|
|
}
|
|
|
|
const array = Array.from(this._webcams.values());
|
|
const len = array.length;
|
|
const currentWebcamId =
|
|
this._webcam.device ? this._webcam.device.deviceId : undefined;
|
|
|
|
logger.debug('_updateWebcams() [webcams:%o]', array);
|
|
|
|
if (len === 0)
|
|
this._webcam.device = null;
|
|
else if (!this._webcams.has(currentWebcamId))
|
|
this._webcam.device = array[0];
|
|
|
|
store.dispatch(
|
|
stateActions.setCanChangeWebcam(this._webcams.size > 1));
|
|
}
|
|
|
|
_getWebcamType(device)
|
|
{
|
|
if (/(back|rear)/i.test(device.label))
|
|
{
|
|
logger.debug('_getWebcamType() | it seems to be a back camera');
|
|
|
|
return 'back';
|
|
}
|
|
else
|
|
{
|
|
logger.debug('_getWebcamType() | it seems to be a front camera');
|
|
|
|
return 'front';
|
|
}
|
|
}
|
|
|
|
async _pauseConsumer(consumer)
|
|
{
|
|
if (consumer.paused)
|
|
return;
|
|
|
|
try
|
|
{
|
|
await this._protoo.request('pauseConsumer', { consumerId: consumer.id });
|
|
|
|
consumer.pause();
|
|
|
|
store.dispatch(
|
|
stateActions.setConsumerPaused(consumer.id, 'local'));
|
|
}
|
|
catch (error)
|
|
{
|
|
logger.error('_pauseConsumer() | failed:%o', error);
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `暂停用户时出错: ${error}`
|
|
}));
|
|
}
|
|
}
|
|
|
|
async _resumeConsumer(consumer)
|
|
{
|
|
if (!consumer.paused)
|
|
return;
|
|
|
|
try
|
|
{
|
|
await this._protoo.request('resumeConsumer', { consumerId: consumer.id });
|
|
|
|
consumer.resume();
|
|
|
|
store.dispatch(
|
|
stateActions.setConsumerResumed(consumer.id, 'local'));
|
|
}
|
|
catch (error)
|
|
{
|
|
logger.error('_resumeConsumer() | failed:%o', error);
|
|
|
|
store.dispatch(requestActions.notify(
|
|
{
|
|
type : 'error',
|
|
text : `恢复用户时出错: ${error}`
|
|
}));
|
|
}
|
|
}
|
|
|
|
async _getExternalVideoStream()
|
|
{
|
|
if (this._externalVideoStream)
|
|
return this._externalVideoStream;
|
|
|
|
if (this._externalVideo.readyState < 3)
|
|
{
|
|
await new Promise((resolve) => (
|
|
this._externalVideo.addEventListener('canplay', resolve)
|
|
));
|
|
}
|
|
|
|
if (this._externalVideo.captureStream)
|
|
this._externalVideoStream = this._externalVideo.captureStream();
|
|
else if (this._externalVideo.mozCaptureStream)
|
|
this._externalVideoStream = this._externalVideo.mozCaptureStream();
|
|
else
|
|
throw new Error('video.captureStream() not supported');
|
|
|
|
return this._externalVideoStream;
|
|
}
|
|
}
|