Merge pull request #646 from jitsi/flip

Implements custom context menu to flip the local video
This commit is contained in:
yanas 2016-05-08 10:50:35 -05:00
commit 626b37b4fe
11 changed files with 340 additions and 17 deletions

View File

@ -3,7 +3,7 @@ BROWSERIFY = ./node_modules/.bin/browserify
UGLIFYJS = ./node_modules/.bin/uglifyjs UGLIFYJS = ./node_modules/.bin/uglifyjs
EXORCIST = ./node_modules/.bin/exorcist EXORCIST = ./node_modules/.bin/exorcist
CLEANCSS = ./node_modules/.bin/cleancss CLEANCSS = ./node_modules/.bin/cleancss
CSS_FILES = font.css toastr.css main.css videolayout_default.css font-awesome.css jquery-impromptu.css modaldialog.css notice.css popup_menu.css login_menu.css popover.css jitsi_popover.css contact_list.css chat.css welcome_page.css settingsmenu.css feedback.css CSS_FILES = font.css toastr.css main.css videolayout_default.css font-awesome.css jquery-impromptu.css modaldialog.css notice.css popup_menu.css login_menu.css popover.css jitsi_popover.css contact_list.css chat.css welcome_page.css settingsmenu.css feedback.css jquery.contextMenu.css
DEPLOY_DIR = libs DEPLOY_DIR = libs
BROWSERIFY_FLAGS = -d BROWSERIFY_FLAGS = -d
OUTPUT_DIR = . OUTPUT_DIR = .

1
app.js
View File

@ -3,6 +3,7 @@
import "babel-polyfill"; import "babel-polyfill";
import "jquery"; import "jquery";
import "jquery-contextmenu";
import "jquery-ui"; import "jquery-ui";
import "strophe"; import "strophe";
import "strophe-disco"; import "strophe-disco";

206
css/jquery.contextMenu.css Normal file
View File

@ -0,0 +1,206 @@
@charset "UTF-8";
/*!
* jQuery contextMenu - Plugin for simple contextMenu handling
*
* Version: v2.1.1
*
* Authors: Björn Brala (SWIS.nl), Rodney Rehm, Addy Osmani (patches for FF)
* Web: http://swisnl.github.io/jQuery-contextMenu/
*
* Copyright (c) 2011-2016 SWIS BV and contributors
*
* Licensed under
* MIT License http://www.opensource.org/licenses/mit-license
*
* Date: 2016-02-28T09:53:18.890Z
*/
@font-face {
font-family: "context-menu-icons";
font-style: normal;
font-weight: normal;
src: url("font/context-menu-icons.eot?2qmzf");
src: url("font/context-menu-icons.eot?2qmzf#iefix") format("embedded-opentype"), url("font/context-menu-icons.woff2?2qmzf") format("woff2"), url("font/context-menu-icons.woff?2qmzf") format("woff"), url("font/context-menu-icons.ttf?2qmzf") format("truetype");
}
.context-menu-icon:before {
position: absolute;
top: 50%;
left: 0;
width: 28px;
font-family: "context-menu-icons";
font-size: 16px;
font-style: normal;
font-weight: normal;
line-height: 1;
color: #2980b9;
text-align: center;
-webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%);
-o-transform: translateY(-50%);
transform: translateY(-50%);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.context-menu-icon-add:before {
content: "";
}
.context-menu-icon-copy:before {
content: "";
}
.context-menu-icon-cut:before {
content: "";
}
.context-menu-icon-delete:before {
content: "";
}
.context-menu-icon-edit:before {
content: "";
}
.context-menu-icon-paste:before {
content: "";
}
.context-menu-icon-quit:before {
content: "";
}
.context-menu-icon.context-menu-hover:before {
color: #fff;
}
.context-menu-list {
position: absolute;
display: inline-block;
min-width: 180px;
max-width: 360px;
padding: 4px 0;
margin: 5px;
font-family: inherit;
font-size: inherit;
list-style-type: none;
background: #fff;
border: 1px solid #bebebe;
border-radius: 3px;
-webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, .5);
box-shadow: 0 2px 5px rgba(0, 0, 0, .5);
}
.context-menu-item {
position: relative;
padding: 3px 28px;
color: #2f2f2f;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-color: #fff;
}
.context-menu-separator {
padding: 0;
margin: 5px 0;
border-bottom: 1px solid #e6e6e6;
}
.context-menu-item > label > input,
.context-menu-item > label > textarea {
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
}
.context-menu-item.context-menu-hover {
color: #fff;
cursor: pointer;
background-color: #2980b9;
}
.context-menu-item.context-menu-disabled {
color: #626262;
background-color: #fff;
}
.context-menu-item.context-menu-disabled {
color: #626262;
}
.context-menu-input.context-menu-hover,
.context-menu-item.context-menu-disabled.context-menu-hover {
cursor: default;
background-color: #eee;
}
.context-menu-submenu:after {
position: absolute;
top: 50%;
right: 8px;
z-index: 1;
width: 0;
height: 0;
content: '';
border-color: transparent transparent transparent #2f2f2f;
border-style: solid;
border-width: 4px 0 4px 4px;
-webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%);
-o-transform: translateY(-50%);
transform: translateY(-50%);
}
/**
* Inputs
*/
.context-menu-item.context-menu-input {
padding: 5px 10px;
}
/* vertically align inside labels */
.context-menu-input > label > * {
vertical-align: top;
}
/* position checkboxes and radios as icons */
.context-menu-input > label > input[type="checkbox"],
.context-menu-input > label > input[type="radio"] {
position: relative;
top: 3px;
}
.context-menu-input > label,
.context-menu-input > label > input[type="text"],
.context-menu-input > label > textarea,
.context-menu-input > label > select {
display: block;
width: 100%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.context-menu-input > label > textarea {
height: 100px;
}
.context-menu-item > .context-menu-list {
top: 5px;
/* re-positioned by js */
right: -5px;
display: none;
}
.context-menu-item.context-menu-visible > .context-menu-list {
display: block;
}
.context-menu-accesskey {
text-decoration: underline;
}

View File

@ -99,7 +99,8 @@
"mute": "Participant is muted", "mute": "Participant is muted",
"kick": "Kick out", "kick": "Kick out",
"muted": "Muted", "muted": "Muted",
"domute": "Mute" "domute": "Mute",
"flip": "Flip"
}, },
"connectionindicator": "connectionindicator":

