Adds IE support through Temasys WebRTC plugin.
This commit is contained in:
parent
cd6928d770
commit
ae759fab5b
5
app.js
5
app.js
|
@ -21,14 +21,11 @@ var APP =
|
|||
|
||||
function init() {
|
||||
|
||||
APP.desktopsharing.init();
|
||||
APP.RTC.start();
|
||||
APP.xmpp.start();
|
||||
APP.statistics.start();
|
||||
APP.connectionquality.init();
|
||||
|
||||
// Set default desktop sharing method
|
||||
APP.desktopsharing.init();
|
||||
|
||||
APP.keyboardshortcut.init();
|
||||
APP.members.start();
|
||||
}
|
||||
|
|
|
@ -60,28 +60,31 @@
|
|||
}
|
||||
|
||||
#remoteVideos .videocontainer:hover {
|
||||
-webkit-box-shadow: inset 0 0 10px #FFFFFF, 0 0 10px #FFFFFF;
|
||||
box-shadow: inset 0 0 10px #FFFFFF, 0 0 10px #FFFFFF;
|
||||
border: 2px solid #FFFFFF;
|
||||
}
|
||||
|
||||
#remoteVideos .videocontainer.videoContainerFocused {
|
||||
-webkit-box-shadow: inset 0 0 28px #006d91;
|
||||
box-shadow: inset 0 0 28px #006d91;
|
||||
border: 2px solid #006d91;
|
||||
}
|
||||
|
||||
#remoteVideos .videocontainer.videoContainerFocused:hover {
|
||||
-webkit-box-shadow: inset 0 0 5px #FFFFFF, 0 0 10px #FFFFFF, inset 0 0 60px #006d91;
|
||||
box-shadow: inset 0 0 5px #FFFFFF, 0 0 10px #FFFFFF, inset 0 0 60px #006d91;
|
||||
border: 2px solid #FFFFFF;
|
||||
}
|
||||
|
||||
#localVideoWrapper {
|
||||
display:inline-block;
|
||||
-webkit-mask-box-image: url(../images/videomask.svg);
|
||||
border-radius:0px !important;
|
||||
border-radius:4px !important;
|
||||
border: 0px !important;
|
||||
}
|
||||
|
||||
#remoteVideos .videocontainer>video {
|
||||
/* With TemasysWebRTC plugin <object/> element is used
|
||||
instead of <video/> */
|
||||
#remoteVideos .videocontainer>video,
|
||||
#remoteVideos .videocontainer>object {
|
||||
border-radius:4px;
|
||||
}
|
||||
|
||||
|
@ -92,8 +95,9 @@
|
|||
-o-transform: scale(-1, 1);
|
||||
}
|
||||
|
||||
#localVideoWrapper>video {
|
||||
border-radius:0px !important;
|
||||
#localVideoWrapper>video,
|
||||
#localVideoWrapper>object {
|
||||
border-radius:4px !important;
|
||||
}
|
||||
|
||||
#largeVideo,
|
||||
|
@ -110,8 +114,10 @@
|
|||
#presentation,
|
||||
#etherpad,
|
||||
#localVideoWrapper>video,
|
||||
#localVideoWrapper>object,
|
||||
#localVideoWrapper,
|
||||
.videocontainer>video {
|
||||
.videocontainer>video,
|
||||
.videocontainer>object {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
@ -363,7 +369,7 @@
|
|||
margin-right: 40%;
|
||||
text-align: center;
|
||||
background: linear-gradient(to bottom, rgba(255,255,255,.85) , rgba(255,255,255,.35));
|
||||
-webkit-box-shadow: 0 0 2px #000000, 0 0 10px #000000;
|
||||
box-shadow: 0 0 2px #000000, 0 0 10px #000000;
|
||||
border-bottom-left-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
display: none;
|
||||
|
|
|
@ -22,12 +22,12 @@
|
|||
<script src="libs/popover.js?v=1"></script><!-- bootstrap tooltip lib -->
|
||||
<script src="libs/toastr.js?v=1"></script><!-- notifications lib -->
|
||||
<script src="interface_config.js?v=5"></script>
|
||||
<script src="libs/app.bundle.js?v=98"></script>
|
||||
<script src="libs/app.bundle.js?v=99"></script>
|
||||
<script src="analytics.js?v=1"></script><!-- google analytics plugin -->
|
||||
<link rel="stylesheet" href="css/font.css?v=7"/>
|
||||
<link rel="stylesheet" href="css/toastr.css?v=1">
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="css/main.css?v=30"/>
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="css/videolayout_default.css?v=17" id="videolayout_default"/>
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="css/videolayout_default.css?v=18" id="videolayout_default"/>
|
||||
<link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/jquery-impromptu.css?v=4">
|
||||
<link rel="stylesheet" href="css/modaldialog.css?v=3">
|
||||
|
|
2387
libs/app.bundle.js
2387
libs/app.bundle.js
File diff suppressed because it is too large
Load Diff
|
@ -1,4 +1,5 @@
|
|||
var EventEmitter = require("events");
|
||||
var RTCBrowserType = require("./RTCBrowserType");
|
||||
var RTCUtils = require("./RTCUtils.js");
|
||||
var LocalStream = require("./LocalStream.js");
|
||||
var DataChannels = require("./DataChannels");
|
||||
|
@ -99,7 +100,7 @@ var RTC = {
|
|||
},
|
||||
createRemoteStream: function (data, sid, thessrc) {
|
||||
var remoteStream = new MediaStream(data, sid, thessrc,
|
||||
this.getBrowserType(), eventEmitter);
|
||||
RTCBrowserType.getBrowserType(), eventEmitter);
|
||||
var jid = data.peerjid || APP.xmpp.myJid();
|
||||
if(!this.remoteStreams[jid]) {
|
||||
this.remoteStreams[jid] = {};
|
||||
|
@ -108,9 +109,6 @@ var RTC = {
|
|||
eventEmitter.emit(StreamEventTypes.EVENT_TYPE_REMOTE_CREATED, remoteStream);
|
||||
return remoteStream;
|
||||
},
|
||||
getBrowserType: function () {
|
||||
return this.rtcUtils.browser;
|
||||
},
|
||||
getPCConstraints: function () {
|
||||
return this.rtcUtils.pc_constraints;
|
||||
},
|
||||
|
@ -133,6 +131,9 @@ var RTC = {
|
|||
setVideoSrc: function (element, src) {
|
||||
this.rtcUtils.setVideoSrc(element, src);
|
||||
},
|
||||
getVideoElementName: function () {
|
||||
return RTCBrowserType.isTemasysPluginUsed() ? 'object' : 'video';
|
||||
},
|
||||
dispose: function() {
|
||||
if (this.rtcUtils) {
|
||||
this.rtcUtils = null;
|
||||
|
@ -168,9 +169,22 @@ var RTC = {
|
|||
DataChannels.handleSelectedEndpointEvent);
|
||||
APP.UI.addListener(UIEvents.PINNED_ENDPOINT,
|
||||
DataChannels.handlePinnedEndpointEvent);
|
||||
this.rtcUtils = new RTCUtils(this);
|
||||
this.rtcUtils.obtainAudioAndVideoPermissions(
|
||||
null, null, getMediaStreamUsage());
|
||||
|
||||
// In case of IE we continue from 'onReady' callback
|
||||
// passed to RTCUtils constructor. It will be invoked by Temasys plugin
|
||||
// once it is initialized.
|
||||
var onReady = function () {
|
||||
eventEmitter.emit(RTCEvents.RTC_READY, true);
|
||||
self.rtcUtils.obtainAudioAndVideoPermissions(
|
||||
null, null, getMediaStreamUsage());
|
||||
};
|
||||
|
||||
this.rtcUtils = new RTCUtils(this, onReady);
|
||||
|
||||
// Call onReady() if Temasys plugin is not used
|
||||
if (!RTCBrowserType.isTemasysPluginUsed()) {
|
||||
onReady();
|
||||
}
|
||||
},
|
||||
muteRemoteVideoStream: function (jid, value) {
|
||||
var stream;
|
||||
|
@ -211,6 +225,10 @@ var RTC = {
|
|||
callback();
|
||||
};
|
||||
}
|
||||
// FIXME: Workaround for FF/IE/Safari
|
||||
if (stream && stream.videoStream) {
|
||||
stream = stream.videoStream;
|
||||
}
|
||||
var videoStream = this.rtcUtils.createStream(stream, true);
|
||||
this.localVideo = this.createLocalStream(videoStream, "video", true, type);
|
||||
// Stop the stream to trigger onended event for old stream
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
|
||||
var currentBrowser;
|
||||
|
||||
var browserVersion;
|
||||
|
||||
var RTCBrowserType = {
|
||||
|
||||
RTC_BROWSER_CHROME: "rtc_browser.chrome",
|
||||
|
||||
RTC_BROWSER_OPERA: "rtc_browser.opera",
|
||||
|
||||
RTC_BROWSER_FIREFOX: "rtc_browser.firefox",
|
||||
|
||||
RTC_BROWSER_IEXPLORER: "rtc_browser.iexplorer",
|
||||
|
||||
RTC_BROWSER_SAFARI: "rtc_browser.safari",
|
||||
|
||||
getBrowserType: function () {
|
||||
return currentBrowser;
|
||||
},
|
||||
|
||||
isChrome: function () {
|
||||
return currentBrowser === RTCBrowserType.RTC_BROWSER_CHROME;
|
||||
},
|
||||
|
||||
isOpera: function () {
|
||||
return currentBrowser === RTCBrowserType.RTC_BROWSER_OPERA;
|
||||
},
|
||||
isFirefox: function () {
|
||||
return currentBrowser === RTCBrowserType.RTC_BROWSER_FIREFOX;
|
||||
},
|
||||
|
||||
isIExplorer: function () {
|
||||
return currentBrowser === RTCBrowserType.RTC_BROWSER_IEXPLORER;
|
||||
},
|
||||
|
||||
isSafari: function () {
|
||||
return currentBrowser === RTCBrowserType.RTC_BROWSER_SAFARI;
|
||||
},
|
||||
isTemasysPluginUsed: function () {
|
||||
return RTCBrowserType.isIExplorer() || RTCBrowserType.isSafari();
|
||||
},
|
||||
getFirefoxVersion: function () {
|
||||
return RTCBrowserType.isFirefox() ? browserVersion : null;
|
||||
},
|
||||
|
||||
getChromeVersion: function () {
|
||||
return RTCBrowserType.isChrome() ? browserVersion : null;
|
||||
}
|
||||
|
||||
// Add version getters for other browsers when needed
|
||||
};
|
||||
|
||||
// detectOpera() must be called before detectChrome() !!!
|
||||
// otherwise Opera wil be detected as Chrome
|
||||
function detectChrome() {
|
||||
if (navigator.webkitGetUserMedia) {
|
||||
currentBrowser = RTCBrowserType.RTC_BROWSER_CHROME;
|
||||
var userAgent = navigator.userAgent.toLowerCase();
|
||||
// We can assume that user agent is chrome, because it's
|
||||
// enforced when 'ext' streaming method is set
|
||||
var ver = parseInt(userAgent.match(/chrome\/(\d+)\./)[1], 10);
|
||||
console.log("This appears to be Chrome, ver: " + ver);
|
||||
return ver;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function detectOpera() {
|
||||
var userAgent = navigator.userAgent;
|
||||
if (userAgent.match(/Opera|OPR/)) {
|
||||
currentBrowser = RTCBrowserType.RTC_BROWSER_OPERA;
|
||||
var version = userAgent.match(/(Opera|OPR) ?\/?(\d+)\.?/)[2];
|
||||
console.info("This appears to be Opera, ver: " + version);
|
||||
return version;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function detectFirefox() {
|
||||
if (navigator.mozGetUserMedia) {
|
||||
currentBrowser = RTCBrowserType.RTC_BROWSER_FIREFOX;
|
||||
var version = parseInt(
|
||||
navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10);
|
||||
console.log('This appears to be Firefox, ver: ' + version);
|
||||
return version;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function detectSafari() {
|
||||
if ((/^((?!chrome).)*safari/i.test(navigator.userAgent))) {
|
||||
currentBrowser = RTCBrowserType.RTC_BROWSER_SAFARI;
|
||||
console.info("This appears to be Safari");
|
||||
// FIXME detect Safari version when needed
|
||||
return 1;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function detectIE() {
|
||||
var version;
|
||||
var ua = window.navigator.userAgent;
|
||||
|
||||
var msie = ua.indexOf('MSIE ');
|
||||
if (msie > 0) {
|
||||
// IE 10 or older => return version number
|
||||
version = parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10);
|
||||
}
|
||||
|
||||
var trident = ua.indexOf('Trident/');
|
||||
if (!version && trident > 0) {
|
||||
// IE 11 => return version number
|
||||
var rv = ua.indexOf('rv:');
|
||||
version = parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10);
|
||||
}
|
||||
|
||||
var edge = ua.indexOf('Edge/');
|
||||
if (!version && edge > 0) {
|
||||
// IE 12 => return version number
|
||||
version = parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10);
|
||||
}
|
||||
|
||||
if (version) {
|
||||
currentBrowser = RTCBrowserType.RTC_BROWSER_IEXPLORER;
|
||||
console.info("This appears to be IExplorer, ver: " + version);
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
function detectBrowser() {
|
||||
var version;
|
||||
var detectors = [
|
||||
detectOpera,
|
||||
detectChrome,
|
||||
detectFirefox,
|
||||
detectIE,
|
||||
detectSafari
|
||||
];
|
||||
// Try all browser detectors
|
||||
for (var i = 0; i < detectors.length; i++) {
|
||||
version = detectors[i]();
|
||||
if (version)
|
||||
return version;
|
||||
}
|
||||
console.error("Failed to detect browser type");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
browserVersion = detectBrowser();
|
||||
|
||||
module.exports = RTCBrowserType;
|
|
@ -1,5 +1,7 @@
|
|||
var RTCBrowserType = require("../../service/RTC/RTCBrowserType.js");
|
||||
var RTCBrowserType = require("./RTCBrowserType");
|
||||
var Resolutions = require("../../service/RTC/Resolutions");
|
||||
var AdapterJS = require("./adapter.screenshare");
|
||||
var SDPUtil = require("../xmpp/SDPUtil");
|
||||
|
||||
var currentResolution = null;
|
||||
|
||||
|
@ -58,16 +60,30 @@ function getConstraints(um, resolution, bandwidth, fps, desktopStream, isAndroid
|
|||
constraints.audio = { mandatory: {}, optional: []};// same behaviour as true
|
||||
}
|
||||
if (um.indexOf('screen') >= 0) {
|
||||
constraints.video = {
|
||||
mandatory: {
|
||||
chromeMediaSource: "screen",
|
||||
googLeakyBucket: true,
|
||||
maxWidth: window.screen.width,
|
||||
maxHeight: window.screen.height,
|
||||
maxFrameRate: 3
|
||||
},
|
||||
optional: []
|
||||
};
|
||||
if (RTCBrowserType.isChrome()) {
|
||||
constraints.video = {
|
||||
mandatory: {
|
||||
chromeMediaSource: "screen",
|
||||
googLeakyBucket: true,
|
||||
maxWidth: window.screen.width,
|
||||
maxHeight: window.screen.height,
|
||||
maxFrameRate: 3
|
||||
},
|
||||
optional: []
|
||||
};
|
||||
} else if (RTCBrowserType.isTemasysPluginUsed()) {
|
||||
constraints.video = {
|
||||
optional: [
|
||||
{
|
||||
sourceId: AdapterJS.WebRTCPlugin.plugin.screensharingKey
|
||||
}
|
||||
]
|
||||
};
|
||||
} else {
|
||||
console.error(
|
||||
"'screen' WebRTC media source is supported only in Chrome" +
|
||||
" and with Temasys plugin");
|
||||
}
|
||||
}
|
||||
if (um.indexOf('desktop') >= 0) {
|
||||
constraints.video = {
|
||||
|
@ -124,16 +140,14 @@ function getConstraints(um, resolution, bandwidth, fps, desktopStream, isAndroid
|
|||
}
|
||||
|
||||
|
||||
function RTCUtils(RTCService)
|
||||
function RTCUtils(RTCService, onTemasysPluginReady)
|
||||
{
|
||||
var self = this;
|
||||
this.service = RTCService;
|
||||
if (navigator.mozGetUserMedia) {
|
||||
console.log('This appears to be Firefox');
|
||||
var version = parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10);
|
||||
if (version >= 40
|
||||
&& config.useBundle && config.useRtcpMux) {
|
||||
if (RTCBrowserType.isFirefox()) {
|
||||
var FFversion = RTCBrowserType.getFirefoxVersion();
|
||||
if (FFversion >= 40 && config.useBundle && config.useRtcpMux) {
|
||||
this.peerconnection = mozRTCPeerConnection;
|
||||
this.browser = RTCBrowserType.RTC_BROWSER_FIREFOX;
|
||||
this.getUserMedia = navigator.mozGetUserMedia.bind(navigator);
|
||||
this.pc_constraints = {};
|
||||
this.attachMediaStream = function (element, stream) {
|
||||
|
@ -155,7 +169,7 @@ function RTCUtils(RTCService)
|
|||
{
|
||||
tracks = stream.getAudioTracks();
|
||||
}
|
||||
return tracks[0].id.replace(/[\{,\}]/g,"");
|
||||
return SDPUtil.filter_special_chars(tracks[0].id);
|
||||
};
|
||||
this.getVideoSrc = function (element) {
|
||||
if(!element)
|
||||
|
@ -169,14 +183,16 @@ function RTCUtils(RTCService)
|
|||
RTCSessionDescription = mozRTCSessionDescription;
|
||||
RTCIceCandidate = mozRTCIceCandidate;
|
||||
} else {
|
||||
console.error(
|
||||
"Firefox requirements not met, ver: " + FFversion +
|
||||
", bundle: " + config.useBundle +
|
||||
", rtcp-mux: " + config.useRtcpMux);
|
||||
window.location.href = 'unsupported_browser.html';
|
||||
return;
|
||||
}
|
||||
|
||||
} else if (navigator.webkitGetUserMedia) {
|
||||
console.log('This appears to be Chrome');
|
||||
} else if (RTCBrowserType.isChrome() || RTCBrowserType.isOpera()) {
|
||||
this.peerconnection = webkitRTCPeerConnection;
|
||||
this.browser = RTCBrowserType.RTC_BROWSER_CHROME;
|
||||
this.getUserMedia = navigator.webkitGetUserMedia.bind(navigator);
|
||||
this.attachMediaStream = function (element, stream) {
|
||||
element.attr('src', webkitURL.createObjectURL(stream));
|
||||
|
@ -184,7 +200,7 @@ function RTCUtils(RTCService)
|
|||
this.getStreamID = function (stream) {
|
||||
// streams from FF endpoints have the characters '{' and '}'
|
||||
// that make jQuery choke.
|
||||
return stream.id.replace(/[\{,\}]/g,"");
|
||||
return SDPUtil.filter_special_chars(stream.id);
|
||||
};
|
||||
this.getVideoSrc = function (element) {
|
||||
if(!element)
|
||||
|
@ -211,12 +227,62 @@ function RTCUtils(RTCService)
|
|||
};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try { console.log('Browser does not appear to be WebRTC-capable'); } catch (e) { }
|
||||
// Detect IE/Safari
|
||||
else if (RTCBrowserType.isTemasysPluginUsed()) {
|
||||
|
||||
AdapterJS.WebRTCPlugin.setLogLevel(
|
||||
AdapterJS.WebRTCPlugin.PLUGIN_LOG_LEVELS.VERBOSE);
|
||||
|
||||
AdapterJS.webRTCReady(function (isPlugin) {
|
||||
|
||||
self.peerconnection = RTCPeerConnection;
|
||||
self.getUserMedia = getUserMedia;
|
||||
self.attachMediaStream = function (element, stream) {
|
||||
|
||||
if (stream.id === "dummyAudio" || stream.id === "dummyVideo") {
|
||||
return;
|
||||
}
|
||||
|
||||
attachMediaStream(element[0], stream);
|
||||
};
|
||||
self.getStreamID = function (stream) {
|
||||
var id = SDPUtil.filter_special_chars(stream.label);
|
||||
return id;
|
||||
};
|
||||
self.getVideoSrc = function (element) {
|
||||
if (!element) {
|
||||
console.warn("Attempt to get video SRC of null element");
|
||||
return null;
|
||||
}
|
||||
var src = null;
|
||||
var children = element.children;
|
||||
for (var i = 0; i !== children.length; ++i) {
|
||||
if (children[i].name === 'streamId') {
|
||||
return children[i].value;
|
||||
}
|
||||
}
|
||||
//console.info(element.id + " SRC: " + src);
|
||||
return null;
|
||||
};
|
||||
self.setVideoSrc = function (element, src) {
|
||||
//console.info("Set video src: ", element, src);
|
||||
if (!src) {
|
||||
console.warn("Not attaching video stream, 'src' is null");
|
||||
return;
|
||||
}
|
||||
AdapterJS.WebRTCPlugin.WaitForPluginReady();
|
||||
var stream = AdapterJS.WebRTCPlugin.plugin
|
||||
.getStreamWithId(AdapterJS.WebRTCPlugin.pageId, src);
|
||||
attachMediaStream(element, stream);
|
||||
};
|
||||
|
||||
onTemasysPluginReady(isPlugin);
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
console.log('Browser does not appear to be WebRTC-capable');
|
||||
} catch (e) { }
|
||||
window.location.href = 'unsupported_browser.html';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -232,7 +298,7 @@ RTCUtils.prototype.getUserMediaWithConstraints = function(
|
|||
var constraints = getConstraints(
|
||||
um, resolution, bandwidth, fps, desktopStream, isAndroid);
|
||||
|
||||
var isFF = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
||||
console.info("Get media constraints", constraints);
|
||||
|
||||
var self = this;
|
||||
|
||||
|
@ -311,9 +377,9 @@ RTCUtils.prototype.obtainAudioAndVideoPermissions =
|
|||
return;
|
||||
}
|
||||
|
||||
if (navigator.mozGetUserMedia) {
|
||||
if (RTCBrowserType.isFirefox() || RTCBrowserType.isTemasysPluginUsed()) {
|
||||
|
||||
// With FF we can't split the stream into audio and video because FF
|
||||
// With FF/IE we can't split the stream into audio and video because FF
|
||||
// doesn't support media stream constructors. So, we need to get the
|
||||
// audio stream separately from the video stream using two distinct GUM
|
||||
// calls. Not very user friendly :-( but we don't have many other
|
||||
|
@ -321,31 +387,41 @@ RTCUtils.prototype.obtainAudioAndVideoPermissions =
|
|||
//
|
||||
// Note that we pack those 2 streams in a single object and pass it to
|
||||
// the successCallback method.
|
||||
|
||||
self.getUserMediaWithConstraints(
|
||||
['audio'],
|
||||
function (audioStream) {
|
||||
self.getUserMediaWithConstraints(
|
||||
['video'],
|
||||
function (videoStream) {
|
||||
return self.successCallback({
|
||||
audioStream: audioStream,
|
||||
videoStream: videoStream
|
||||
});
|
||||
},
|
||||
function (error) {
|
||||
console.error('failed to obtain video stream - stop',
|
||||
error);
|
||||
return self.successCallback(null);
|
||||
},
|
||||
config.resolution || '360');
|
||||
},
|
||||
function (error) {
|
||||
console.error('failed to obtain audio stream - stop',
|
||||
error);
|
||||
return self.successCallback(null);
|
||||
}
|
||||
);
|
||||
var obtainVideo = function (audioStream) {
|
||||
self.getUserMediaWithConstraints(
|
||||
['video'],
|
||||
function (videoStream) {
|
||||
return successCallback({
|
||||
audioStream: audioStream,
|
||||
videoStream: videoStream
|
||||
});
|
||||
},
|
||||
function (error) {
|
||||
console.error(
|
||||
'failed to obtain video stream - stop', error);
|
||||
self.errorCallback(error);
|
||||
},
|
||||
config.resolution || '360');
|
||||
};
|
||||
var obtainAudio = function () {
|
||||
self.getUserMediaWithConstraints(
|
||||
['audio'],
|
||||
function (audioStream) {
|
||||
if (newDevices.indexOf('video') !== -1)
|
||||
obtainVideo(audioStream);
|
||||
},
|
||||
function (error) {
|
||||
console.error(
|
||||
'failed to obtain audio stream - stop', error);
|
||||
self.errorCallback(error);
|
||||
}
|
||||
);
|
||||
};
|
||||
if (newDevices.indexOf('audio') !== -1) {
|
||||
obtainAudio();
|
||||
} else {
|
||||
obtainVideo(null);
|
||||
}
|
||||
} else {
|
||||
this.getUserMediaWithConstraints(
|
||||
newDevices,
|
||||
|
@ -358,12 +434,12 @@ RTCUtils.prototype.obtainAudioAndVideoPermissions =
|
|||
config.resolution || '360');
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
RTCUtils.prototype.successCallback = function (stream, usageOptions) {
|
||||
// If this is FF, the stream parameter is *not* a MediaStream object, it's
|
||||
// an object with two properties: audioStream, videoStream.
|
||||
if(stream && !navigator.mozGetUserMedia)
|
||||
// If this is FF or IE, the stream parameter is *not* a MediaStream object,
|
||||
// it's an object with two properties: audioStream, videoStream.
|
||||
if (stream && stream.getAudioTracks && stream.getVideoTracks)
|
||||
console.log('got', stream, stream.getAudioTracks().length,
|
||||
stream.getVideoTracks().length);
|
||||
this.handleLocalStream(stream, usageOptions);
|
||||
|
@ -427,10 +503,17 @@ RTCUtils.prototype.handleLocalStream = function(stream, usageOptions)
|
|||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{//firefox
|
||||
audioStream = stream.audioStream;
|
||||
videoStream = stream.videoStream;
|
||||
else if (RTCBrowserType.isFirefox() || RTCBrowserType.isTemasysPluginUsed())
|
||||
{ // Firefox and Temasys plugin
|
||||
if (stream && stream.audioStream)
|
||||
audioStream = stream.audioStream;
|
||||
else
|
||||
audioStream = new DummyMediaStream("dummyAudio");
|
||||
|
||||
if (stream && stream.videoStream)
|
||||
videoStream = stream.videoStream;
|
||||
else
|
||||
videoStream = new DummyMediaStream("dummyVideo");
|
||||
}
|
||||
|
||||
var audioMuted = (usageOptions && usageOptions.audio === false),
|
||||
|
@ -447,15 +530,20 @@ RTCUtils.prototype.handleLocalStream = function(stream, usageOptions)
|
|||
videoMuted, videoGUM);
|
||||
};
|
||||
|
||||
RTCUtils.prototype.createStream = function(stream, isVideo)
|
||||
{
|
||||
function DummyMediaStream(id) {
|
||||
this.id = id;
|
||||
this.label = id;
|
||||
this.stop = function() { };
|
||||
this.getAudioTracks = function() { return []; }
|
||||
this.getVideoTracks = function() { return []; }
|
||||
}
|
||||
|
||||
RTCUtils.prototype.createStream = function(stream, isVideo) {
|
||||
var newStream = null;
|
||||
if(window.webkitMediaStream)
|
||||
{
|
||||
if (window.webkitMediaStream) {
|
||||
newStream = new webkitMediaStream();
|
||||
if(newStream)
|
||||
{
|
||||
var tracks = (isVideo? stream.getVideoTracks() : stream.getAudioTracks());
|
||||
if (newStream) {
|
||||
var tracks = (isVideo ? stream.getVideoTracks() : stream.getAudioTracks());
|
||||
|
||||
for (i = 0; i < tracks.length; i++) {
|
||||
newStream.addTrack(tracks[i]);
|
||||
|
@ -463,8 +551,14 @@ RTCUtils.prototype.createStream = function(stream, isVideo)
|
|||
}
|
||||
|
||||
}
|
||||
else
|
||||
newStream = stream;
|
||||
else {
|
||||
// FIXME: this is duplicated with 'handleLocalStream' !!!
|
||||
if (stream) {
|
||||
newStream = stream;
|
||||
} else {
|
||||
newStream = new DummyMediaStream(isVideo ? "dummyVideo" : "dummyAudio");
|
||||
}
|
||||
}
|
||||
|
||||
return newStream;
|
||||
};
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -24,6 +24,7 @@ var CQEvents = require("../../service/connectionquality/CQEvents");
|
|||
var DesktopSharingEventTypes
|
||||
= require("../../service/desktopsharing/DesktopSharingEventTypes");
|
||||
var RTCEvents = require("../../service/RTC/RTCEvents");
|
||||
var RTCBrowserType = require("../RTC/RTCBrowserType");
|
||||
var StreamEventTypes = require("../../service/RTC/StreamEventTypes");
|
||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
||||
var MemberEvents = require("../../service/members/Events");
|
||||
|
@ -253,7 +254,7 @@ function registerListeners() {
|
|||
{
|
||||
// might need to update the direction if participant just went from sendrecv to recvonly
|
||||
if (stream.type === 'video' || stream.type === 'screen') {
|
||||
var el = $('#participant_' + Strophe.getResourceFromJid(jid) + '>video');
|
||||
var el = $('#participant_' + Strophe.getResourceFromJid(jid) + '>' + APP.RTC.getVideoElementName());
|
||||
switch (stream.direction) {
|
||||
case 'sendrecv':
|
||||
el.show();
|
||||
|
@ -405,7 +406,9 @@ UI.start = function (init) {
|
|||
$('#notice').css({display: 'block'});
|
||||
}
|
||||
|
||||
document.getElementById('largeVideo').volume = 0;
|
||||
if (!RTCBrowserType.isIExplorer()) {
|
||||
document.getElementById('largeVideo').volume = 0;
|
||||
}
|
||||
|
||||
if(config.requireDisplayName) {
|
||||
var currentSettings = Settings.getSettings();
|
||||
|
|
|
@ -336,7 +336,10 @@ ConnectionIndicator.prototype.create = function () {
|
|||
*/
|
||||
ConnectionIndicator.prototype.remove = function()
|
||||
{
|
||||
this.connectionIndicatorContainer.remove();
|
||||
if (this.connectionIndicatorContainer.parentNode) {
|
||||
this.connectionIndicatorContainer.parentNode.removeChild(
|
||||
this.connectionIndicatorContainer);
|
||||
}
|
||||
this.popover.forceHide();
|
||||
|
||||
};
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
var Avatar = require("../avatar/Avatar");
|
||||
var RTCBrowserType = require("../../RTC/RTCBrowserType");
|
||||
var UIUtil = require("../util/UIUtil");
|
||||
var UIEvents = require("../../../service/UI/UIEvents");
|
||||
var xmpp = require("../../xmpp/xmpp");
|
||||
|
||||
var video = $('#largeVideo');
|
||||
// FIXME: With Temasys we have to re-select everytime
|
||||
//var video = $('#largeVideo');
|
||||
|
||||
var currentVideoWidth = null;
|
||||
var currentVideoHeight = null;
|
||||
|
@ -244,9 +246,7 @@ function changeVideo(isVisible) {
|
|||
}
|
||||
|
||||
if (isVisible) {
|
||||
// using "this" should be ok because we're called
|
||||
// from within the fadeOut event.
|
||||
$(this).fadeIn(300);
|
||||
$('#largeVideo').fadeIn(300);
|
||||
}
|
||||
|
||||
if(oldSmallVideo)
|
||||
|
@ -260,12 +260,16 @@ var LargeVideo = {
|
|||
this.eventEmitter = emitter;
|
||||
var self = this;
|
||||
// Listen for large video size updates
|
||||
document.getElementById('largeVideo')
|
||||
.addEventListener('loadedmetadata', function (e) {
|
||||
currentVideoWidth = this.videoWidth;
|
||||
currentVideoHeight = this.videoHeight;
|
||||
self.position(currentVideoWidth, currentVideoHeight);
|
||||
});
|
||||
var largeVideo = $('#largeVideo')[0];
|
||||
var onplaying = function (arg1, arg2, arg3) {
|
||||
// re-select
|
||||
if (RTCBrowserType.isTemasysPluginUsed())
|
||||
largeVideo = $('#largeVideo')[0];
|
||||
currentVideoWidth = largeVideo.videoWidth;
|
||||
currentVideoHeight = largeVideo.videoHeight;
|
||||
self.position(currentVideoWidth, currentVideoHeight);
|
||||
};
|
||||
largeVideo.onplaying = onplaying;
|
||||
},
|
||||
/**
|
||||
* Indicates if the large video is currently visible.
|
||||
|
@ -273,14 +277,14 @@ var LargeVideo = {
|
|||
* @return <tt>true</tt> if visible, <tt>false</tt> - otherwise
|
||||
*/
|
||||
isLargeVideoVisible: function() {
|
||||
return video.is(':visible');
|
||||
return $('#largeVideo').is(':visible');
|
||||
},
|
||||
/**
|
||||
* Updates the large video with the given new video source.
|
||||
*/
|
||||
updateLargeVideo: function(resourceJid, forceUpdate) {
|
||||
console.log('hover in', resourceJid);
|
||||
var newSmallVideo = this.VideoLayout.getSmallVideo(resourceJid);
|
||||
console.log('hover in ' + resourceJid + ', video: ', newSmallVideo);
|
||||
|
||||
if ((currentSmallVideo && currentSmallVideo.resourceJid !== resourceJid)
|
||||
|| forceUpdate) {
|
||||
|
@ -303,8 +307,8 @@ var LargeVideo = {
|
|||
this.eventEmitter.emit(UIEvents.SELECTED_ENDPOINT,
|
||||
resourceJid);
|
||||
}
|
||||
|
||||
video.fadeOut(300, changeVideo.bind(video, this.isLargeVideoVisible()));
|
||||
$('#largeVideo').fadeOut(300,
|
||||
changeVideo.bind($('#largeVideo'), this.isLargeVideoVisible()));
|
||||
} else {
|
||||
if(currentSmallVideo) {
|
||||
currentSmallVideo.showAvatar();
|
||||
|
|
|
@ -3,6 +3,7 @@ var ConnectionIndicator = require("./ConnectionIndicator");
|
|||
var NicknameHandler = require("../util/NicknameHandler");
|
||||
var UIUtil = require("../util/UIUtil");
|
||||
var LargeVideo = require("./LargeVideo");
|
||||
var RTCBrowserType = require("../../RTC/RTCBrowserType");
|
||||
|
||||
function LocalVideo(VideoLayout)
|
||||
{
|
||||
|
@ -163,13 +164,18 @@ LocalVideo.prototype.changeVideo = function (stream, isMuted) {
|
|||
var self = this;
|
||||
|
||||
function localVideoClick(event) {
|
||||
event.stopPropagation();
|
||||
// FIXME: with Temasys plugin event arg is not an event, but
|
||||
// the clicked object itself, so we have to skip this call
|
||||
if (event.stopPropagation) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
self.VideoLayout.handleVideoThumbClicked(
|
||||
false,
|
||||
APP.xmpp.myResource());
|
||||
}
|
||||
|
||||
$('#localVideoContainer').click(localVideoClick);
|
||||
$('#localVideoContainer').off('click');
|
||||
$('#localVideoContainer').on('click', localVideoClick);
|
||||
|
||||
// Add hover handler
|
||||
$('#localVideoContainer').hover(
|
||||
|
@ -192,8 +198,10 @@ LocalVideo.prototype.changeVideo = function (stream, isMuted) {
|
|||
var localVideo = document.createElement('video');
|
||||
localVideo.id = 'localVideo_' +
|
||||
APP.RTC.getStreamID(stream.getOriginalStream());
|
||||
localVideo.autoplay = true;
|
||||
localVideo.volume = 0; // is it required if audio is separated ?
|
||||
if (!RTCBrowserType.isIExplorer()) {
|
||||
localVideo.autoplay = true;
|
||||
localVideo.volume = 0; // is it required if audio is separated ?
|
||||
}
|
||||
localVideo.oncontextmenu = function () { return false; };
|
||||
|
||||
var localVideoContainer = document.getElementById('localVideoWrapper');
|
||||
|
@ -203,7 +211,9 @@ LocalVideo.prototype.changeVideo = function (stream, isMuted) {
|
|||
|
||||
// Add click handler to both video and video wrapper elements in case
|
||||
// there's no video.
|
||||
localVideoSelector.click(localVideoClick);
|
||||
|
||||
// onclick has to be used with Temasys plugin
|
||||
localVideo.onclick = localVideoClick;
|
||||
|
||||
if (this.flipX) {
|
||||
localVideoSelector.addClass("flipVideoX");
|
||||
|
@ -214,6 +224,9 @@ LocalVideo.prototype.changeVideo = function (stream, isMuted) {
|
|||
|
||||
// Add stream ended handler
|
||||
stream.getOriginalStream().onended = function () {
|
||||
// We have to re-select after attach when Temasys plugin is used,
|
||||
// because <video> element is replaced with <object>
|
||||
localVideo = $('#' + localVideo.id)[0];
|
||||
localVideoContainer.removeChild(localVideo);
|
||||
self.VideoLayout.updateRemovedVideo(APP.xmpp.myResource());
|
||||
};
|
||||
|
|
|
@ -3,6 +3,7 @@ var SmallVideo = require("./SmallVideo");
|
|||
var AudioLevels = require("../audio_levels/AudioLevels");
|
||||
var LargeVideo = require("./LargeVideo");
|
||||
var Avatar = require("../avatar/Avatar");
|
||||
var RTCBrowserType = require("../../RTC/RTCBrowserType");
|
||||
|
||||
function RemoteVideo(peerJid, VideoLayout)
|
||||
{
|
||||
|
@ -146,14 +147,15 @@ RemoteVideo.prototype.removeRemoteStreamElement = function (stream, isVideo, id)
|
|||
select.remove();
|
||||
|
||||
var audioCount = $('#' + this.videoSpanId + '>audio').length;
|
||||
var videoCount = $('#' + this.videoSpanId + '>video').length;
|
||||
var videoCount = $('#' + this.videoSpanId + '>' + APP.RTC.getVideoElementName()).length;
|
||||
|
||||
if (!audioCount && !videoCount) {
|
||||
console.log("Remove whole user", this.videoSpanId);
|
||||
if(this.connectionIndicator)
|
||||
this.connectionIndicator.remove();
|
||||
// Remove whole container
|
||||
this.container.remove();
|
||||
if (this.container.parentNode)
|
||||
this.container.parentNode.removeChild(this.container);
|
||||
|
||||
this.VideoLayout.resizeThumbnails();
|
||||
}
|
||||
|
@ -185,11 +187,21 @@ RemoteVideo.prototype.addRemoteStreamElement = function (sid, stream, thessrc) {
|
|||
if (isVideo && stream.id !== 'mixedmslabel') {
|
||||
var onPlayingHandler = function () {
|
||||
// FIXME: why do i have to do this for FF?
|
||||
APP.RTC.attachMediaStream(sel, stream);
|
||||
if (RTCBrowserType.isFirefox()) {
|
||||
APP.RTC.attachMediaStream(sel, stream);
|
||||
}
|
||||
if (RTCBrowserType.isTemasysPluginUsed()) {
|
||||
sel = $('#' + newElementId);
|
||||
}
|
||||
self.VideoLayout.videoactive(sel, self.resourceJid);
|
||||
sel.off("playing", onPlayingHandler);
|
||||
sel[0].onplaying = null;
|
||||
if (RTCBrowserType.isTemasysPluginUsed()) {
|
||||
// 'currentTime' is used to check if the video has started
|
||||
// and the value is not set by the plugin, so we do it
|
||||
sel[0].currentTime = 1;
|
||||
}
|
||||
};
|
||||
sel.on("playing", onPlayingHandler);
|
||||
sel[0].onplaying = onPlayingHandler;
|
||||
}
|
||||
|
||||
APP.RTC.attachMediaStream(sel, stream);
|
||||
|
@ -203,23 +215,35 @@ RemoteVideo.prototype.addRemoteStreamElement = function (sid, stream, thessrc) {
|
|||
|
||||
};
|
||||
|
||||
// Name of video element name is different for IE/Safari
|
||||
var videoElem = APP.RTC.getVideoElementName();
|
||||
|
||||
// Add click handler.
|
||||
this.container.onclick = function (event) {
|
||||
var onClickHandler = function (event) {
|
||||
/*
|
||||
* FIXME It turns out that videoThumb may not exist (if there is
|
||||
* no actual video).
|
||||
*/
|
||||
var videoThumb = $('#' + self.videoSpanId + '>video').get(0);
|
||||
var videoThumb = $('#' + self.videoSpanId + '>' + videoElem).get(0);
|
||||
if (videoThumb) {
|
||||
self.VideoLayout.handleVideoThumbClicked(
|
||||
false,
|
||||
self.resourceJid);
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
// On IE we need to populate this handler on video <object>
|
||||
// and it does not give event instance as an argument,
|
||||
// so we check here for methods.
|
||||
if (event.stopPropagation && event.preventDefault) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
this.container.onclick = onClickHandler;
|
||||
// reselect
|
||||
if (RTCBrowserType.isTemasysPluginUsed())
|
||||
sel = $('#' + newElementId);
|
||||
sel[0].onclick = onClickHandler;
|
||||
|
||||
//FIXME
|
||||
// Add hover handler
|
||||
|
@ -229,9 +253,9 @@ RemoteVideo.prototype.addRemoteStreamElement = function (sid, stream, thessrc) {
|
|||
},
|
||||
function() {
|
||||
var videoSrc = null;
|
||||
if ($('#' + self.videoSpanId + '>video')
|
||||
&& $('#' + self.videoSpanId + '>video').length > 0) {
|
||||
videoSrc = APP.RTC.getVideoSrc($('#' + self.videoSpanId + '>video').get(0));
|
||||
var videoSelector = $('#' + self.videoSpanId + '>' + videoElem);
|
||||
if (videoSelector && videoSelector.length > 0) {
|
||||
videoSrc = APP.RTC.getVideoSrc(videoSelector.get(0));
|
||||
}
|
||||
|
||||
// If the video has been "pinned" by the user we want to
|
||||
|
@ -241,7 +265,7 @@ RemoteVideo.prototype.addRemoteStreamElement = function (sid, stream, thessrc) {
|
|||
self.showDisplayName(false);
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Show/hide peer container for the given resourceJid.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
var UIUtil = require("../util/UIUtil");
|
||||
var LargeVideo = require("./LargeVideo");
|
||||
var RTCBrowserType = require("../../RTC/RTCBrowserType");
|
||||
|
||||
function SmallVideo(){
|
||||
this.isMuted = false;
|
||||
|
@ -96,7 +97,9 @@ SmallVideo.createStreamElement = function (sid, stream) {
|
|||
+ sid + '_' + APP.RTC.getStreamID(stream);
|
||||
|
||||
element.id = id;
|
||||
element.autoplay = true;
|
||||
if (!RTCBrowserType.isIExplorer()) {
|
||||
element.autoplay = true;
|
||||
}
|
||||
element.oncontextmenu = function () { return false; };
|
||||
|
||||
return element;
|
||||
|
@ -204,7 +207,7 @@ SmallVideo.prototype.enableDominantSpeaker = function (isEnable)
|
|||
return;
|
||||
}
|
||||
|
||||
var video = $('#' + this.videoSpanId + '>video');
|
||||
var video = $('#' + this.videoSpanId + '>' + APP.RTC.getVideoElementName());
|
||||
|
||||
if (video && video.length > 0) {
|
||||
if (isEnable) {
|
||||
|
@ -275,8 +278,10 @@ SmallVideo.prototype.createModeratorIndicatorElement = function () {
|
|||
|
||||
|
||||
SmallVideo.prototype.getSrc = function () {
|
||||
return APP.RTC.getVideoSrc($('#' + this.videoSpanId).find("video").get(0));
|
||||
}
|
||||
var videoElement = APP.RTC.getVideoElementName();
|
||||
return APP.RTC.getVideoSrc(
|
||||
$('#' + this.videoSpanId).find(videoElement).get(0));
|
||||
},
|
||||
|
||||
SmallVideo.prototype.focus = function(isFocused)
|
||||
{
|
||||
|
@ -290,7 +295,8 @@ SmallVideo.prototype.focus = function(isFocused)
|
|||
}
|
||||
|
||||
SmallVideo.prototype.hasVideo = function () {
|
||||
return $("#" + this.videoSpanId).find("video").length !== 0;
|
||||
return $("#" + this.videoSpanId).find(
|
||||
APP.RTC.getVideoElementName()).length !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -302,7 +308,8 @@ SmallVideo.prototype.showAvatar = function (show) {
|
|||
if(!this.hasAvatar)
|
||||
return;
|
||||
|
||||
var video = $('#' + this.videoSpanId).find("video");
|
||||
var videoElem = APP.RTC.getVideoElementName();
|
||||
var video = $('#' + this.videoSpanId).find(videoElem);
|
||||
var avatar = $('#avatar_' + this.resourceJid);
|
||||
|
||||
if (show === undefined || show === null) {
|
||||
|
|
|
@ -3,6 +3,10 @@ var Avatar = require("../avatar/Avatar");
|
|||
var ContactList = require("../side_pannels/contactlist/ContactList");
|
||||
var MediaStreamType = require("../../../service/RTC/MediaStreamTypes");
|
||||
var UIEvents = require("../../../service/UI/UIEvents");
|
||||
|
||||
var RTC = require("../../RTC/RTC");
|
||||
var RTCBrowserType = require('../../RTC/RTCBrowserType');
|
||||
|
||||
var RemoteVideo = require("./RemoteVideo");
|
||||
var LargeVideo = require("./LargeVideo");
|
||||
var LocalVideo = require("./LocalVideo");
|
||||
|
@ -48,11 +52,15 @@ var VideoLayout = (function (my) {
|
|||
};
|
||||
|
||||
my.changeLocalAudio = function(stream, isMuted) {
|
||||
if(isMuted)
|
||||
if (isMuted)
|
||||
APP.UI.setAudioMuted(true, true);
|
||||
APP.RTC.attachMediaStream($('#localAudio'), stream.getOriginalStream());
|
||||
document.getElementById('localAudio').autoplay = true;
|
||||
document.getElementById('localAudio').volume = 0;
|
||||
var localAudio = document.getElementById('localAudio');
|
||||
// Writing volume not allowed in IE
|
||||
if (!RTCBrowserType.isIExplorer()) {
|
||||
localAudio.autoplay = true;
|
||||
localAudio.volume = 0;
|
||||
}
|
||||
};
|
||||
|
||||
my.changeLocalVideo = function(stream, isMuted) {
|
||||
|
@ -107,22 +115,26 @@ var VideoLayout = (function (my) {
|
|||
* @param removedVideoSrc src stream identifier of the video.
|
||||
*/
|
||||
my.updateRemovedVideo = function(resourceJid) {
|
||||
|
||||
var videoElem = RTC.getVideoElementName();
|
||||
|
||||
if (resourceJid === LargeVideo.getResourceJid()) {
|
||||
// this is currently displayed as large
|
||||
// pick the last visible video in the row
|
||||
// if nobody else is left, this picks the local video
|
||||
var pick
|
||||
= $('#remoteVideos>span[id!="mixedstream"]:visible:last>video')
|
||||
.get(0);
|
||||
= $('#remoteVideos>' +
|
||||
'span[id!="mixedstream"]:visible:last>' + videoElem).get(0);
|
||||
|
||||
if (!pick) {
|
||||
console.info("Last visible video no longer exists");
|
||||
pick = $('#remoteVideos>span[id!="mixedstream"]>video').get(0);
|
||||
pick = $('#remoteVideos>' +
|
||||
'span[id!="mixedstream"]>' + videoElem).get(0);
|
||||
|
||||
if (!pick || !APP.RTC.getVideoSrc(pick)) {
|
||||
// Try local video
|
||||
console.info("Fallback to local video...");
|
||||
pick = $('#remoteVideos>span>span>video').get(0);
|
||||
pick = $('#remoteVideos>span>span>' + videoElem).get(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -223,10 +235,13 @@ var VideoLayout = (function (my) {
|
|||
|
||||
LargeVideo.updateLargeVideo(resourceJid);
|
||||
|
||||
$('audio').each(function (idx, el) {
|
||||
el.volume = 0;
|
||||
el.volume = 1;
|
||||
});
|
||||
// Writing volume not allowed in IE
|
||||
if (!RTCBrowserType.isIExplorer()) {
|
||||
$('audio').each(function (idx, el) {
|
||||
el.volume = 0;
|
||||
el.volume = 1;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
@ -452,7 +467,8 @@ var VideoLayout = (function (my) {
|
|||
var resource = Strophe.getResourceFromJid(jid);
|
||||
var videoContainer = $("#participant_" + resource);
|
||||
if (videoContainer.length > 0) {
|
||||
var videoThumb = $('video', videoContainer).get(0);
|
||||
var videoThumb
|
||||
= $(RTC.getVideoElementName(), videoContainer).get(0);
|
||||
// It is not always the case that a videoThumb exists (if there is
|
||||
// no actual video).
|
||||
if (videoThumb) {
|
||||
|
@ -571,7 +587,8 @@ var VideoLayout = (function (my) {
|
|||
// since we don't want to switch to local video.
|
||||
if (container && !focusedVideoResourceJid)
|
||||
{
|
||||
var video = container.getElementsByTagName("video");
|
||||
var video
|
||||
= container.getElementsByTagName(RTC.getVideoElementName());
|
||||
|
||||
// Update the large video if the video source is already available,
|
||||
// otherwise wait for the "videoactive.jingle" event.
|
||||
|
@ -673,7 +690,8 @@ var VideoLayout = (function (my) {
|
|||
|
||||
var jid = APP.xmpp.findJidFromResource(resourceJid);
|
||||
var mediaStream = APP.RTC.remoteStreams[jid][MediaStreamType.VIDEO_TYPE];
|
||||
var sel = $('#participant_' + resourceJid + '>video');
|
||||
var sel = $('#participant_' + resourceJid +
|
||||
'>' + RTC.getVideoElementName());
|
||||
|
||||
APP.RTC.attachMediaStream(sel, mediaStream.stream);
|
||||
if (lastNPickupJid == mediaStream.peerjid) {
|
||||
|
|
|
@ -31,13 +31,7 @@ var extInstalled = false;
|
|||
*/
|
||||
var extUpdateRequired = false;
|
||||
|
||||
/**
|
||||
* Flag used to cache desktop sharing enabled state. Do not use directly as
|
||||
* it can be <tt>null</tt>.
|
||||
*
|
||||
* @type {null|boolean}
|
||||
*/
|
||||
var _desktopSharingEnabled = null;
|
||||
var AdapterJS = require("../RTC/adapter.screenshare");
|
||||
|
||||
var EventEmitter = require("events");
|
||||
|
||||
|
@ -46,6 +40,10 @@ var eventEmitter = new EventEmitter();
|
|||
var DesktopSharingEventTypes
|
||||
= require("../../service/desktopsharing/DesktopSharingEventTypes");
|
||||
|
||||
var RTCBrowserType = require("../RTC/RTCBrowserType");
|
||||
|
||||
var RTCEvents = require("../../service/RTC/RTCEvents");
|
||||
|
||||
/**
|
||||
* Method obtains desktop stream from WebRTC 'screen' source.
|
||||
* Flag 'chrome://flags/#enable-usermedia-screen-capture' must be enabled.
|
||||
|
@ -116,7 +114,7 @@ function isUpdateRequired(minVersion, extVersion)
|
|||
}
|
||||
}
|
||||
|
||||
function checkExtInstalled(callback) {
|
||||
function checkChromeExtInstalled(callback) {
|
||||
if (!chrome.runtime) {
|
||||
// No API, so no extension for sure
|
||||
callback(false, false);
|
||||
|
@ -213,20 +211,39 @@ function obtainScreenFromExtension(streamCallback, failCallback) {
|
|||
* feature completely.
|
||||
*/
|
||||
function setDesktopSharing(method) {
|
||||
// Check if we are running chrome
|
||||
if (!navigator.webkitGetUserMedia) {
|
||||
obtainDesktopStream = null;
|
||||
console.info("Desktop sharing disabled");
|
||||
} else if (method == "ext") {
|
||||
obtainDesktopStream = obtainScreenFromExtension;
|
||||
console.info("Using Chrome extension for desktop sharing");
|
||||
} else if (method == "webrtc") {
|
||||
obtainDesktopStream = obtainWebRTCScreen;
|
||||
console.info("Using Chrome WebRTC for desktop sharing");
|
||||
|
||||
obtainDesktopStream = null;
|
||||
|
||||
// When TemasysWebRTC plugin is used we always use getUserMedia, so we don't
|
||||
// care about 'method' parameter
|
||||
if (RTCBrowserType.isTemasysPluginUsed()) {
|
||||
if (!AdapterJS.WebRTCPlugin.plugin.HasScreensharingFeature) {
|
||||
console.info("Screensharing not supported by this plugin version");
|
||||
} else if (!AdapterJS.WebRTCPlugin.plugin.isScreensharingAvailable) {
|
||||
console.info(
|
||||
"Screensharing not available with Temasys plugin on this site");
|
||||
} else {
|
||||
obtainDesktopStream = obtainWebRTCScreen;
|
||||
console.info("Using Temasys plugin for desktop sharing");
|
||||
}
|
||||
} else if (RTCBrowserType.isChrome()) {
|
||||
if (method == "ext") {
|
||||
if (RTCBrowserType.getChromeVersion() >= 34) {
|
||||
obtainDesktopStream = obtainScreenFromExtension;
|
||||
console.info("Using Chrome extension for desktop sharing");
|
||||
initChromeExtension();
|
||||
} else {
|
||||
console.info("Chrome extension not supported until ver 34");
|
||||
}
|
||||
} else if (method == "webrtc") {
|
||||
obtainDesktopStream = obtainWebRTCScreen;
|
||||
console.info("Using Chrome WebRTC for desktop sharing");
|
||||
}
|
||||
}
|
||||
|
||||
// Reset enabled cache
|
||||
_desktopSharingEnabled = null;
|
||||
if (!obtainDesktopStream) {
|
||||
console.info("Desktop sharing disabled");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -239,6 +256,19 @@ function initInlineInstalls()
|
|||
$("link[rel=chrome-webstore-item]").attr("href", getWebStoreInstallUrl());
|
||||
}
|
||||
|
||||
function initChromeExtension() {
|
||||
// Initialize Chrome extension inline installs
|
||||
initInlineInstalls();
|
||||
// Check if extension is installed
|
||||
checkChromeExtInstalled(function (installed, updateRequired) {
|
||||
extInstalled = installed;
|
||||
extUpdateRequired = updateRequired;
|
||||
console.info(
|
||||
"Chrome extension installed: " + extInstalled +
|
||||
" updateRequired: " + extUpdateRequired);
|
||||
});
|
||||
}
|
||||
|
||||
function getVideoStreamFailed(error) {
|
||||
console.error("Failed to obtain the stream to switch to", error);
|
||||
switchInProgress = false;
|
||||
|
@ -264,6 +294,25 @@ function newStreamCreated(stream)
|
|||
stream, isUsingScreenStream, streamSwitchDone);
|
||||
}
|
||||
|
||||
function onEndedHandler(stream) {
|
||||
if (!switchInProgress && isUsingScreenStream) {
|
||||
APP.desktopsharing.toggleScreenSharing();
|
||||
}
|
||||
//FIXME: to be verified
|
||||
if (stream.removeEventListener) {
|
||||
stream.removeEventListener('ended', onEndedHandler);
|
||||
} else {
|
||||
stream.detachEvent('ended', onEndedHandler);
|
||||
}
|
||||
}
|
||||
|
||||
// Called when RTC finishes initialization
|
||||
function onWebRtcReady() {
|
||||
|
||||
setDesktopSharing(config.desktopSharing);
|
||||
|
||||
eventEmitter.emit(DesktopSharingEventTypes.INIT);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isUsingScreenStream: function () {
|
||||
|
@ -274,43 +323,10 @@ module.exports = {
|
|||
* @returns {boolean} <tt>true</tt> if desktop sharing feature is available
|
||||
* and enabled.
|
||||
*/
|
||||
isDesktopSharingEnabled: function () {
|
||||
if (_desktopSharingEnabled === null) {
|
||||
if (obtainDesktopStream === obtainScreenFromExtension) {
|
||||
// Parse chrome version
|
||||
var userAgent = navigator.userAgent.toLowerCase();
|
||||
// We can assume that user agent is chrome, because it's
|
||||
// enforced when 'ext' streaming method is set
|
||||
var ver = parseInt(userAgent.match(/chrome\/(\d+)\./)[1], 10);
|
||||
console.log("Chrome version" + userAgent, ver);
|
||||
_desktopSharingEnabled = ver >= 34;
|
||||
} else {
|
||||
_desktopSharingEnabled =
|
||||
obtainDesktopStream === obtainWebRTCScreen;
|
||||
}
|
||||
}
|
||||
return _desktopSharingEnabled;
|
||||
},
|
||||
isDesktopSharingEnabled: function () { return !!obtainDesktopStream; },
|
||||
|
||||
init: function () {
|
||||
setDesktopSharing(config.desktopSharing);
|
||||
|
||||
// Initialize Chrome extension inline installs
|
||||
if (config.chromeExtensionId) {
|
||||
|
||||
initInlineInstalls();
|
||||
|
||||
// Check if extension is installed
|
||||
checkExtInstalled(function (installed, updateRequired) {
|
||||
extInstalled = installed;
|
||||
extUpdateRequired = updateRequired;
|
||||
console.info(
|
||||
"Chrome extension installed: " + extInstalled +
|
||||
" updateRequired: " + extUpdateRequired);
|
||||
});
|
||||
}
|
||||
|
||||
eventEmitter.emit(DesktopSharingEventTypes.INIT);
|
||||
APP.RTC.addListener(RTCEvents.RTC_READY, onWebRtcReady);
|
||||
},
|
||||
|
||||
addListener: function (listener, type)
|
||||
|
@ -341,13 +357,16 @@ module.exports = {
|
|||
isUsingScreenStream = true;
|
||||
// Hook 'ended' event to restore camera
|
||||
// when screen stream stops
|
||||
stream.addEventListener('ended',
|
||||
function (e) {
|
||||
if (!switchInProgress && isUsingScreenStream) {
|
||||
APP.desktopsharing.toggleScreenSharing();
|
||||
}
|
||||
}
|
||||
);
|
||||
//FIXME: to be verified
|
||||
if (stream.addEventListener) {
|
||||
stream.addEventListener('ended', function () {
|
||||
onEndedHandler(stream);
|
||||
});
|
||||
} else {
|
||||
stream.attachEvent('ended', function () {
|
||||
onEndedHandler(stream);
|
||||
});
|
||||
}
|
||||
newStreamCreated(stream);
|
||||
},
|
||||
getDesktopStreamFailed);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* global ssrc2jid */
|
||||
/* jshint -W117 */
|
||||
var RTCBrowserType = require("../../service/RTC/RTCBrowserType");
|
||||
var RTCBrowserType = require("../RTC/RTCBrowserType");
|
||||
|
||||
|
||||
/**
|
||||
|
@ -17,10 +17,12 @@ function calculatePacketLoss(lostPackets, totalPackets) {
|
|||
}
|
||||
|
||||
function getStatValue(item, name) {
|
||||
if(!keyMap[APP.RTC.getBrowserType()][name])
|
||||
var browserType = RTCBrowserType.getBrowserType();
|
||||
if (!keyMap[browserType][name])
|
||||
throw "The property isn't supported!";
|
||||
var key = keyMap[APP.RTC.getBrowserType()][name];
|
||||
return APP.RTC.getBrowserType() == RTCBrowserType.RTC_BROWSER_CHROME? item.stat(key) : item[key];
|
||||
var key = keyMap[browserType][name];
|
||||
return (RTCBrowserType.isChrome() || RTCBrowserType.isOpera()) ?
|
||||
item.stat(key) : item[key];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -415,6 +417,8 @@ keyMap[RTCBrowserType.RTC_BROWSER_CHROME] = {
|
|||
"audioInputLevel": "audioInputLevel",
|
||||
"audioOutputLevel": "audioOutputLevel"
|
||||
};
|
||||
keyMap[RTCBrowserType.RTC_BROWSER_OPERA] =
|
||||
keyMap[RTCBrowserType.RTC_BROWSER_CHROME];
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,6 +6,7 @@ var SDP = require("./SDP");
|
|||
var async = require("async");
|
||||
var transform = require("sdp-transform");
|
||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
||||
var RTCBrowserType = require("../RTC/RTCBrowserType");
|
||||
|
||||
// Jingle stuff
|
||||
function JingleSession(me, sid, connection, service, eventEmitter) {
|
||||
|
@ -1332,9 +1333,10 @@ JingleSession.prototype.remoteStreamAdded = function (data, times) {
|
|||
var self = this;
|
||||
var thessrc;
|
||||
var ssrc2jid = this.connection.emuc.ssrc2jid;
|
||||
var streamId = APP.RTC.getStreamID(data.stream);
|
||||
|
||||
// look up an associated JID for a stream id
|
||||
if (data.stream.id && data.stream.id.indexOf('mixedmslabel') === -1) {
|
||||
if (streamId && streamId.indexOf('mixedmslabel') === -1) {
|
||||
// look only at a=ssrc: and _not_ at a=ssrc-group: lines
|
||||
|
||||
var ssrclines
|
||||
|
@ -1344,7 +1346,11 @@ JingleSession.prototype.remoteStreamAdded = function (data, times) {
|
|||
// is not always present.
|
||||
// return line.indexOf('mslabel:' + data.stream.label) !== -1;
|
||||
|
||||
return ((line.indexOf('msid:' + data.stream.id) !== -1));
|
||||
if (RTCBrowserType.isTemasysPluginUsed()) {
|
||||
return ((line.indexOf('mslabel:' + streamId) !== -1));
|
||||
} else {
|
||||
return ((line.indexOf('msid:' + streamId) !== -1));
|
||||
}
|
||||
});
|
||||
if (ssrclines.length) {
|
||||
thessrc = ssrclines[0].substring(7).split(' ')[0];
|
||||
|
@ -1379,7 +1385,7 @@ JingleSession.prototype.remoteStreamAdded = function (data, times) {
|
|||
}
|
||||
|
||||
// ok to overwrite the one from focus? might save work in colibri.js
|
||||
console.log('associated jid', ssrc2jid[thessrc], data.peerjid);
|
||||
console.log('associated jid', ssrc2jid[thessrc], thessrc);
|
||||
if (ssrc2jid[thessrc]) {
|
||||
data.peerjid = ssrc2jid[thessrc];
|
||||
}
|
||||
|
|
|
@ -218,8 +218,12 @@ SDP.prototype.toJingle = function (elem, thecreator, ssrcs) {
|
|||
if (kv.indexOf(':') == -1) {
|
||||
elem.attrs({ name: kv });
|
||||
} else {
|
||||
elem.attrs({ name: kv.split(':', 2)[0] });
|
||||
elem.attrs({ value: kv.split(':', 2)[1] });
|
||||
var k = kv.split(':', 2)[0];
|
||||
elem.attrs({ name: k });
|
||||
|
||||
var v = kv.split(':', 2)[1];
|
||||
v = SDPUtil.filter_special_chars(v);
|
||||
elem.attrs({ value: v });
|
||||
}
|
||||
elem.up();
|
||||
});
|
||||
|
@ -243,7 +247,7 @@ SDP.prototype.toJingle = function (elem, thecreator, ssrcs) {
|
|||
}
|
||||
if(msid != null)
|
||||
{
|
||||
msid = msid.replace(/[\{,\}]/g,"");
|
||||
msid = SDPUtil.filter_special_chars(msid);
|
||||
elem.c('parameter');
|
||||
elem.attrs({name: "msid", value:msid});
|
||||
elem.up();
|
||||
|
@ -605,9 +609,12 @@ SDP.prototype.jingle2media = function (content) {
|
|||
tmp.each(function () {
|
||||
var ssrc = this.getAttribute('ssrc');
|
||||
$(this).find('>parameter').each(function () {
|
||||
media += 'a=ssrc:' + ssrc + ' ' + this.getAttribute('name');
|
||||
if (this.getAttribute('value') && this.getAttribute('value').length)
|
||||
media += ':' + this.getAttribute('value');
|
||||
var name = this.getAttribute('name');
|
||||
var value = this.getAttribute('value');
|
||||
value = SDPUtil.filter_special_chars(value);
|
||||
media += 'a=ssrc:' + ssrc + ' ' + name;
|
||||
if (value && value.length)
|
||||
media += ':' + value;
|
||||
media += '\r\n';
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
|
||||
var SDPUtil = require("./SDPUtil");
|
||||
|
||||
function SDPDiffer(mySDP, otherSDP) {
|
||||
this.mySDP = mySDP;
|
||||
this.otherSDP = otherSDP;
|
||||
|
@ -130,8 +133,11 @@ SDPDiffer.prototype.toJingle = function(modify) {
|
|||
if (kv.indexOf(':') == -1) {
|
||||
modify.attrs({ name: kv });
|
||||
} else {
|
||||
modify.attrs({ name: kv.split(':', 2)[0] });
|
||||
modify.attrs({ value: kv.split(':', 2)[1] });
|
||||
var nv = kv.split(':', 2);
|
||||
var name = nv[0];
|
||||
var value = SDPUtil.filter_special_chars(nv[1]);
|
||||
modify.attrs({ name: name });
|
||||
modify.attrs({ value: value });
|
||||
}
|
||||
modify.up(); // end of parameter
|
||||
});
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
SDPUtil = {
|
||||
filter_special_chars: function (text) {
|
||||
return text.replace(/[\\\/\{,\}\+]/g, "");
|
||||
},
|
||||
iceparams: function (mediadesc, sessiondesc) {
|
||||
var data = null;
|
||||
if (SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc) &&
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
var RTC = require('../RTC/RTC');
|
||||
var RTCBrowserType = require("../RTC/RTCBrowserType.js");
|
||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
||||
|
||||
function TraceablePeerConnection(ice_config, constraints, session) {
|
||||
var self = this;
|
||||
var RTCPeerconnection = navigator.mozGetUserMedia ? mozRTCPeerConnection : webkitRTCPeerConnection;
|
||||
this.peerconnection = new RTCPeerconnection(ice_config, constraints);
|
||||
var RTCPeerconnectionType = null;
|
||||
if (RTCBrowserType.isFirefox()) {
|
||||
RTCPeerconnectionType = mozRTCPeerConnection;
|
||||
} else if (RTCBrowserType.isTemasysPluginUsed()) {
|
||||
RTCPeerconnectionType = RTCPeerConnection;
|
||||
} else {
|
||||
RTCPeerconnectionType = webkitRTCPeerConnection;
|
||||
}
|
||||
this.peerconnection = new RTCPeerconnectionType(ice_config, constraints);
|
||||
this.updateLog = [];
|
||||
this.stats = {};
|
||||
this.statsinterval = null;
|
||||
|
@ -15,7 +24,15 @@ function TraceablePeerConnection(ice_config, constraints, session) {
|
|||
|
||||
// override as desired
|
||||
this.trace = function (what, info) {
|
||||
//console.warn('WTRACE', what, info);
|
||||
/*console.warn('WTRACE', what, info);
|
||||
if (info && RTCBrowserType.isIExplorer()) {
|
||||
if (info.length > 1024) {
|
||||
console.warn('WTRACE', what, info.substr(1024));
|
||||
}
|
||||
if (info.length > 2048) {
|
||||
console.warn('WTRACE', what, info.substr(2048));
|
||||
}
|
||||
}*/
|
||||
self.updateLog.push({
|
||||
time: new Date(),
|
||||
type: what,
|
||||
|
@ -24,7 +41,9 @@ function TraceablePeerConnection(ice_config, constraints, session) {
|
|||
};
|
||||
this.onicecandidate = null;
|
||||
this.peerconnection.onicecandidate = function (event) {
|
||||
self.trace('onicecandidate', JSON.stringify(event.candidate, null, ' '));
|
||||
// FIXME: this causes stack overflow with Temasys Plugin
|
||||
if (!RTCBrowserType.isTemasysPluginUsed())
|
||||
self.trace('onicecandidate', JSON.stringify(event.candidate, null, ' '));
|
||||
if (self.onicecandidate !== null) {
|
||||
self.onicecandidate(event);
|
||||
}
|
||||
|
@ -155,16 +174,26 @@ TraceablePeerConnection.prototype.removeStream = function (stream, stopStreams)
|
|||
this.trace('removeStream', stream.id);
|
||||
if(stopStreams) {
|
||||
stream.getAudioTracks().forEach(function (track) {
|
||||
track.stop();
|
||||
// stop() not supported with IE
|
||||
if (track.stop) {
|
||||
track.stop();
|
||||
}
|
||||
});
|
||||
stream.getVideoTracks().forEach(function (track) {
|
||||
track.stop();
|
||||
// stop() not supported with IE
|
||||
if (track.stop) {
|
||||
track.stop();
|
||||
}
|
||||
});
|
||||
if (stream.stop) {
|
||||
stream.stop();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// FF doesn't support this yet.
|
||||
this.peerconnection.removeStream(stream);
|
||||
if (this.peerconnection.removeStream)
|
||||
this.peerconnection.removeStream(stream);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
|
|
@ -28,12 +28,11 @@ module.exports = function(XMPP, eventEmitter) {
|
|||
initPresenceMap: function (myroomjid) {
|
||||
this.presMap['to'] = myroomjid;
|
||||
this.presMap['xns'] = 'http://jabber.org/protocol/muc';
|
||||
if(APP.RTC.localAudio.isMuted())
|
||||
if (APP.RTC.localAudio && APP.RTC.localAudio.isMuted())
|
||||
{
|
||||
this.addAudioInfoToPresence(true);
|
||||
}
|
||||
|
||||
if(APP.RTC.localVideo.isMuted())
|
||||
if (APP.RTC.localVideo && APP.RTC.localVideo.isMuted())
|
||||
{
|
||||
this.addVideoInfoToPresence(true);
|
||||
}
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
var RTCBrowserType = {
|
||||
RTC_BROWSER_CHROME: "rtc_browser.chrome",
|
||||
|
||||
RTC_BROWSER_FIREFOX: "rtc_browser.firefox"
|
||||
};
|
||||
|
||||
module.exports = RTCBrowserType;
|
|
@ -1,4 +1,5 @@
|
|||
var RTCEvents = {
|
||||
RTC_READY: "rtc.ready",
|
||||
LASTN_CHANGED: "rtc.lastn_changed",
|
||||
DOMINANTSPEAKER_CHANGED: "rtc.dominantspeaker_changed",
|
||||
LASTN_ENDPOINT_CHANGED: "rtc.lastn_endpoint_changed",
|
||||
|
|
Loading…
Reference in New Issue