mirror of https://github.com/OpenVidu/openvidu.git
openvidu-browser: IExplorer support
parent
89db47dd37
commit
8f25b937e2
|
@ -96,8 +96,8 @@ export class Connection {
|
||||||
*/
|
*/
|
||||||
sendIceCandidate(candidate: RTCIceCandidate): void {
|
sendIceCandidate(candidate: RTCIceCandidate): void {
|
||||||
|
|
||||||
console.debug((!!this.stream.outboundStreamOpts ? 'Local' : 'Remote'), 'candidate for',
|
console.debug((!!this.stream.outboundStreamOpts ? 'Local' : 'Remote') + 'candidate for' +
|
||||||
this.connectionId, JSON.stringify(candidate));
|
this.connectionId, candidate);
|
||||||
|
|
||||||
this.session.openvidu.sendRequest('onIceCandidate', {
|
this.session.openvidu.sendRequest('onIceCandidate', {
|
||||||
endpointName: this.connectionId,
|
endpointName: this.connectionId,
|
||||||
|
|
|
@ -32,6 +32,9 @@ import * as screenSharing from '../OpenViduInternal/ScreenSharing/Screen-Capturi
|
||||||
import RpcBuilder = require('../OpenViduInternal/KurentoUtils/kurento-jsonrpc');
|
import RpcBuilder = require('../OpenViduInternal/KurentoUtils/kurento-jsonrpc');
|
||||||
import platform = require('platform');
|
import platform = require('platform');
|
||||||
platform['isIonicIos'] = (platform.product === 'iPhone' || platform.product === 'iPad') && platform.ua!!.indexOf('Safari') === -1;
|
platform['isIonicIos'] = (platform.product === 'iPhone' || platform.product === 'iPad') && platform.ua!!.indexOf('Safari') === -1;
|
||||||
|
platform['isInternetExplorer'] = platform.name === 'IE' && platform.version !== undefined && parseInt(platform.version) >= 11;
|
||||||
|
platform['isReactNative'] = navigator.product === 'ReactNative';
|
||||||
|
declare const AdapterJS: any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @hidden
|
* @hidden
|
||||||
|
@ -97,6 +100,10 @@ export class OpenVidu {
|
||||||
console.info("'OpenVidu' initialized");
|
console.info("'OpenVidu' initialized");
|
||||||
console.info("openvidu-browser version: " + this.libraryVersion);
|
console.info("openvidu-browser version: " + this.libraryVersion);
|
||||||
|
|
||||||
|
if (platform['isInternetExplorer']) {
|
||||||
|
this.importIEAdapterJS();
|
||||||
|
}
|
||||||
|
|
||||||
if (platform.os!!.family === 'iOS' || platform.os!!.family === 'Android') {
|
if (platform.os!!.family === 'iOS' || platform.os!!.family === 'Android') {
|
||||||
// Listen to orientationchange only on mobile devices
|
// Listen to orientationchange only on mobile devices
|
||||||
(<any>window).addEventListener('orientationchange', () => {
|
(<any>window).addEventListener('orientationchange', () => {
|
||||||
|
@ -216,12 +223,12 @@ export class OpenVidu {
|
||||||
|
|
||||||
properties = {
|
properties = {
|
||||||
audioSource: (typeof properties.audioSource !== 'undefined') ? properties.audioSource : undefined,
|
audioSource: (typeof properties.audioSource !== 'undefined') ? properties.audioSource : undefined,
|
||||||
frameRate: (properties.videoSource instanceof MediaStreamTrack) ? undefined : ((typeof properties.frameRate !== 'undefined') ? properties.frameRate : undefined),
|
frameRate: (typeof MediaStreamTrack !== 'undefined' && properties.videoSource instanceof MediaStreamTrack) ? undefined : ((typeof properties.frameRate !== 'undefined') ? properties.frameRate : undefined),
|
||||||
insertMode: (typeof properties.insertMode !== 'undefined') ? ((typeof properties.insertMode === 'string') ? VideoInsertMode[properties.insertMode] : properties.insertMode) : VideoInsertMode.APPEND,
|
insertMode: (typeof properties.insertMode !== 'undefined') ? ((typeof properties.insertMode === 'string') ? VideoInsertMode[properties.insertMode] : properties.insertMode) : VideoInsertMode.APPEND,
|
||||||
mirror: (typeof properties.mirror !== 'undefined') ? properties.mirror : true,
|
mirror: (typeof properties.mirror !== 'undefined') ? properties.mirror : true,
|
||||||
publishAudio: (typeof properties.publishAudio !== 'undefined') ? properties.publishAudio : true,
|
publishAudio: (typeof properties.publishAudio !== 'undefined') ? properties.publishAudio : true,
|
||||||
publishVideo: (typeof properties.publishVideo !== 'undefined') ? properties.publishVideo : true,
|
publishVideo: (typeof properties.publishVideo !== 'undefined') ? properties.publishVideo : true,
|
||||||
resolution: (properties.videoSource instanceof MediaStreamTrack) ? undefined : ((typeof properties.resolution !== 'undefined') ? properties.resolution : '640x480'),
|
resolution: (typeof MediaStreamTrack !== 'undefined' && properties.videoSource instanceof MediaStreamTrack) ? undefined : ((typeof properties.resolution !== 'undefined') ? properties.resolution : '640x480'),
|
||||||
videoSource: (typeof properties.videoSource !== 'undefined') ? properties.videoSource : undefined,
|
videoSource: (typeof properties.videoSource !== 'undefined') ? properties.videoSource : undefined,
|
||||||
filter: properties.filter
|
filter: properties.filter
|
||||||
};
|
};
|
||||||
|
@ -325,7 +332,8 @@ export class OpenVidu {
|
||||||
(browser !== 'Chrome') && (browser !== 'Chrome Mobile') &&
|
(browser !== 'Chrome') && (browser !== 'Chrome Mobile') &&
|
||||||
(browser !== 'Firefox') && (browser !== 'Firefox Mobile') &&
|
(browser !== 'Firefox') && (browser !== 'Firefox Mobile') &&
|
||||||
(browser !== 'Opera') && (browser !== 'Opera Mobile') &&
|
(browser !== 'Opera') && (browser !== 'Opera Mobile') &&
|
||||||
(browser !== 'Android Browser')
|
(browser !== 'Android Browser') &&
|
||||||
|
(browser !== 'IE' || platform.version !== undefined && parseInt(platform.version) < 11)
|
||||||
) {
|
) {
|
||||||
return 0;
|
return 0;
|
||||||
} else {
|
} else {
|
||||||
|
@ -431,6 +439,8 @@ export class OpenVidu {
|
||||||
return new Promise<MediaStream>((resolve, reject) => {
|
return new Promise<MediaStream>((resolve, reject) => {
|
||||||
this.generateMediaConstraints(options)
|
this.generateMediaConstraints(options)
|
||||||
.then(constraints => {
|
.then(constraints => {
|
||||||
|
|
||||||
|
let userMediaFunc = () => {
|
||||||
navigator.mediaDevices.getUserMedia(constraints)
|
navigator.mediaDevices.getUserMedia(constraints)
|
||||||
.then(mediaStream => {
|
.then(mediaStream => {
|
||||||
resolve(mediaStream);
|
resolve(mediaStream);
|
||||||
|
@ -445,6 +455,15 @@ export class OpenVidu {
|
||||||
}
|
}
|
||||||
reject(new OpenViduError(errorName, errorMessage));
|
reject(new OpenViduError(errorName, errorMessage));
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (platform['isInternetExplorer']) {
|
||||||
|
AdapterJS.webRTCReady(isUsingPlugin => {
|
||||||
|
userMediaFunc();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
userMediaFunc();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((error: OpenViduError) => {
|
.catch((error: OpenViduError) => {
|
||||||
reject(error);
|
reject(error);
|
||||||
|
@ -742,4 +761,19 @@ export class OpenVidu {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private importIEAdapterJS(): void {
|
||||||
|
const moduleSpecifier = 'https://cdn.temasys.io/adapterjs/0.15.x/adapter.screenshare.min.js';
|
||||||
|
//Create a script tag
|
||||||
|
var script = document.createElement('script');
|
||||||
|
// Assign a URL to the script element
|
||||||
|
script.src = moduleSpecifier;
|
||||||
|
// Get the first script tag on the page (we'll insert our new one before it)
|
||||||
|
var ref = document.querySelector('script');
|
||||||
|
// Insert the new node before the reference node
|
||||||
|
if (ref && ref.parentNode) {
|
||||||
|
ref.parentNode.insertBefore(script, ref);
|
||||||
|
console.info("Detected IE Explorer " + platform.version + ". IEAdapter imported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -30,6 +30,9 @@ import { VideoInsertMode } from '../OpenViduInternal/Enums/VideoInsertMode';
|
||||||
|
|
||||||
import platform = require('platform');
|
import platform = require('platform');
|
||||||
platform['isIonicIos'] = (platform.product === 'iPhone' || platform.product === 'iPad') && platform.ua!!.indexOf('Safari') === -1;
|
platform['isIonicIos'] = (platform.product === 'iPhone' || platform.product === 'iPad') && platform.ua!!.indexOf('Safari') === -1;
|
||||||
|
platform['isInternetExplorer'] = platform.name === 'IE' && platform.version !== undefined && parseInt(platform.version) >= 11;
|
||||||
|
platform['isReactNative'] = navigator.product === 'ReactNative';
|
||||||
|
declare const AdapterJS: any, attachMediaStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Packs local media streams. Participants can publish it to a session. Initialized with [[OpenVidu.initPublisher]] method
|
* Packs local media streams. Participants can publish it to a session. Initialized with [[OpenVidu.initPublisher]] method
|
||||||
|
@ -67,6 +70,10 @@ export class Publisher extends StreamManager {
|
||||||
* @hidden
|
* @hidden
|
||||||
*/
|
*/
|
||||||
screenShareResizeInterval: NodeJS.Timer;
|
screenShareResizeInterval: NodeJS.Timer;
|
||||||
|
/**
|
||||||
|
* @hidden
|
||||||
|
*/
|
||||||
|
IEAdapter: any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @hidden
|
* @hidden
|
||||||
|
@ -284,12 +291,12 @@ export class Publisher extends StreamManager {
|
||||||
this.accessAllowed = true;
|
this.accessAllowed = true;
|
||||||
this.accessDenied = false;
|
this.accessDenied = false;
|
||||||
|
|
||||||
if (this.properties.audioSource instanceof MediaStreamTrack) {
|
if (typeof MediaStreamTrack !== 'undefined' && this.properties.audioSource instanceof MediaStreamTrack) {
|
||||||
mediaStream.removeTrack(mediaStream.getAudioTracks()[0]);
|
mediaStream.removeTrack(mediaStream.getAudioTracks()[0]);
|
||||||
mediaStream.addTrack((<MediaStreamTrack>this.properties.audioSource));
|
mediaStream.addTrack((<MediaStreamTrack>this.properties.audioSource));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.properties.videoSource instanceof MediaStreamTrack) {
|
if (typeof MediaStreamTrack !== 'undefined' && this.properties.videoSource instanceof MediaStreamTrack) {
|
||||||
mediaStream.removeTrack(mediaStream.getVideoTracks()[0]);
|
mediaStream.removeTrack(mediaStream.getVideoTracks()[0]);
|
||||||
mediaStream.addTrack((<MediaStreamTrack>this.properties.videoSource));
|
mediaStream.addTrack((<MediaStreamTrack>this.properties.videoSource));
|
||||||
}
|
}
|
||||||
|
@ -310,18 +317,61 @@ export class Publisher extends StreamManager {
|
||||||
this.videoReference.setAttribute('playsinline', 'true');
|
this.videoReference.setAttribute('playsinline', 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.videoReference.srcObject = mediaStream;
|
if (!!this.firstVideoElement) {
|
||||||
|
let video = this.createVideoElement(this.firstVideoElement.targetElement, <VideoInsertMode>this.properties.insertMode);
|
||||||
|
if (platform['isInternetExplorer']) {
|
||||||
|
this.videoReference = video;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.stream.setMediaStream(mediaStream);
|
this.stream.setMediaStream(mediaStream);
|
||||||
|
|
||||||
|
if (platform['isInternetExplorer']) {
|
||||||
|
AdapterJS.webRTCReady(isUsingPlugin => {
|
||||||
|
this.videoReference = this.customAttachMediaStreamIE(this.videoReference, mediaStream);
|
||||||
|
if (this.stream.isSendVideo()) {
|
||||||
|
if (!this.stream.isSendScreen()) {
|
||||||
|
/*this.videoReference.onloadedmetadata = () => {
|
||||||
|
this.stream.videoDimensions = {
|
||||||
|
width: this.videoReference.videoWidth,
|
||||||
|
height: this.videoReference.videoHeight
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: if screen-share, set this.screenShareResizeInterval
|
||||||
|
|
||||||
|
console.warn(this.stream.videoDimensions);
|
||||||
|
this.stream.isLocalStreamReadyToPublish = true;
|
||||||
|
this.stream.ee.emitEvent('stream-ready-to-publish', []);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
this.stream.videoDimensions = {
|
||||||
|
width: this.videoReference.videoWidth,
|
||||||
|
height: this.videoReference.videoHeight
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: if screen-share, set this.screenShareResizeInterval
|
||||||
|
|
||||||
|
console.warn(this.stream.videoDimensions);
|
||||||
|
this.stream.isLocalStreamReadyToPublish = true;
|
||||||
|
this.stream.ee.emitEvent('stream-ready-to-publish', []);
|
||||||
|
|
||||||
|
this.videoReference.onplaying = () => {
|
||||||
|
console.warn("PLAYINNNGNGNGNGNGNG!!!");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.videoReference.srcObject = mediaStream;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.stream.displayMyRemote()) {
|
if (!this.stream.displayMyRemote()) {
|
||||||
// When we are subscribed to our remote we don't still set the MediaStream object in the video elements to
|
// When we are subscribed to our remote we don't still set the MediaStream object in the video elements to
|
||||||
// avoid early 'streamPlaying' event
|
// avoid early 'streamPlaying' event
|
||||||
this.stream.updateMediaStreamInVideos();
|
this.stream.updateMediaStreamInVideos();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!!this.firstVideoElement) {
|
|
||||||
this.createVideoElement(this.firstVideoElement.targetElement, <VideoInsertMode>this.properties.insertMode);
|
|
||||||
}
|
|
||||||
delete this.firstVideoElement;
|
delete this.firstVideoElement;
|
||||||
|
|
||||||
if (this.stream.isSendVideo()) {
|
if (this.stream.isSendVideo()) {
|
||||||
|
@ -357,8 +407,8 @@ export class Publisher extends StreamManager {
|
||||||
videoDimensionsSet();
|
videoDimensionsSet();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else {
|
} else if (platform.name !== 'IE') {
|
||||||
// Rest of platforms
|
// Rest of platforms except IE
|
||||||
// With no screen share, video dimension can be set directly from MediaStream (getSettings)
|
// With no screen share, video dimension can be set directly from MediaStream (getSettings)
|
||||||
// Orientation must be checked for mobile devices (width and height are reversed)
|
// Orientation must be checked for mobile devices (width and height are reversed)
|
||||||
const { width, height } = mediaStream.getVideoTracks()[0].getSettings();
|
const { width, height } = mediaStream.getVideoTracks()[0].getSettings();
|
||||||
|
@ -480,14 +530,14 @@ export class Publisher extends StreamManager {
|
||||||
// - video track is given and no audio
|
// - video track is given and no audio
|
||||||
// - audio track is given and no video
|
// - audio track is given and no video
|
||||||
// - both video and audio tracks are given
|
// - both video and audio tracks are given
|
||||||
if ((this.properties.videoSource instanceof MediaStreamTrack && !this.properties.audioSource)
|
if ((typeof MediaStreamTrack !== 'undefined' && this.properties.videoSource instanceof MediaStreamTrack && !this.properties.audioSource)
|
||||||
|| (this.properties.audioSource instanceof MediaStreamTrack && !this.properties.videoSource)
|
|| (typeof MediaStreamTrack !== 'undefined' && this.properties.audioSource instanceof MediaStreamTrack && !this.properties.videoSource)
|
||||||
|| (this.properties.videoSource instanceof MediaStreamTrack && this.properties.audioSource instanceof MediaStreamTrack)) {
|
|| (typeof MediaStreamTrack !== 'undefined' && this.properties.videoSource instanceof MediaStreamTrack && this.properties.audioSource instanceof MediaStreamTrack)) {
|
||||||
const mediaStream = new MediaStream();
|
const mediaStream = new MediaStream();
|
||||||
if (this.properties.videoSource instanceof MediaStreamTrack) {
|
if (typeof MediaStreamTrack !== 'undefined' && this.properties.videoSource instanceof MediaStreamTrack) {
|
||||||
mediaStream.addTrack((<MediaStreamTrack>this.properties.videoSource));
|
mediaStream.addTrack((<MediaStreamTrack>this.properties.videoSource));
|
||||||
}
|
}
|
||||||
if (this.properties.audioSource instanceof MediaStreamTrack) {
|
if (typeof MediaStreamTrack !== 'undefined' && this.properties.audioSource instanceof MediaStreamTrack) {
|
||||||
mediaStream.addTrack((<MediaStreamTrack>this.properties.audioSource));
|
mediaStream.addTrack((<MediaStreamTrack>this.properties.audioSource));
|
||||||
}
|
}
|
||||||
// MediaStreamTracks are handled within callback - just call callback with new MediaStream() and let it handle the sources
|
// MediaStreamTracks are handled within callback - just call callback with new MediaStream() and let it handle the sources
|
||||||
|
@ -521,11 +571,16 @@ export class Publisher extends StreamManager {
|
||||||
afterGetMedia(mediaStream, definedAudioConstraint);
|
afterGetMedia(mediaStream, definedAudioConstraint);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
let userMediaFunc = () => {
|
||||||
navigator.mediaDevices.getUserMedia(constraintsAux)
|
navigator.mediaDevices.getUserMedia(constraintsAux)
|
||||||
.then(mediaStream => {
|
.then(mediaStream => {
|
||||||
afterGetMedia(mediaStream, definedAudioConstraint);
|
afterGetMedia(mediaStream, definedAudioConstraint);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
this.clearPermissionDialogTimer(startTime, timeForDialogEvent);
|
this.clearPermissionDialogTimer(startTime, timeForDialogEvent);
|
||||||
if (error.name === 'Error') {
|
if (error.name === 'Error') {
|
||||||
// Safari OverConstrainedError has as name property 'Error' instead of 'OverConstrainedError'
|
// Safari OverConstrainedError has as name property 'Error' instead of 'OverConstrainedError'
|
||||||
|
@ -599,6 +654,16 @@ export class Publisher extends StreamManager {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (platform['isInternetExplorer']) {
|
||||||
|
AdapterJS.webRTCReady(isUsingPlugin => {
|
||||||
|
userMediaFunc();
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
userMediaFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
reject(new OpenViduError(OpenViduErrorName.NO_INPUT_SOURCE_SET,
|
reject(new OpenViduError(OpenViduErrorName.NO_INPUT_SOURCE_SET,
|
||||||
"Properties 'audioSource' and 'videoSource' cannot be set to false or null at the same time when calling 'OpenVidu.initPublisher'"));
|
"Properties 'audioSource' and 'videoSource' cannot be set to false or null at the same time when calling 'OpenVidu.initPublisher'"));
|
||||||
|
|
|
@ -160,7 +160,7 @@ export class Session implements EventDispatcher {
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
reject(new OpenViduError(OpenViduErrorName.BROWSER_NOT_SUPPORTED, 'Browser ' + platform.name + ' for ' + platform.os!!.family + ' is not supported in OpenVidu'));
|
reject(new OpenViduError(OpenViduErrorName.BROWSER_NOT_SUPPORTED, 'Browser ' + platform.name + ' (version ' + platform.version + ') for ' + platform.os!!.family + ' is not supported in OpenVidu'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -882,10 +882,17 @@ export class Session implements EventDispatcher {
|
||||||
this.getConnection(msg.senderConnectionId, 'Connection not found for connectionId ' + msg.senderConnectionId + ' owning endpoint ' + msg.endpointName + '. Ice candidate will be ignored: ' + candidate)
|
this.getConnection(msg.senderConnectionId, 'Connection not found for connectionId ' + msg.senderConnectionId + ' owning endpoint ' + msg.endpointName + '. Ice candidate will be ignored: ' + candidate)
|
||||||
.then(connection => {
|
.then(connection => {
|
||||||
const stream = connection.stream;
|
const stream = connection.stream;
|
||||||
|
if (platform['isInternetExplorer']) {
|
||||||
|
(<any>stream.getWebRtcPeer()).addIceCandidate(candidate, () => {}, error => {
|
||||||
|
console.error('Error adding candidate for ' + stream.streamId
|
||||||
|
+ ' stream of endpoint ' + msg.endpointName + ': ' + error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
stream.getWebRtcPeer().addIceCandidate(candidate).catch(error => {
|
stream.getWebRtcPeer().addIceCandidate(candidate).catch(error => {
|
||||||
console.error('Error adding candidate for ' + stream.streamId
|
console.error('Error adding candidate for ' + stream.streamId
|
||||||
+ ' stream of endpoint ' + msg.endpointName + ': ' + error);
|
+ ' stream of endpoint ' + msg.endpointName + ': ' + error);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(openViduError => {
|
.catch(openViduError => {
|
||||||
console.error(openViduError);
|
console.error(openViduError);
|
||||||
|
@ -1023,10 +1030,10 @@ export class Session implements EventDispatcher {
|
||||||
const joinParams = {
|
const joinParams = {
|
||||||
token: (!!token) ? token : '',
|
token: (!!token) ? token : '',
|
||||||
session: this.sessionId,
|
session: this.sessionId,
|
||||||
platform: platform.description,
|
platform: !!platform.description ? platform.description : 'unknown',
|
||||||
metadata: !!this.options.metadata ? this.options.metadata : '',
|
metadata: !!this.options.metadata ? this.options.metadata : '',
|
||||||
secret: this.openvidu.getSecret(),
|
secret: this.openvidu.getSecret(),
|
||||||
recorder: this.openvidu.getRecorder(),
|
recorder: this.openvidu.getRecorder()
|
||||||
};
|
};
|
||||||
|
|
||||||
this.openvidu.sendRequest('joinRoom', joinParams, (error, response) => {
|
this.openvidu.sendRequest('joinRoom', joinParams, (error, response) => {
|
||||||
|
|
|
@ -35,6 +35,9 @@ import EventEmitter = require('wolfy87-eventemitter');
|
||||||
import hark = require('hark');
|
import hark = require('hark');
|
||||||
import platform = require('platform');
|
import platform = require('platform');
|
||||||
platform['isIonicIos'] = (platform.product === 'iPhone' || platform.product === 'iPad') && platform.ua!!.indexOf('Safari') === -1;
|
platform['isIonicIos'] = (platform.product === 'iPhone' || platform.product === 'iPad') && platform.ua!!.indexOf('Safari') === -1;
|
||||||
|
platform['isInternetExplorer'] = platform.name === 'IE' && platform.version !== undefined && parseInt(platform.version) >= 11;
|
||||||
|
platform['isReactNative'] = navigator.product === 'ReactNative';
|
||||||
|
declare const AdapterJS: any;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -221,7 +224,7 @@ export class Stream implements EventDispatcher {
|
||||||
if (this.hasVideo) {
|
if (this.hasVideo) {
|
||||||
this.videoActive = !!this.outboundStreamOpts.publisherProperties.publishVideo;
|
this.videoActive = !!this.outboundStreamOpts.publisherProperties.publishVideo;
|
||||||
this.frameRate = this.outboundStreamOpts.publisherProperties.frameRate;
|
this.frameRate = this.outboundStreamOpts.publisherProperties.frameRate;
|
||||||
if (this.outboundStreamOpts.publisherProperties.videoSource instanceof MediaStreamTrack) {
|
if (typeof MediaStreamTrack !== 'undefined' && this.outboundStreamOpts.publisherProperties.videoSource instanceof MediaStreamTrack) {
|
||||||
this.typeOfVideo = 'CUSTOM';
|
this.typeOfVideo = 'CUSTOM';
|
||||||
} else {
|
} else {
|
||||||
this.typeOfVideo = this.isSendScreen() ? 'SCREEN' : 'CAMERA';
|
this.typeOfVideo = this.isSendScreen() ? 'SCREEN' : 'CAMERA';
|
||||||
|
@ -455,7 +458,7 @@ export class Stream implements EventDispatcher {
|
||||||
disposeWebRtcPeer(): void {
|
disposeWebRtcPeer(): void {
|
||||||
if (this.webRtcPeer) {
|
if (this.webRtcPeer) {
|
||||||
const isSenderAndCustomTrack: boolean = !!this.outboundStreamOpts &&
|
const isSenderAndCustomTrack: boolean = !!this.outboundStreamOpts &&
|
||||||
this.outboundStreamOpts.publisherProperties.videoSource instanceof MediaStreamTrack;
|
typeof MediaStreamTrack !== 'undefined' && this.outboundStreamOpts.publisherProperties.videoSource instanceof MediaStreamTrack;
|
||||||
this.webRtcPeer.dispose(isSenderAndCustomTrack);
|
this.webRtcPeer.dispose(isSenderAndCustomTrack);
|
||||||
}
|
}
|
||||||
if (this.speechEvent) {
|
if (this.speechEvent) {
|
||||||
|
@ -693,7 +696,7 @@ export class Stream implements EventDispatcher {
|
||||||
|
|
||||||
let typeOfVideo = '';
|
let typeOfVideo = '';
|
||||||
if (this.isSendVideo()) {
|
if (this.isSendVideo()) {
|
||||||
typeOfVideo = this.outboundStreamOpts.publisherProperties.videoSource instanceof MediaStreamTrack ? 'CUSTOM' : (this.isSendScreen() ? 'SCREEN' : 'CAMERA');
|
typeOfVideo = (typeof MediaStreamTrack !== 'undefined' && this.outboundStreamOpts.publisherProperties.videoSource instanceof MediaStreamTrack) ? 'CUSTOM' : (this.isSendScreen() ? 'SCREEN' : 'CAMERA');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.session.openvidu.sendRequest('publishVideo', {
|
this.session.openvidu.sendRequest('publishVideo', {
|
||||||
|
@ -795,6 +798,7 @@ export class Stream implements EventDispatcher {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const initWebRtcPeer = () => {
|
||||||
this.webRtcPeer = new WebRtcPeerRecvonly(options);
|
this.webRtcPeer = new WebRtcPeerRecvonly(options);
|
||||||
this.webRtcPeer.generateOffer()
|
this.webRtcPeer.generateOffer()
|
||||||
.then(offer => {
|
.then(offer => {
|
||||||
|
@ -803,12 +807,21 @@ export class Stream implements EventDispatcher {
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
reject(new Error('(subscribe) SDP offer error: ' + JSON.stringify(error)));
|
reject(new Error('(subscribe) SDP offer error: ' + JSON.stringify(error)));
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (platform['isInternetExplorer']) {
|
||||||
|
AdapterJS.webRTCReady(isUsingPlugin => {
|
||||||
|
initWebRtcPeer();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
initWebRtcPeer();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private remotePeerSuccessfullyEstablished(): void {
|
private remotePeerSuccessfullyEstablished(): void {
|
||||||
if (platform['isIonicIos']) {
|
if (platform['isIonicIos'] || platform['isInternetExplorer']) {
|
||||||
// iOS Ionic. LIMITATION: must use deprecated WebRTC API
|
// iOS Ionic or IExplorer. LIMITATION: must use deprecated WebRTC API
|
||||||
const pc1: any = this.webRtcPeer.pc;
|
const pc1: any = this.webRtcPeer.pc;
|
||||||
this.mediaStream = pc1.getRemoteStreams()[0];
|
this.mediaStream = pc1.getRemoteStreams()[0];
|
||||||
} else {
|
} else {
|
||||||
|
@ -836,7 +849,7 @@ export class Stream implements EventDispatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ee.emitEvent('mediastream-updated', []);
|
this.updateMediaStreamInVideos();
|
||||||
if (!this.displayMyRemote() && !!this.mediaStream.getAudioTracks()[0] && this.session.speakingEventsEnabled) {
|
if (!this.displayMyRemote() && !!this.mediaStream.getAudioTracks()[0] && this.session.speakingEventsEnabled) {
|
||||||
this.enableSpeakingEvents();
|
this.enableSpeakingEvents();
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,9 @@ import { VideoInsertMode } from '../OpenViduInternal/Enums/VideoInsertMode';
|
||||||
import EventEmitter = require('wolfy87-eventemitter');
|
import EventEmitter = require('wolfy87-eventemitter');
|
||||||
import platform = require('platform');
|
import platform = require('platform');
|
||||||
platform['isIonicIos'] = (platform.product === 'iPhone' || platform.product === 'iPad') && platform.ua!!.indexOf('Safari') === -1;
|
platform['isIonicIos'] = (platform.product === 'iPhone' || platform.product === 'iPad') && platform.ua!!.indexOf('Safari') === -1;
|
||||||
|
platform['isInternetExplorer'] = platform.name === 'IE' && platform.version !== undefined && parseInt(platform.version) >= 11;
|
||||||
|
platform['isReactNative'] = navigator.product === 'ReactNative';
|
||||||
|
declare const attachMediaStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface in charge of displaying the media streams in the HTML DOM. This wraps any [[Publisher]] and [[Subscriber]] object.
|
* Interface in charge of displaying the media streams in the HTML DOM. This wraps any [[Publisher]] and [[Subscriber]] object.
|
||||||
|
@ -240,6 +243,10 @@ export class StreamManager implements EventDispatcher {
|
||||||
video.srcObject = this.stream.getMediaStream();
|
video.srcObject = this.stream.getMediaStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (platform['isInternetExplorer'] && !!this.stream.getMediaStream()) {
|
||||||
|
video = this.customAttachMediaStreamIE(video, this.stream.getMediaStream());
|
||||||
|
}
|
||||||
|
|
||||||
// If the video element is already part of this StreamManager do nothing
|
// If the video element is already part of this StreamManager do nothing
|
||||||
for (const v of this.videos) {
|
for (const v of this.videos) {
|
||||||
if (v.video === video) {
|
if (v.video === video) {
|
||||||
|
@ -293,7 +300,7 @@ export class StreamManager implements EventDispatcher {
|
||||||
throw new Error("The provided 'targetElement' couldn't be resolved to any HTML element: " + targetElement);
|
throw new Error("The provided 'targetElement' couldn't be resolved to any HTML element: " + targetElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
const video = document.createElement('video');
|
let video = document.createElement('video');
|
||||||
this.initializeVideoProperties(video);
|
this.initializeVideoProperties(video);
|
||||||
|
|
||||||
let insMode = !!insertMode ? insertMode : VideoInsertMode.APPEND;
|
let insMode = !!insertMode ? insertMode : VideoInsertMode.APPEND;
|
||||||
|
@ -319,6 +326,10 @@ export class StreamManager implements EventDispatcher {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (platform['isInternetExplorer'] && !!this.stream.getMediaStream()) {
|
||||||
|
video = this.customAttachMediaStreamIE(video, this.stream.getMediaStream());
|
||||||
|
}
|
||||||
|
|
||||||
const v: StreamManagerVideo = {
|
const v: StreamManagerVideo = {
|
||||||
targetElement: targEl,
|
targetElement: targEl,
|
||||||
video,
|
video,
|
||||||
|
@ -327,9 +338,12 @@ export class StreamManager implements EventDispatcher {
|
||||||
};
|
};
|
||||||
this.pushNewStreamManagerVideo(v);
|
this.pushNewStreamManagerVideo(v);
|
||||||
|
|
||||||
|
let launchVideoCreatedEvent = !platform['isInternetExplorer'];
|
||||||
|
if (launchVideoCreatedEvent) {
|
||||||
|
// For IE the event is called in this.customAttachMediaStreamIE
|
||||||
this.ee.emitEvent('videoElementCreated', [new VideoElementEvent(v.video, this, 'videoElementCreated')]);
|
this.ee.emitEvent('videoElementCreated', [new VideoElementEvent(v.video, this, 'videoElementCreated')]);
|
||||||
|
}
|
||||||
this.lazyLaunchVideoElementCreatedEvent = !!this.firstVideoElement;
|
this.lazyLaunchVideoElementCreatedEvent = !!this.firstVideoElement && launchVideoCreatedEvent;
|
||||||
|
|
||||||
return video;
|
return video;
|
||||||
}
|
}
|
||||||
|
@ -432,6 +446,9 @@ export class StreamManager implements EventDispatcher {
|
||||||
vParent!!.replaceChild(newVideo, streamManagerVideo.video);
|
vParent!!.replaceChild(newVideo, streamManagerVideo.video);
|
||||||
streamManagerVideo.video = newVideo;
|
streamManagerVideo.video = newVideo;
|
||||||
}
|
}
|
||||||
|
if (platform['isInternetExplorer']) {
|
||||||
|
this.customAttachMediaStreamIE(streamManagerVideo.video, mediaStream);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -462,4 +479,21 @@ export class StreamManager implements EventDispatcher {
|
||||||
video.style.webkitTransform = 'unset';
|
video.style.webkitTransform = 'unset';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected customAttachMediaStreamIE(video: HTMLVideoElement, mediaStream: MediaStream): HTMLVideoElement {
|
||||||
|
var simVideo = attachMediaStream(video, mediaStream);
|
||||||
|
|
||||||
|
// Replace HTMLVideoElemet (if exists) with new HTMLObjectElement returned by IE plugin
|
||||||
|
for (let i = 0; i < this.videos.length; i++) {
|
||||||
|
if (this.videos[i].video === video) {
|
||||||
|
this.videos[i].video = simVideo;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always launch videoElementCreated event after IE plugin has inserted simulated video into DOM
|
||||||
|
this.ee.emitEvent('videoElementCreated', [new VideoElementEvent(simVideo, this, 'videoElementCreated')]);
|
||||||
|
this.addPlayEventToFirstVideo();
|
||||||
|
return simVideo;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -19,6 +19,8 @@ import freeice = require('freeice');
|
||||||
import uuid = require('uuid');
|
import uuid = require('uuid');
|
||||||
import platform = require('platform');
|
import platform = require('platform');
|
||||||
platform['isIonicIos'] = (platform.product === 'iPhone' || platform.product === 'iPad') && platform.ua!!.indexOf('Safari') === -1;
|
platform['isIonicIos'] = (platform.product === 'iPhone' || platform.product === 'iPad') && platform.ua!!.indexOf('Safari') === -1;
|
||||||
|
platform['isInternetExplorer'] = platform.name === 'IE' && platform.version !== undefined && parseInt(platform.version) >= 11;
|
||||||
|
platform['isReactNative'] = navigator.product === 'ReactNative';
|
||||||
|
|
||||||
export interface WebRtcPeerConfiguration {
|
export interface WebRtcPeerConfiguration {
|
||||||
mediaConstraints: {
|
mediaConstraints: {
|
||||||
|
@ -66,9 +68,13 @@ export class WebRtcPeer {
|
||||||
this.pc.onsignalingstatechange = () => {
|
this.pc.onsignalingstatechange = () => {
|
||||||
if (this.pc.signalingState === 'stable') {
|
if (this.pc.signalingState === 'stable') {
|
||||||
while (this.iceCandidateList.length > 0) {
|
while (this.iceCandidateList.length > 0) {
|
||||||
|
if (platform['isInternetExplorer']) {
|
||||||
|
(<any>this.pc).addIceCandidate(<RTCIceCandidate>this.iceCandidateList.shift(), () => {}, () => {});
|
||||||
|
} else {
|
||||||
this.pc.addIceCandidate(<RTCIceCandidate>this.iceCandidateList.shift());
|
this.pc.addIceCandidate(<RTCIceCandidate>this.iceCandidateList.shift());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.start();
|
this.start();
|
||||||
|
@ -87,8 +93,8 @@ export class WebRtcPeer {
|
||||||
reject('The peer connection object is in "closed" state. This is most likely due to an invocation of the dispose method before accepting in the dialogue');
|
reject('The peer connection object is in "closed" state. This is most likely due to an invocation of the dispose method before accepting in the dialogue');
|
||||||
}
|
}
|
||||||
if (!!this.configuration.mediaStream) {
|
if (!!this.configuration.mediaStream) {
|
||||||
if (platform['isIonicIos']) {
|
if (platform['isIonicIos'] || platform['isInternetExplorer']) {
|
||||||
// iOS Ionic. LIMITATION: must use deprecated WebRTC API
|
// iOS Ionic and IExplorer. LIMITATION: must use deprecated WebRTC API
|
||||||
const pc2: any = this.pc;
|
const pc2: any = this.pc;
|
||||||
pc2.addStream(this.configuration.mediaStream);
|
pc2.addStream(this.configuration.mediaStream);
|
||||||
} else {
|
} else {
|
||||||
|
@ -114,8 +120,8 @@ export class WebRtcPeer {
|
||||||
this.remoteCandidatesQueue = [];
|
this.remoteCandidatesQueue = [];
|
||||||
this.localCandidatesQueue = [];
|
this.localCandidatesQueue = [];
|
||||||
|
|
||||||
if (platform['isIonicIos']) {
|
if (platform['isIonicIos'] || platform['isInternetExplorer']) {
|
||||||
// iOS Ionic. LIMITATION: must use deprecated WebRTC API
|
// iOS Ionic or IExplorer. LIMITATION: must use deprecated WebRTC API
|
||||||
// Stop senders deprecated
|
// Stop senders deprecated
|
||||||
const pc1: any = this.pc;
|
const pc1: any = this.pc;
|
||||||
for (const sender of pc1.getLocalStreams()) {
|
for (const sender of pc1.getLocalStreams()) {
|
||||||
|
@ -156,7 +162,7 @@ export class WebRtcPeer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1) Function that creates an offer, sets it as local description and returns the offer param
|
* Function that creates an offer, sets it as local description and returns the offer param
|
||||||
* to send to OpenVidu Server (will be the remote description of other peer)
|
* to send to OpenVidu Server (will be the remote description of other peer)
|
||||||
*/
|
*/
|
||||||
generateOffer(): Promise<string> {
|
generateOffer(): Promise<string> {
|
||||||
|
@ -209,11 +215,43 @@ export class WebRtcPeer {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => reject(error));
|
.catch(error => reject(error));
|
||||||
|
|
||||||
|
} else if (platform['isInternetExplorer']) {
|
||||||
|
|
||||||
|
// IE Explorer cannot use Promise base API
|
||||||
|
let setLocalDescriptionOnSuccess = () => {
|
||||||
|
const localDescription = this.pc.localDescription;
|
||||||
|
if (!!localDescription) {
|
||||||
|
console.debug('Local description set', localDescription.sdp);
|
||||||
|
resolve(localDescription.sdp);
|
||||||
} else {
|
} else {
|
||||||
|
reject('Local description is not defined');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let setLocalDescriptionOnError = error => {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
let createOfferOnSuccess = offer => {
|
||||||
|
console.debug('Created SDP offer');
|
||||||
|
(<any>this.pc).setLocalDescription(offer, setLocalDescriptionOnSuccess, setLocalDescriptionOnError);
|
||||||
|
};
|
||||||
|
let createOfferOnError = error => {
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
// FIX: for IExplorer WebRTC peer connections must be negotiated to receive video and audio
|
||||||
|
constraints.offerToReceiveAudio = true;
|
||||||
|
constraints.offerToReceiveVideo = true;
|
||||||
|
(<any>this.pc).createOffer(createOfferOnSuccess, createOfferOnError, constraints);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Rest of platforms
|
||||||
this.pc.createOffer(constraints).then(offer => {
|
this.pc.createOffer(constraints).then(offer => {
|
||||||
console.debug('Created SDP offer');
|
console.debug('Created SDP offer');
|
||||||
return this.pc.setLocalDescription(offer);
|
return this.pc.setLocalDescription(offer);
|
||||||
}).then(() => {
|
})
|
||||||
|
.then(() => {
|
||||||
const localDescription = this.pc.localDescription;
|
const localDescription = this.pc.localDescription;
|
||||||
if (!!localDescription) {
|
if (!!localDescription) {
|
||||||
console.debug('Local description set', localDescription.sdp);
|
console.debug('Local description set', localDescription.sdp);
|
||||||
|
@ -228,52 +266,15 @@ export class WebRtcPeer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 2) Function to invoke when a SDP offer is received. Sets it as remote description,
|
* Function invoked when a SDP answer is received. Final step in SDP negotiation, the peer
|
||||||
* generates and answer and returns it to send it to OpenVidu Server
|
|
||||||
*/
|
|
||||||
processOffer(sdpOffer: string): Promise<ConstrainDOMString> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const offer: RTCSessionDescriptionInit = {
|
|
||||||
type: 'offer',
|
|
||||||
sdp: sdpOffer
|
|
||||||
};
|
|
||||||
|
|
||||||
console.debug('SDP offer received, setting remote description');
|
|
||||||
|
|
||||||
if (this.pc.signalingState === 'closed') {
|
|
||||||
reject('PeerConnection is closed');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pc.setRemoteDescription(offer)
|
|
||||||
.then(() => {
|
|
||||||
return this.pc.createAnswer();
|
|
||||||
}).then(answer => {
|
|
||||||
console.debug('Created SDP answer');
|
|
||||||
return this.pc.setLocalDescription(answer);
|
|
||||||
}).then(() => {
|
|
||||||
const localDescription = this.pc.localDescription;
|
|
||||||
if (!!localDescription) {
|
|
||||||
console.debug('Local description set', localDescription.sdp);
|
|
||||||
resolve(<string>localDescription.sdp);
|
|
||||||
} else {
|
|
||||||
reject('Local description is not defined');
|
|
||||||
}
|
|
||||||
}).catch(error => reject(error));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 3) Function invoked when a SDP answer is received. Final step in SDP negotiation, the peer
|
|
||||||
* just needs to set the answer as its remote description
|
* just needs to set the answer as its remote description
|
||||||
*/
|
*/
|
||||||
processAnswer(sdpAnswer: string, needsTimeoutOnProcessAswer: boolean): Promise<string> {
|
processAnswer(sdpAnswer: string, needsTimeoutOnProcessAswer: boolean): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
const answer: RTCSessionDescriptionInit = {
|
const answer: RTCSessionDescriptionInit = {
|
||||||
type: 'answer',
|
type: 'answer',
|
||||||
sdp: sdpAnswer
|
sdp: sdpAnswer
|
||||||
};
|
};
|
||||||
|
|
||||||
console.debug('SDP answer received, setting remote description');
|
console.debug('SDP answer received, setting remote description');
|
||||||
|
|
||||||
if (this.pc.signalingState === 'closed') {
|
if (this.pc.signalingState === 'closed') {
|
||||||
|
@ -285,8 +286,14 @@ export class WebRtcPeer {
|
||||||
this.pc.setRemoteDescription(answer).then(() => resolve()).catch(error => reject(error));
|
this.pc.setRemoteDescription(answer).then(() => resolve()).catch(error => reject(error));
|
||||||
}, 250);
|
}, 250);
|
||||||
} else {
|
} else {
|
||||||
|
if (platform['isInternetExplorer']) {
|
||||||
|
// IE Explorer cannot use Promise base API
|
||||||
|
(<any>this.pc).setRemoteDescription(answer, resolve(), error => reject(error));
|
||||||
|
} else {
|
||||||
|
// Rest of platforms
|
||||||
this.pc.setRemoteDescription(answer).then(() => resolve()).catch(error => reject(error));
|
this.pc.setRemoteDescription(answer).then(() => resolve()).catch(error => reject(error));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -303,8 +310,12 @@ export class WebRtcPeer {
|
||||||
break;
|
break;
|
||||||
case 'stable':
|
case 'stable':
|
||||||
if (!!this.pc.remoteDescription) {
|
if (!!this.pc.remoteDescription) {
|
||||||
|
if (platform['isInternetExplorer']) {
|
||||||
|
(<any>this.pc).addIceCandidate(iceCandidate, () => resolve(), error => reject(error));
|
||||||
|
} else {
|
||||||
this.pc.addIceCandidate(iceCandidate).then(() => resolve()).catch(error => reject(error));
|
this.pc.addIceCandidate(iceCandidate).then(() => resolve()).catch(error => reject(error));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
this.iceCandidateList.push(iceCandidate);
|
this.iceCandidateList.push(iceCandidate);
|
||||||
|
|
Loading…
Reference in New Issue