View File

@ -168,6 +168,7 @@ class VideoContainer extends LargeContainer {
super(); super();
this.stream = null; this.stream = null;
this.videoType = null; this.videoType = null;
this.localFlipX = true;
this.isVisible = false; this.isVisible = false;
@ -284,13 +285,25 @@ class VideoContainer extends LargeContainer {
} }
stream.attach(this.$video[0]); stream.attach(this.$video[0]);
let flipX = stream.isLocal() && this.localFlipX;
let flipX = stream.isLocal() && !this.isScreenSharing();
this.$video.css({ this.$video.css({
transform: flipX ? 'scaleX(-1)' : 'none' transform: flipX ? 'scaleX(-1)' : 'none'
}); });
} }
/**
* Changes the flipX state of the local video.
* @param val {boolean} true if flipped.
*/
setLocalFlipX(val) {
this.localFlipX = val;
if(!this.$video || !this.stream || !this.stream.isLocal())
return;
this.$video.css({
transform: this.localFlipX ? 'scaleX(-1)' : 'none'
});
}
/** /**
* Check if current video stream is screen sharing. * Check if current video stream is screen sharing.
* @returns {boolean} * @returns {boolean}
@ -453,7 +466,7 @@ export default class LargeVideoManager {
} else { } else {
preUpdate = Promise.resolve(); preUpdate = Promise.resolve();
} }
preUpdate.then(() => { preUpdate.then(() => {
let {id, stream, videoType, resolve} = this.newStreamData; let {id, stream, videoType, resolve} = this.newStreamData;
this.newStreamData = null; this.newStreamData = null;
@ -651,4 +664,12 @@ export default class LargeVideoManager {
} }
}); });
} }
/**
* Changes the flipX state of the local video.
* @param val {boolean} true if flipped.
*/
onLocalFlipXChange(val) {
this.videoContainer.setLocalFlipX(val);
}
} }

View File

