blob: fb146d9189c213a0895ac1ae64bf497b50a57b3d [file] [log] [blame]
/* Licensed under the Apache License, Version 2.0 (the "License") http://www.apache.org/licenses/LICENSE-2.0 */
const AudioCtx = window.AudioContext || window.webkitAudioContext;
const VideoMgrUtil = require('./video-manager-util');
const Sharer = require('./sharer');
const Volume = require('./volume');
module.exports = class Video {
constructor(msg) {
const states = [], self = this, vidSize = {};
let sd, v, vc, t, footer, size, vol, iceServers
, lm, level, userSpeaks = false, muteOthers
, hasVideo, isSharing, isRecording;
function __getVideo(_state) {
const vid = self.video(_state);
return vid && vid.length > 0 ? vid[0] : null;
}
function _resizeDlgArea(_w, _h) {
if (Room.getOptions().interview) {
VideoUtil.setPos(v, VideoUtil.getPos());
} else if (v.dialog('instance')) {
v.dialog('option', 'width', _w).dialog('option', 'height', _h);
}
}
function _micActivity(speaks) {
if (speaks !== userSpeaks) {
userSpeaks = speaks;
OmUtil.sendMessage({type: 'mic', id: 'activity', active: speaks});
}
}
function _getScreenStream(msg, state, callback) {
function __handleScreenError(err) {
VideoMgrUtil.sendMessage({id: 'errorSharing'});
Sharer.setShareState(Sharer.SHARE_STOPPED);
Sharer.setRecState(Sharer.SHARE_STOPPED);
OmUtil.error(err);
}
const b = OmUtil.browser;
let promise, cnts;
if (OmUtil.isEdge() && b.major > 16) {
cnts = {
video: true
};
promise = navigator.getDisplayMedia(cnts);
} else if (b.name === 'Firefox') {
// https://mozilla.github.io/webrtc-landing/gum_test.html
cnts = Sharer.baseConstraints(sd);
cnts.video.mediaSource = sd.shareType;
promise = navigator.mediaDevices.getUserMedia(cnts);
} else if (VideoUtil.sharingSupported()) {
cnts = {
video: true
};
promise = navigator.mediaDevices.getDisplayMedia(cnts);
} else {
promise = new Promise(() => {
Sharer.close();
throw 'Screen-sharing is not supported in ' + b.name + '[' + b.major + ']';
});
}
promise.then(stream => {
if (!state.disposed) {
__createVideo(state);
state.stream = stream;
callback(msg, state, cnts);
}
}).catch(__handleScreenError);
}
function _getVideoStream(msg, state, callback) {
VideoSettings.constraints(sd, function(cnts) {
if ((VideoUtil.hasCam(sd) && !cnts.video) || (VideoUtil.hasMic(sd) && !cnts.audio)) {
VideoMgrUtil.sendMessage({
id : 'devicesAltered'
, uid: sd.uid
, audio: !!cnts.audio
, video: !!cnts.video
});
}
if (!cnts.audio && !cnts.video) {
OmUtil.error('Requested devices are not available');
VideoMgrUtil.close(sd.uid)
return;
}
navigator.mediaDevices.getUserMedia(cnts)
.then(stream => {
if (state.disposed || msg.instanceUid !== v.data('instance-uid')) {
return;
}
state.localStream = stream;
let _stream = stream;
const data = {};
if (stream.getAudioTracks().length !== 0) {
lm = vc.find('.level-meter');
lm.show();
data.aCtx = new AudioCtx();
data.gainNode = data.aCtx.createGain();
data.analyser = data.aCtx.createAnalyser();
data.aSrc = data.aCtx.createMediaStreamSource(stream);
data.aSrc.connect(data.gainNode);
data.gainNode.connect(data.analyser);
if (OmUtil.isEdge()) {
data.analyser.connect(data.aCtx.destination);
} else {
data.aDest = data.aCtx.createMediaStreamDestination();
data.analyser.connect(data.aDest);
_stream = data.aDest.stream;
stream.getVideoTracks().forEach(function(track) {
_stream.addTrack(track);
});
}
}
state.data = data;
__createVideo(state);
state.stream = _stream;
callback(msg, state, cnts);
})
.catch(function(err) {
VideoMgrUtil.sendMessage({
id : 'devicesAltered'
, uid: sd.uid
, audio: false
, video: false
});
VideoMgrUtil.close(sd.uid);
if ('NotReadableError' === err.name) {
OmUtil.error('Camera/Microphone is busy and can\'t be used');
} else {
OmUtil.error(err);
}
});
});
}
function __attachListener(state) {
if (!state.disposed && state.data.rtcPeer) {
const pc = state.data.rtcPeer.peerConnection;
pc.onconnectionstatechange = function(event) {
console.warn(`!!RTCPeerConnection state changed: ${pc.connectionState}, user: ${sd.user.displayName}, uid: ${sd.uid}`);
switch(pc.connectionState) {
case "connected":
if (sd.self) {
// The connection has become fully connected
OmUtil.alert('info', `Connection to Media server has been established`, 3000);//notify user
}
break;
case "disconnected":
case "failed":
//connection has been dropped
OmUtil.alert('warning', `Media server connection for user ${sd.user.displayName} is ${pc.connectionState}, will try to re-connect`, 3000);//notify user
_refresh();
break;
case "closed":
// The connection has been closed
break;
}
}
}
}
function __createSendPeer(msg, state, cnts) {
state.options = {
videoStream: state.stream
, mediaConstraints: cnts
, onicecandidate: self.onIceCandidate
};
if (!isSharing) {
state.options.localVideo = __getVideo(state);
}
const data = state.data;
data.rtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(
VideoUtil.addIceServers(state.options, msg)
, function (error) {
if (state.disposed || true === data.rtcPeer.cleaned) {
return;
}
if (error) {
return OmUtil.error(error);
}
if (data.analyser) {
level = new MicLevel();
level.meter(data.analyser, lm, _micActivity, OmUtil.error);
}
data.rtcPeer.generateOffer(function(genErr, offerSdp) {
if (state.disposed || true === data.rtcPeer.cleaned) {
return;
}
if (genErr) {
return OmUtil.error('Sender sdp offer error ' + genErr);
}
OmUtil.log('Invoking Sender SDP offer callback function');
const bmsg = {
id : 'broadcastStarted'
, uid: sd.uid
, sdpOffer: offerSdp
}, vtracks = state.stream.getVideoTracks();
if (vtracks && vtracks.length > 0) {
const vts = vtracks[0].getSettings();
vidSize.width = vts.width;
vidSize.height = vts.height;
bmsg.width = vts.width;
bmsg.height = vts.height;
bmsg.fps = vts.frameRate;
}
VideoMgrUtil.sendMessage(bmsg);
if (isSharing) {
Sharer.setShareState(Sharer.SHARE_STARTED);
}
if (isRecording) {
Sharer.setRecState(Sharer.SHARE_STARTED);
}
});
});
data.rtcPeer.cleaned = false;
__attachListener(state);
}
function _createSendPeer(msg, state) {
if (isSharing || isRecording) {
_getScreenStream(msg, state, __createSendPeer);
} else {
_getVideoStream(msg, state, __createSendPeer);
}
}
function _createResvPeer(msg, state) {
__createVideo(state);
const options = VideoUtil.addIceServers({
remoteVideo : __getVideo(state)
, onicecandidate : self.onIceCandidate
}, msg);
const data = state.data;
data.rtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(
options
, function(error) {
if (state.disposed || true === data.rtcPeer.cleaned) {
return;
}
if (error) {
return OmUtil.error(error);
}
data.rtcPeer.generateOffer(function(genErr, offerSdp) {
if (state.disposed || true === data.rtcPeer.cleaned) {
return;
}
if (genErr) {
return OmUtil.error('Receiver sdp offer error ' + genErr);
}
OmUtil.log('Invoking Receiver SDP offer callback function');
VideoMgrUtil.sendMessage({
id : 'addListener'
, sender: sd.uid
, sdpOffer: offerSdp
});
});
});
data.rtcPeer.cleaned = false;
__attachListener(state);
}
function _handleMicStatus(state) {
if (!footer || !footer.is(':visible')) {
return;
}
if (state) {
footer.text(footer.data('on'));
footer.addClass('mic-on');
t.addClass('mic-on');
} else {
footer.text(footer.data('off'));
footer.removeClass('mic-on');
t.removeClass('mic-on');
}
}
function _initContainer(_id, name, opts, instanceUid) {
let contSel = '#user' + sd.cuid;
const _hasVideo = VideoUtil.hasVideo(sd)
if (_hasVideo) {
const s = VideoSettings.load();
if (s.fixed.enabled && !opts.interview && !isSharing) {
size = {
width: s.fixed.width
, height: s.fixed.height
};
} else {
size = {
width: sd.width
, height: sd.height
};
}
} else {
size = {
width: 120
, height: 90
};
}
hasVideo = _hasVideo || $(contSel).length < 1;
if (hasVideo) {
if (opts.interview) {
const area = $('.pod-area');
const contId = uuidv4();
contSel = '#' + contId;
area.append($('<div class="pod"></div>').attr('id', contId));
WbArea.updateAreaClass();
} else {
contSel = '.room-block .room-container';
}
}
$(contSel).append(OmUtil.tmpl('#user-video', _id)
.attr('title', name)
.attr('data-client-uid', sd.cuid)
.attr('data-client-type', sd.type)
.attr('data-instance-uid', instanceUid)
.data(self));
v = $('#' + _id);
vc = v.find('.video');
muteOthers = vc.find('.mute-others');
_setRights();
return contSel;
}
function _initDialog(v, opts) {
if (opts.interview) {
v.dialog('option', 'draggable', false);
v.dialog('option', 'resizable', false);
$('.pod-area').sortable('refresh');
} else {
v.dialog('option', 'draggable', true);
v.dialog('option', 'resizable', true);
if (isSharing) {
v.on('dialogclose', function() {
VideoMgrUtil.close(sd.uid, true);
});
}
}
_initDialogBtns(opts);
}
function _initDialogBtns(opts) {
function noDblClick(e) {
e.dblclick(function(e) {
e.stopImmediatePropagation();
return false;
});
}
v.parent().find('.ui-dialog-titlebar-close').remove();
v.parent().append(OmUtil.tmpl('#video-button-bar'));
const refresh = v.parent().find('.btn-refresh')
, tgl = v.parent().find('.btn-toggle')
, cls = v.parent().find('.btn-wclose');
if (isSharing) {
cls.click(function (_) {
v.dialog('close');
return false;
});
noDblClick(cls);
refresh.remove();
} else {
cls.remove();
refresh.click(function(e) {
e.stopImmediatePropagation();
_refresh();
return false;
});
}
if (opts.interview) {
tgl.remove();
} else {
tgl.click(function (e) {
e.stopImmediatePropagation();
$(this).toggleClass('minimized');
v.toggle();
return false;
});
noDblClick(tgl);
}
}
function _update(_c) {
const prevA = sd.activities
, prevW = vidSize.width || sd.width // try to check actual size of video first
, prevH = vidSize.height || sd.height // try to check actual size of video first
, prevCam = sd.cam
, prevMic = sd.mic;
sd.activities = _c.activities.sort();
sd.level = _c.level;
sd.user.firstName = _c.user.firstName;
sd.user.lastName = _c.user.lastName;
sd.user.displayName = _c.user.displayName;
sd.width = _c.width;
sd.height = _c.height;
sd.cam = _c.cam;
sd.mic = _c.mic;
const name = sd.user.displayName;
if (hasVideo) {
v.dialog('option', 'title', name).parent().find('.ui-dialog-titlebar').attr('title', name);
}
const same = prevA.length === sd.activities.length
&& prevA.every(function(value, index) { return value === sd.activities[index]})
&& prevW === sd.width && prevH === sd.height
&& prevCam == sd.cam && prevMic === sd.mic;
if (sd.self && !same) {
_cleanup();
v.remove();
_init({stream: sd, iceServers: iceServers});
}
}
function __createVideo(state) {
const _id = VideoUtil.getVid(sd.uid);
_resizeDlgArea(size.width, size.height);
if (hasVideo && !isSharing && !isRecording) {
VideoUtil.setPos(v, VideoUtil.getPos(VideoUtil.getRects(VIDWIN_SEL, _id), sd.width, sd.height + 25));
}
state.video = $(hasVideo ? '<video>' : '<audio>').attr('id', 'vid' + _id)
.attr('playsinline', 'playsinline')
//.attr('autoplay', 'autoplay')
.prop('controls', false)
.width(vc.width()).height(vc.height());
if (state.data) {
state.video.data(state.data);
}
if (hasVideo) {
vc.removeClass('audio-only').css('background-image', '');
vc.parents('.ui-dialog').removeClass('audio-only');
state.video.attr('poster', sd.user.pictureUri);
} else {
vc.addClass('audio-only');
}
vc.append(state.video);
if (VideoUtil.hasMic(sd)) {
const volIco = vol.create(self)
if (hasVideo) {
v.parent().find('.buttonpane').append(volIco);
} else {
volIco.addClass('ulist-small');
volIco.insertAfter('#user' + sd.cuid + ' .typing-activity');
}
} else {
vol.destroy();
}
}
function _refresh(_msg) {
const msg = _msg || {
iceServers: iceServers
, instanceUid: v.length > 0 ? v.data('instance-uid') : undefined
};
if (sd.self) {
VideoMgrUtil.sendMessage({
id : 'broadcastRestarted'
, uid: sd.uid
});
}
_cleanup();
const hasAudio = VideoUtil.hasMic(sd)
, state = {
disposed: false
, data: {}
};
states.push(state);
if (sd.self) {
_createSendPeer(msg, state);
} else {
_createResvPeer(msg, state);
}
_handleMicStatus(hasAudio);
}
function _setRights() {
if (Room.hasRight(['MUTE_OTHERS']) && VideoUtil.hasMic(sd)) {
muteOthers.addClass('enabled').off().click(function() {
VideoMgrUtil.clickMuteOthers(sd.cuid);
});
} else {
muteOthers.removeClass('enabled').off();
}
}
function _cleanData(data) {
if (!data) {
return;
}
if (data.analyser) {
VideoUtil.disconnect(data.analyser);
data.analyser = null;
}
if (data.gainNode) {
VideoUtil.disconnect(data.gainNode);
data.gainNode = null;
}
if (data.aSrc) {
VideoUtil.cleanStream(data.aSrc.mediaStream);
VideoUtil.disconnect(data.aSrc);
data.aSrc = null;
}
if (data.aDest) {
VideoUtil.disconnect(data.aDest);
data.aDest = null;
}
if (data.aCtx) {
if (data.aCtx.destination) {
VideoUtil.disconnect(data.aCtx.destination);
}
if ('closed' !== data.aCtx.state) {
try {
data.aCtx.close();
} catch(e) {
console.error(e);
}
}
data.aCtx = null;
}
VideoUtil.cleanPeer(data.rtcPeer);
data.rtcPeer = null;
}
function _cleanup(evt) {
delete vidSize.width;
delete vidSize.height;
OmUtil.log('!!Disposing participant ' + sd.uid);
let state;
while(state = states.pop()) {
state.disposed = true;
if (state.options) {
delete state.options.videoStream;
delete state.options.mediaConstraints;
delete state.options.onicecandidate;
delete state.options.localVideo;
state.options = null;
}
_cleanData(state.data);
VideoUtil.cleanStream(state.localStream);
VideoUtil.cleanStream(state.stream);
state.data = null;
state.localStream = null;
state.stream = null;
const video = state.video;
if (video && video.length > 0) {
video.attr('id', 'dummy');
const vidNode = video[0];
VideoUtil.cleanStream(vidNode.srcObject);
vidNode.srcObject = null;
vidNode.load();
vidNode.removeAttribute("src");
vidNode.removeAttribute("srcObject");
vidNode.parentNode.removeChild(vidNode);
state.video.data({});
state.video = null;
}
}
if (lm && lm.length > 0) {
_micActivity(false);
lm.hide();
muteOthers.removeClass('enabled').off();
}
if (level) {
level.dispose();
level = null;
}
vc.find('audio,video').remove();
vol.destroy();
if (evt && evt.target) {
$(evt).off();
}
}
function _reattachStream() {
states.forEach(state => {
if (state.video && state.video.length > 0) {
const data = state.data
, videoEl = state.video[0];
if (data.rtcPeer && (!videoEl.srcObject || !videoEl.srcObject.active)) {
videoEl.srcObject = sd.self ? data.rtcPeer.getLocalStream() : data.rtcPeer.getRemoteStream();
}
}
});
}
function _processSdpAnswer(answer) {
const state = states.length > 0 ? states[0] : null;
if (!state || state.disposed || !state.data.rtcPeer || state.data.rtcPeer.cleaned) {
return;
}
state.data.rtcPeer.processAnswer(answer, function (error) {
if (true === this.cleaned) {
return;
}
const video = __getVideo(state);
if (this.peerConnection.signalingState === 'stable' && video && video.paused) {
video.play().catch(function (err) {
if ('NotAllowedError' === err.name) {
VideoUtil.askPermission(function () {
video.play();
});
}
});
return;
}
if (error) {
OmUtil.error(error, true);
}
});
}
function _processIceCandidate(candidate) {
const state = states.length > 0 ? states[0] : null;
if (!state || state.disposed || !state.data.rtcPeer || state.data.rtcPeer.cleaned) {
return;
}
state.data.rtcPeer.addIceCandidate(candidate, function (error) {
if (true === this.cleaned) {
return;
}
if (error) {
OmUtil.error('Error adding candidate: ' + error, true);
}
});
}
function _init(_msg) {
sd = _msg.stream;
_msg.instanceUid = uuidv4();
if (!vol) {
vol = new Volume();
}
iceServers = _msg.iceServers;
sd.activities = sd.activities.sort();
isSharing = VideoUtil.isSharing(sd);
isRecording = VideoUtil.isRecording(sd);
const _id = VideoUtil.getVid(sd.uid)
, name = sd.user.displayName
, _w = sd.width
, _h = sd.height
, opts = Room.getOptions();
sd.self = sd.cuid === opts.uid;
const contSel = _initContainer(_id, name, opts, _msg.instanceUid);
footer = v.find('.footer');
if (!opts.showMicStatus) {
footer.hide();
}
if (!sd.self && isSharing) {
Sharer.close();
}
if (sd.self && (isSharing || isRecording)) {
v.hide();
} else if (hasVideo) {
v.dialog({
classes: {
'ui-dialog': 'video user-video' + (opts.showMicStatus ? ' mic-status' : '')
, 'ui-dialog-titlebar': '' + (opts.showMicStatus ? ' ui-state-highlight' : '')
}
, width: _w
, minWidth: 40
, minHeight: 50
, autoOpen: true
, modal: false
, appendTo: contSel
});
_initDialog(v, opts);
}
t = v.parent().find('.ui-dialog-titlebar').attr('title', name);
v.on('remove', _cleanup);
if (hasVideo) {
vc.width(_w).height(_h);
}
_refresh(_msg);
}
this.update = _update;
this.refresh = _refresh;
this.mute = function(_mute) {
vol.mute(_mute);
};
this.isMuted = function() {
return vol.muted();
};
this.stream = function() { return sd; };
this.setRights = _setRights;
this.processSdpAnswer = _processSdpAnswer;
this.processIceCandidate = _processIceCandidate;
this.onIceCandidate = function(candidate) {
const opts = Room.getOptions();
OmUtil.log('Local candidate ' + JSON.stringify(candidate));
VideoMgrUtil.sendMessage({
id: 'onIceCandidate'
, candidate: candidate
, uid: sd.uid
, luid: sd.self ? sd.uid : opts.uid
});
};
this.reattachStream = _reattachStream;
this.video = function(_state) {
const state = _state || (states.length > 0 ? states[0] : null);
if (!state || state.disposed) {
return null;
}
return state.video;
};
this.handleMicStatus = _handleMicStatus;
_init(msg);
}
};