2017-09-09 01:00:00 +08:00
|
|
|
import Auth from '/imports/ui/services/auth';
|
2018-07-27 02:26:56 +08:00
|
|
|
import logger from '/imports/startup/client/logger';
|
2020-12-10 06:00:54 +08:00
|
|
|
import BridgeService from './service';
|
|
|
|
import ScreenshareBroker from '/imports/ui/services/bbb-webrtc-sfu/screenshare-broker';
|
2021-03-12 08:44:38 +08:00
|
|
|
import { setSharingScreen, screenShareEndAlert } from '/imports/ui/components/screenshare/service';
|
2021-01-12 23:24:01 +08:00
|
|
|
import { SCREENSHARING_ERRORS } from './errors';
|
2021-12-01 04:31:09 +08:00
|
|
|
import { shouldForceRelay } from '/imports/ui/services/bbb-webrtc-sfu/utils';
|
2022-05-08 00:50:34 +08:00
|
|
|
import MediaStreamUtils from '/imports/utils/media-stream-utils';
|
2017-09-09 01:00:00 +08:00
|
|
|
|
2018-07-10 05:29:27 +08:00
|
|
|
const SFU_CONFIG = Meteor.settings.public.kurento;
|
|
|
|
const SFU_URL = SFU_CONFIG.wsUrl;
|
2021-08-12 03:36:32 +08:00
|
|
|
const OFFERING = SFU_CONFIG.screenshare.subscriberOffering;
|
2021-09-24 03:32:14 +08:00
|
|
|
const SIGNAL_CANDIDATES = Meteor.settings.public.kurento.signalCandidates;
|
2022-07-12 22:19:57 +08:00
|
|
|
const TRACE_LOGS = Meteor.settings.public.kurento.traceLogs;
|
2022-08-16 05:24:05 +08:00
|
|
|
const { screenshare: NETWORK_PRIORITY } = Meteor.settings.public.media.networkPriorities || {};
|
2020-12-10 06:00:54 +08:00
|
|
|
|
|
|
|
const BRIDGE_NAME = 'kurento'
|
2018-07-10 05:29:27 +08:00
|
|
|
const SCREENSHARE_VIDEO_TAG = 'screenshareVideo';
|
2020-12-10 06:00:54 +08:00
|
|
|
const SEND_ROLE = 'send';
|
|
|
|
const RECV_ROLE = 'recv';
|
2021-12-09 00:24:26 +08:00
|
|
|
const DEFAULT_VOLUME = 1;
|
2017-11-18 02:55:59 +08:00
|
|
|
|
2021-01-12 23:24:01 +08:00
|
|
|
// the error-code mapping is bridge specific; that's why it's not in the errors util
|
|
|
|
const ERROR_MAP = {
|
|
|
|
1301: SCREENSHARING_ERRORS.SIGNALLING_TRANSPORT_DISCONNECTED,
|
|
|
|
1302: SCREENSHARING_ERRORS.SIGNALLING_TRANSPORT_CONNECTION_FAILED,
|
|
|
|
1305: SCREENSHARING_ERRORS.PEER_NEGOTIATION_FAILED,
|
|
|
|
1307: SCREENSHARING_ERRORS.ICE_STATE_FAILED,
|
2022-05-08 00:25:31 +08:00
|
|
|
1310: SCREENSHARING_ERRORS.ENDED_WHILE_STARTING,
|
|
|
|
};
|
2017-09-09 01:00:00 +08:00
|
|
|
|
2020-12-10 06:00:54 +08:00
|
|
|
const mapErrorCode = (error) => {
|
|
|
|
const { errorCode } = error;
|
2021-01-12 23:24:01 +08:00
|
|
|
const mappedError = ERROR_MAP[errorCode];
|
|
|
|
|
|
|
|
if (errorCode == null || mappedError == null) return error;
|
|
|
|
error.errorCode = mappedError.errorCode;
|
|
|
|
error.errorMessage = mappedError.errorMessage;
|
|
|
|
error.message = mappedError.errorMessage;
|
|
|
|
|
2020-12-10 06:00:54 +08:00
|
|
|
return error;
|
|
|
|
}
|
2017-09-09 01:00:00 +08:00
|
|
|
|
2020-12-10 06:00:54 +08:00
|
|
|
export default class KurentoScreenshareBridge {
|
|
|
|
constructor() {
|
|
|
|
this.role;
|
|
|
|
this.broker;
|
|
|
|
this._gdmStream;
|
2020-12-16 01:45:20 +08:00
|
|
|
this.hasAudio = false;
|
2020-12-10 06:00:54 +08:00
|
|
|
this.connectionAttempts = 0;
|
|
|
|
this.reconnecting = false;
|
|
|
|
this.reconnectionTimeout;
|
|
|
|
this.restartIntervalMs = BridgeService.BASE_MEDIA_TIMEOUT;
|
|
|
|
}
|
2018-04-27 06:52:46 +08:00
|
|
|
|
2020-12-10 06:00:54 +08:00
|
|
|
get gdmStream() {
|
|
|
|
return this._gdmStream;
|
|
|
|
}
|
2019-09-11 21:17:23 +08:00
|
|
|
|
2020-12-10 06:00:54 +08:00
|
|
|
set gdmStream(stream) {
|
|
|
|
this._gdmStream = stream;
|
|
|
|
}
|
2018-07-10 05:29:27 +08:00
|
|
|
|
2022-05-09 05:10:43 +08:00
|
|
|
_shouldReconnect() {
|
|
|
|
// Sender/presenter reconnect is *not* implemented yet
|
2023-02-16 06:57:57 +08:00
|
|
|
return this.reconnectionTimeout == null && this.role === RECV_ROLE;
|
2022-05-09 05:10:43 +08:00
|
|
|
}
|
|
|
|
|
2021-08-23 23:36:01 +08:00
|
|
|
/**
|
|
|
|
* Get the RTCPeerConnection object related to the screensharing stream.
|
|
|
|
* @returns {Object} The RTCPeerConnection object related to the presenter/
|
|
|
|
* viewer peer. If there's no stream being shared, returns
|
|
|
|
* null.
|
|
|
|
*/
|
|
|
|
getPeerConnection() {
|
|
|
|
try {
|
|
|
|
let peerConnection = null;
|
|
|
|
|
|
|
|
if (this.broker && this.broker.webRtcPeer) {
|
|
|
|
peerConnection = this.broker.webRtcPeer.peerConnection;
|
|
|
|
}
|
|
|
|
|
|
|
|
return peerConnection;
|
|
|
|
} catch (error) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-10 06:00:54 +08:00
|
|
|
inboundStreamReconnect() {
|
|
|
|
const currentRestartIntervalMs = this.restartIntervalMs;
|
|
|
|
|
|
|
|
logger.warn({
|
|
|
|
logCode: 'screenshare_viewer_reconnect',
|
2020-12-16 01:45:20 +08:00
|
|
|
extraInfo: {
|
|
|
|
reconnecting: this.reconnecting,
|
|
|
|
role: this.role,
|
2022-05-09 05:10:43 +08:00
|
|
|
bridge: BRIDGE_NAME,
|
2020-12-16 01:45:20 +08:00
|
|
|
},
|
2022-05-09 05:10:43 +08:00
|
|
|
}, 'Screenshare viewer is reconnecting');
|
2020-12-16 01:45:20 +08:00
|
|
|
|
2020-12-10 06:00:54 +08:00
|
|
|
// Cleanly stop everything before triggering a reconnect
|
2022-05-09 04:25:49 +08:00
|
|
|
this._stop();
|
2020-12-10 06:00:54 +08:00
|
|
|
// Create new reconnect interval time
|
|
|
|
this.restartIntervalMs = BridgeService.getNextReconnectionInterval(currentRestartIntervalMs);
|
2020-12-16 01:45:20 +08:00
|
|
|
this.view(this.hasAudio).then(() => {
|
2020-12-10 06:00:54 +08:00
|
|
|
this.clearReconnectionTimeout();
|
2022-05-09 05:10:43 +08:00
|
|
|
}).catch((error) => {
|
2020-12-10 06:00:54 +08:00
|
|
|
// Error handling is a no-op because it will be "handled" in handleViewerFailure
|
|
|
|
logger.debug({
|
|
|
|
logCode: 'screenshare_reconnect_failed',
|
|
|
|
extraInfo: {
|
2021-01-12 23:24:01 +08:00
|
|
|
errorCode: error.errorCode,
|
2020-12-10 06:00:54 +08:00
|
|
|
errorMessage: error.errorMessage,
|
|
|
|
reconnecting: this.reconnecting,
|
|
|
|
role: this.role,
|
|
|
|
bridge: BRIDGE_NAME
|
|
|
|
},
|
|
|
|
}, 'Screensharing reconnect failed');
|
|
|
|
});
|
2019-09-07 02:58:22 +08:00
|
|
|
}
|
|
|
|
|
2020-12-10 06:00:54 +08:00
|
|
|
handleConnectionTimeoutExpiry() {
|
|
|
|
this.reconnecting = true;
|
|
|
|
|
|
|
|
switch (this.role) {
|
|
|
|
case RECV_ROLE:
|
|
|
|
return this.inboundStreamReconnect();
|
2022-05-09 05:10:43 +08:00
|
|
|
|
|
|
|
// Sender/presenter reconnect is *not* implemented yet
|
2020-12-10 06:00:54 +08:00
|
|
|
case SEND_ROLE:
|
|
|
|
default:
|
|
|
|
this.reconnecting = false;
|
|
|
|
logger.error({
|
2022-05-09 05:10:43 +08:00
|
|
|
logCode: 'screenshare_wont_reconnect',
|
|
|
|
extraInfo: {
|
|
|
|
role: this.broker?.role || this.role,
|
|
|
|
started: !!(this.broker?.started),
|
|
|
|
bridge: BRIDGE_NAME,
|
|
|
|
},
|
|
|
|
}, 'Screen sharing will not reconnect');
|
2020-12-10 06:00:54 +08:00
|
|
|
break;
|
2019-09-11 00:20:40 +08:00
|
|
|
}
|
2019-09-07 02:58:22 +08:00
|
|
|
}
|
|
|
|
|
2020-12-10 06:00:54 +08:00
|
|
|
maxConnectionAttemptsReached () {
|
|
|
|
return this.connectionAttempts > BridgeService.MAX_CONN_ATTEMPTS;
|
|
|
|
}
|
2020-08-27 07:30:53 +08:00
|
|
|
|
2022-05-09 05:21:22 +08:00
|
|
|
scheduleReconnect(immediate = false) {
|
2020-12-10 06:00:54 +08:00
|
|
|
if (this.reconnectionTimeout == null) {
|
2022-05-09 05:21:22 +08:00
|
|
|
const nextRestartInterval = immediate ? 0 : this.restartIntervalMs;
|
|
|
|
|
2020-12-10 06:00:54 +08:00
|
|
|
this.reconnectionTimeout = setTimeout(
|
|
|
|
this.handleConnectionTimeoutExpiry.bind(this),
|
2022-05-09 05:21:22 +08:00
|
|
|
nextRestartInterval,
|
2020-12-10 06:00:54 +08:00
|
|
|
);
|
2020-08-27 07:30:53 +08:00
|
|
|
}
|
2020-12-10 06:00:54 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
clearReconnectionTimeout () {
|
|
|
|
this.reconnecting = false;
|
|
|
|
this.restartIntervalMs = BridgeService.BASE_MEDIA_TIMEOUT;
|
2020-08-27 07:30:53 +08:00
|
|
|
|
2020-12-10 06:00:54 +08:00
|
|
|
if (this.reconnectionTimeout) {
|
|
|
|
clearTimeout(this.reconnectionTimeout);
|
|
|
|
this.reconnectionTimeout = null;
|
|
|
|
}
|
2020-08-27 07:30:53 +08:00
|
|
|
}
|
|
|
|
|
2021-11-12 01:51:21 +08:00
|
|
|
setVolume(volume) {
|
2021-12-09 00:24:26 +08:00
|
|
|
const mediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG);
|
2021-11-12 01:51:21 +08:00
|
|
|
|
2021-12-09 00:24:26 +08:00
|
|
|
if (mediaElement) {
|
|
|
|
if (typeof volume === 'number' && volume >= 0 && volume <= 1) {
|
|
|
|
mediaElement.volume = volume;
|
|
|
|
}
|
2021-11-12 01:51:21 +08:00
|
|
|
|
2021-12-09 00:24:26 +08:00
|
|
|
return mediaElement.volume;
|
2021-11-12 01:51:21 +08:00
|
|
|
}
|
2021-12-09 00:24:26 +08:00
|
|
|
|
|
|
|
return DEFAULT_VOLUME;
|
2021-11-12 01:51:21 +08:00
|
|
|
}
|
|
|
|
|
2021-12-09 00:24:26 +08:00
|
|
|
getVolume() {
|
|
|
|
const mediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG);
|
2021-11-12 01:51:21 +08:00
|
|
|
|
2021-12-09 00:24:26 +08:00
|
|
|
if (mediaElement) return mediaElement.volume;
|
2021-11-12 01:51:21 +08:00
|
|
|
|
2021-12-09 00:24:26 +08:00
|
|
|
return DEFAULT_VOLUME;
|
2021-11-12 01:51:21 +08:00
|
|
|
}
|
|
|
|
|
2020-12-10 06:00:54 +08:00
|
|
|
handleViewerStart() {
|
|
|
|
const mediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG);
|
2020-08-27 07:30:53 +08:00
|
|
|
|
2020-12-10 06:00:54 +08:00
|
|
|
if (mediaElement && this.broker && this.broker.webRtcPeer) {
|
|
|
|
const stream = this.broker.webRtcPeer.getRemoteStream();
|
|
|
|
BridgeService.screenshareLoadAndPlayMediaStream(stream, mediaElement, !this.broker.hasAudio);
|
2020-08-27 07:30:53 +08:00
|
|
|
}
|
2020-12-10 06:00:54 +08:00
|
|
|
|
|
|
|
this.clearReconnectionTimeout();
|
2022-05-09 04:25:49 +08:00
|
|
|
this.connectionAttempts = 0;
|
2020-08-27 07:30:53 +08:00
|
|
|
}
|
|
|
|
|
2020-12-10 06:00:54 +08:00
|
|
|
handleBrokerFailure(error) {
|
|
|
|
mapErrorCode(error);
|
2021-01-12 23:24:01 +08:00
|
|
|
const { errorMessage, errorCode } = error;
|
2020-12-16 01:45:20 +08:00
|
|
|
|
|
|
|
logger.error({
|
2021-01-12 23:24:01 +08:00
|
|
|
logCode: 'screenshare_broker_failure',
|
2020-12-16 01:45:20 +08:00
|
|
|
extraInfo: {
|
2021-01-12 23:24:01 +08:00
|
|
|
errorCode, errorMessage,
|
2020-12-16 01:45:20 +08:00
|
|
|
role: this.broker.role,
|
|
|
|
started: this.broker.started,
|
|
|
|
reconnecting: this.reconnecting,
|
|
|
|
bridge: BRIDGE_NAME
|
|
|
|
},
|
2021-07-08 03:04:56 +08:00
|
|
|
}, `Screenshare broker failure: ${errorMessage}`);
|
2020-12-16 01:45:20 +08:00
|
|
|
|
2020-12-10 06:00:54 +08:00
|
|
|
// Screensharing was already successfully negotiated and error occurred during
|
|
|
|
// during call; schedule a reconnect
|
2022-05-09 05:10:43 +08:00
|
|
|
if (this._shouldReconnect()) {
|
2022-05-09 05:21:22 +08:00
|
|
|
// this.broker.started => whether the reconnect should happen immediately.
|
|
|
|
// If this session had alredy been established, it should.
|
|
|
|
this.scheduleReconnect(this.broker.started);
|
2018-08-23 23:49:16 +08:00
|
|
|
}
|
2020-12-16 01:45:20 +08:00
|
|
|
|
|
|
|
return error;
|
2017-09-09 01:00:00 +08:00
|
|
|
}
|
|
|
|
|
2020-12-10 06:00:54 +08:00
|
|
|
async view(hasAudio = false) {
|
2020-12-16 01:45:20 +08:00
|
|
|
this.hasAudio = hasAudio;
|
2020-12-10 06:00:54 +08:00
|
|
|
this.role = RECV_ROLE;
|
|
|
|
const iceServers = await BridgeService.getIceServers(Auth.sessionToken);
|
|
|
|
const options = {
|
|
|
|
iceServers,
|
|
|
|
userName: Auth.fullname,
|
|
|
|
hasAudio,
|
2021-08-12 03:36:32 +08:00
|
|
|
offering: OFFERING,
|
2021-08-11 03:49:24 +08:00
|
|
|
mediaServer: BridgeService.getMediaServerAdapter(),
|
2021-09-24 03:32:14 +08:00
|
|
|
signalCandidates: SIGNAL_CANDIDATES,
|
2021-12-01 04:31:09 +08:00
|
|
|
forceRelay: shouldForceRelay(),
|
2022-07-12 22:19:57 +08:00
|
|
|
traceLogs: TRACE_LOGS,
|
2020-12-10 06:00:54 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
this.broker = new ScreenshareBroker(
|
|
|
|
Auth.authenticateURL(SFU_URL),
|
|
|
|
BridgeService.getConferenceBridge(),
|
|
|
|
Auth.userID,
|
|
|
|
Auth.meetingID,
|
|
|
|
this.role,
|
|
|
|
options,
|
|
|
|
);
|
|
|
|
|
|
|
|
this.broker.onstart = this.handleViewerStart.bind(this);
|
|
|
|
this.broker.onerror = this.handleBrokerFailure.bind(this);
|
2021-03-12 08:44:38 +08:00
|
|
|
this.broker.onended = this.handleEnded.bind(this);
|
|
|
|
|
2020-12-10 06:00:54 +08:00
|
|
|
return this.broker.view().finally(this.scheduleReconnect.bind(this));
|
2017-09-09 01:00:00 +08:00
|
|
|
}
|
2017-09-13 04:47:06 +08:00
|
|
|
|
2020-12-10 06:00:54 +08:00
|
|
|
handlePresenterStart() {
|
|
|
|
logger.info({
|
|
|
|
logCode: 'screenshare_presenter_start_success',
|
|
|
|
}, 'Screenshare presenter started succesfully');
|
|
|
|
this.clearReconnectionTimeout();
|
|
|
|
this.reconnecting = false;
|
|
|
|
this.connectionAttempts = 0;
|
2017-09-13 04:47:06 +08:00
|
|
|
}
|
2017-11-06 23:39:55 +08:00
|
|
|
|
2021-03-12 08:44:38 +08:00
|
|
|
handleEnded() {
|
|
|
|
screenShareEndAlert();
|
|
|
|
}
|
|
|
|
|
2020-12-16 01:45:20 +08:00
|
|
|
share(stream, onFailure) {
|
|
|
|
return new Promise(async (resolve, reject) => {
|
|
|
|
this.onerror = onFailure;
|
|
|
|
this.connectionAttempts += 1;
|
|
|
|
this.role = SEND_ROLE;
|
|
|
|
this.hasAudio = BridgeService.streamHasAudioTrack(stream);
|
|
|
|
this.gdmStream = stream;
|
|
|
|
|
|
|
|
const onerror = (error) => {
|
|
|
|
const normalizedError = this.handleBrokerFailure(error);
|
2022-05-09 05:10:43 +08:00
|
|
|
if (!this.broker.started) {
|
|
|
|
// Broker hasn't started - if there are retries left, try again.
|
|
|
|
if (this.maxConnectionAttemptsReached()) {
|
|
|
|
this.clearReconnectionTimeout();
|
|
|
|
this.connectionAttempts = 0;
|
|
|
|
onFailure(SCREENSHARING_ERRORS.MEDIA_TIMEOUT);
|
|
|
|
reject(SCREENSHARING_ERRORS.MEDIA_TIMEOUT);
|
|
|
|
}
|
|
|
|
} else if (!this._shouldReconnect()) {
|
|
|
|
// Broker has started - should reconnect? If it shouldn't, end it.
|
|
|
|
onFailure(normalizedError);
|
2020-12-16 01:45:20 +08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const iceServers = await BridgeService.getIceServers(Auth.sessionToken);
|
|
|
|
const options = {
|
|
|
|
iceServers,
|
|
|
|
userName: Auth.fullname,
|
|
|
|
stream,
|
|
|
|
hasAudio: this.hasAudio,
|
2021-04-25 03:12:59 +08:00
|
|
|
bitrate: BridgeService.BASE_BITRATE,
|
2021-08-12 03:36:32 +08:00
|
|
|
offering: true,
|
2021-04-27 02:39:29 +08:00
|
|
|
mediaServer: BridgeService.getMediaServerAdapter(),
|
2021-09-24 03:32:14 +08:00
|
|
|
signalCandidates: SIGNAL_CANDIDATES,
|
2021-12-01 04:31:09 +08:00
|
|
|
forceRelay: shouldForceRelay(),
|
2022-07-12 22:19:57 +08:00
|
|
|
traceLogs: TRACE_LOGS,
|
2022-08-16 05:24:05 +08:00
|
|
|
networkPriority: NETWORK_PRIORITY,
|
2020-12-16 01:45:20 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
this.broker = new ScreenshareBroker(
|
|
|
|
Auth.authenticateURL(SFU_URL),
|
|
|
|
BridgeService.getConferenceBridge(),
|
|
|
|
Auth.userID,
|
|
|
|
Auth.meetingID,
|
|
|
|
this.role,
|
|
|
|
options,
|
|
|
|
);
|
2020-12-10 06:00:54 +08:00
|
|
|
|
2020-12-16 01:45:20 +08:00
|
|
|
this.broker.onerror = onerror.bind(this);
|
|
|
|
this.broker.onstreamended = this.stop.bind(this);
|
|
|
|
this.broker.onstart = this.handlePresenterStart.bind(this);
|
2021-03-12 08:44:38 +08:00
|
|
|
this.broker.onended = this.handleEnded.bind(this);
|
2020-12-10 06:00:54 +08:00
|
|
|
|
2020-12-16 01:45:20 +08:00
|
|
|
this.broker.share().then(() => {
|
2022-05-08 00:25:31 +08:00
|
|
|
this.scheduleReconnect();
|
|
|
|
return resolve();
|
|
|
|
}).catch((error) => reject(mapErrorCode(error)));
|
2020-12-16 01:45:20 +08:00
|
|
|
});
|
2022-05-08 00:25:31 +08:00
|
|
|
}
|
2020-12-10 06:00:54 +08:00
|
|
|
|
2022-05-09 04:25:49 +08:00
|
|
|
// This is a reconnect-safe internal method. Should be used when one wants
|
|
|
|
// to clear the internal components (ie broker, connection timeouts) without
|
|
|
|
// affecting externally controlled components (ie gDM stream,
|
|
|
|
// media tag, connectionAttempts, ...)
|
|
|
|
_stop() {
|
2020-12-10 06:00:54 +08:00
|
|
|
if (this.broker) {
|
|
|
|
this.broker.stop();
|
|
|
|
// Checks if this session is a sharer and if it's not reconnecting
|
|
|
|
// If that's the case, clear the local sharing state in screen sharing UI
|
|
|
|
// component tracker to be extra sure we won't have any client-side state
|
|
|
|
// inconsistency - prlanzarin
|
2021-07-08 03:09:38 +08:00
|
|
|
if (this.broker && this.broker.role === SEND_ROLE && !this.reconnecting) {
|
|
|
|
setSharingScreen(false);
|
|
|
|
}
|
2020-12-10 06:00:54 +08:00
|
|
|
this.broker = null;
|
|
|
|
}
|
2021-07-23 10:27:02 +08:00
|
|
|
|
2022-05-09 04:25:49 +08:00
|
|
|
this.clearReconnectionTimeout();
|
|
|
|
}
|
|
|
|
|
|
|
|
stop() {
|
|
|
|
const mediaElement = document.getElementById(SCREENSHARE_VIDEO_TAG);
|
|
|
|
|
|
|
|
this._stop();
|
|
|
|
this.connectionAttempts = 0;
|
|
|
|
|
2021-07-23 10:27:02 +08:00
|
|
|
if (mediaElement && typeof mediaElement.pause === 'function') {
|
|
|
|
mediaElement.pause();
|
|
|
|
mediaElement.srcObject = null;
|
|
|
|
}
|
|
|
|
|
2022-05-08 00:50:34 +08:00
|
|
|
if (this.gdmStream) {
|
|
|
|
MediaStreamUtils.stopMediaStreamTracks(this.gdmStream);
|
|
|
|
this.gdmStream = null;
|
|
|
|
}
|
2017-11-06 23:39:55 +08:00
|
|
|
}
|
2017-09-09 01:00:00 +08:00
|
|
|
}
|