@ -4,16 +4,15 @@ import UIUtil from "../util/UIUtil";
import UIEvents from "../../../service/UI/UIEvents"; import UIEvents from "../../../service/UI/UIEvents";
import SmallVideo from "./SmallVideo"; import SmallVideo from "./SmallVideo";
var LargeVideo = require("./LargeVideo");
const RTCUIUtils = JitsiMeetJS.util.RTCUIHelper; const RTCUIUtils = JitsiMeetJS.util.RTCUIHelper;
const TrackEvents = JitsiMeetJS.events.track; const TrackEvents = JitsiMeetJS.events.track;
function LocalVideo(VideoLayout, emitter) { function LocalVideo(VideoLayout, emitter) {
this.videoSpanId = "localVideoContainer"; this.videoSpanId = "localVideoContainer";
this.container = $("#localVideoContainer").get(0); this.container = $("#localVideoContainer").get(0);
this.localVideoId = null;
this.bindHoverHandler(); this.bindHoverHandler();
this.flipX = true; this._buildContextMenu();
this.isLocal = true; this.isLocal = true;
this.emitter = emitter; this.emitter = emitter;
Object.defineProperty(this, 'id', { Object.defineProperty(this, 'id', {
@ -165,9 +164,8 @@ LocalVideo.prototype.changeVideo = function (stream) {
localVideoContainerSelector.off('click'); localVideoContainerSelector.off('click');
localVideoContainerSelector.on('click', localVideoClick); localVideoContainerSelector.on('click', localVideoClick);
this.flipX = stream.videoType != "desktop";
let localVideo = document.createElement('video'); let localVideo = document.createElement('video');
localVideo.id = 'localVideo_' + stream.getId(); localVideo.id = this.localVideoId = 'localVideo_' + stream.getId();
RTCUIUtils.setAutoPlay(localVideo, true); RTCUIUtils.setAutoPlay(localVideo, true);
RTCUIUtils.setVolume(localVideo, 0); RTCUIUtils.setVolume(localVideo, 0);
@ -182,9 +180,9 @@ LocalVideo.prototype.changeVideo = function (stream) {
// onclick has to be used with Temasys plugin // onclick has to be used with Temasys plugin
localVideo.onclick = localVideoClick; localVideo.onclick = localVideoClick;
if (this.flipX) { let isVideo = stream.videoType != "desktop";
$(localVideo).addClass("flipVideoX"); this._enableDisableContextMenu(isVideo);
} this.setFlipX(isVideo? APP.settings.getLocalFlipX() : false);
// Attach WebRTC stream // Attach WebRTC stream
localVideo = stream.attach(localVideo); localVideo = stream.attach(localVideo);
@ -222,4 +220,54 @@ LocalVideo.prototype.setVisible = function(visible) {
} }
}; };
/**
* Sets the flipX state of the video.
* @param val {boolean} true for flipped otherwise false;
*/
LocalVideo.prototype.setFlipX = function (val) {
this.emitter.emit(UIEvents.LOCAL_FLIPX_CHANGED, val);
if(!this.localVideoId)
return;
if(val) {
this.selectVideoElement().addClass("flipVideoX");
} else {
this.selectVideoElement().removeClass("flipVideoX");
}
};
/**
* Builds the context menu for the local video.
*/
LocalVideo.prototype._buildContextMenu = function () {
$.contextMenu({
selector: '#' + this.videoSpanId,
zIndex: 10000,
items: {
flip: {
name: "Flip",
callback: () => {
let val = !APP.settings.getLocalFlipX();
this.setFlipX(val);
APP.settings.setLocalFlipX(val);
}
}
},
events: {
show : function(options){
options.items.flip.name =
APP.translation.translateString("videothumbnail.flip");
}
}
});
};
/**
* Enables or disables the context menu for the local video.
* @param enable {boolean} true for enable, false for disable
*/
LocalVideo.prototype._enableDisableContextMenu = function (enable) {
if($('#' + this.videoSpanId).contextMenu)
$('#' + this.videoSpanId).contextMenu(enable);
};
export default LocalVideo; export default LocalVideo;

View File

@ -165,9 +165,6 @@ SmallVideo.createStreamElement = function (stream) {
console.log("(TIME) Render " + type + ":\t", console.log("(TIME) Render " + type + ":\t",
now); now);
}; };
element.oncontextmenu = function () { return false; };
return element; return element;
}; };

View File

@ -33,6 +33,11 @@ var eventEmitter = null;
*/ */
var pinnedId = null; var pinnedId = null;
/**
* flipX state of the localVideo
*/
let localFlipX = null;
/** /**
* On contact list item clicked. * On contact list item clicked.
*/ */
@ -92,6 +97,11 @@ let largeVideo;
var VideoLayout = { var VideoLayout = {
init (emitter) { init (emitter) {
eventEmitter = emitter; eventEmitter = emitter;
eventEmitter.addListener(UIEvents.LOCAL_FLIPX_CHANGED, function (val) {
localFlipX = val;
if(largeVideo)
largeVideo.onLocalFlipXChange(val);
});
localVideoThumbnail = new LocalVideo(VideoLayout, emitter); localVideoThumbnail = new LocalVideo(VideoLayout, emitter);
// sets default video type of local video // sets default video type of local video
localVideoThumbnail.setVideoType(VIDEO_CONTAINER_TYPE); localVideoThumbnail.setVideoType(VIDEO_CONTAINER_TYPE);
@ -105,6 +115,9 @@ var VideoLayout = {
initLargeVideo (isSideBarVisible) { initLargeVideo (isSideBarVisible) {
largeVideo = new LargeVideoManager(); largeVideo = new LargeVideoManager();
if(localFlipX) {
largeVideo.onLocalFlipXChange(localFlipX);
}
largeVideo.updateContainerSize(isSideBarVisible); largeVideo.updateContainerSize(isSideBarVisible);
AudioLevels.init(); AudioLevels.init();
}, },
@ -1084,6 +1097,15 @@ var VideoLayout = {
videoResolutionLabel.css({display: "block"}); videoResolutionLabel.css({display: "block"});
else if (!isResolutionHD && videoResolutionLabel.is(":visible")) else if (!isResolutionHD && videoResolutionLabel.is(":visible"))
videoResolutionLabel.css({display: "none"}); videoResolutionLabel.css({display: "none"});
},
/**
* Sets the flipX state of the local video.
* @param {boolean} true for flipped otherwise false;
*/
setLocalFlipX: function (val) {
this.localFlipX = val;
} }
}; };

View File

@ -6,6 +6,7 @@ let language = null;
let cameraDeviceId = ''; let cameraDeviceId = '';
let micDeviceId = ''; let micDeviceId = '';
let welcomePageDisabled = false; let welcomePageDisabled = false;
let localFlipX = null;
function supportsLocalStorage() { function supportsLocalStorage() {
try { try {
@ -31,6 +32,7 @@ if (supportsLocalStorage()) {
} }
email = UIUtil.unescapeHtml(window.localStorage.email || ''); email = UIUtil.unescapeHtml(window.localStorage.email || '');
localFlipX = JSON.parse(window.localStorage.localFlipX || true);
displayName = UIUtil.unescapeHtml(window.localStorage.displayname || ''); displayName = UIUtil.unescapeHtml(window.localStorage.displayname || '');
language = window.localStorage.language; language = window.localStorage.language;
cameraDeviceId = window.localStorage.cameraDeviceId || ''; cameraDeviceId = window.localStorage.cameraDeviceId || '';
@ -87,6 +89,23 @@ export default {
window.localStorage.language = lang; window.localStorage.language = lang;
}, },
/**
* Sets new flipX state of local video and saves it to the local storage.
* @param {string} val flipX state of local video
*/
setLocalFlipX: function (val) {
localFlipX = val;
window.localStorage.localFlipX = val;
},
/**
* Returns flipX state of local video.
* @returns {string} flipX
*/
getLocalFlipX: function () {
return localFlipX;
},
/** /**
* Get device id of the camera which is currently in use. * Get device id of the camera which is currently in use.
* Empty string stands for default device. * Empty string stands for default device.

View File

@ -24,6 +24,7 @@
"jquery": "~2.1.1", "jquery": "~2.1.1",
"jQuery-Impromptu": "git+https://github.com/trentrichardson/jQuery-Impromptu.git#v6.0.0", "jQuery-Impromptu": "git+https://github.com/trentrichardson/jQuery-Impromptu.git#v6.0.0",
"lib-jitsi-meet": "jitsi/lib-jitsi-meet", "lib-jitsi-meet": "jitsi/lib-jitsi-meet",
"jquery-contextmenu": "*",
"jquery-ui": "^1.10.5", "jquery-ui": "^1.10.5",
"jssha": "1.5.0", "jssha": "1.5.0",
"retry": "0.6.1", "retry": "0.6.1",
@ -98,6 +99,9 @@
"jQuery-Impromptu": { "jQuery-Impromptu": {
"depends": "jquery:jQuery" "depends": "jquery:jQuery"
}, },
"jquery-contextmenu": {
"depends": "jquery:jQuery"
},
"autosize": { "autosize": {
"depends": "jquery:jQuery" "depends": "jquery:jQuery"
} }

View File

@ -71,5 +71,9 @@ export default {
* Notifies interested listeners that the follow-me feature is enabled or * Notifies interested listeners that the follow-me feature is enabled or
* disabled. * disabled.
*/ */
FOLLOW_ME_ENABLED: "UI.follow_me_enabled" FOLLOW_ME_ENABLED: "UI.follow_me_enabled",
/**
* Notifies that flipX property of the local video is changed.
*/
LOCAL_FLIPX_CHANGED: "UI.local_flipx_changed"
}; };