Merge pull request #646 from jitsi/flip
Implements custom context menu to flip the local video
This commit is contained in:
commit
626b37b4fe
2
Makefile
2
Makefile
|
@ -3,7 +3,7 @@ BROWSERIFY = ./node_modules/.bin/browserify
|
|||
UGLIFYJS = ./node_modules/.bin/uglifyjs
|
||||
EXORCIST = ./node_modules/.bin/exorcist
|
||||
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
|
||||
BROWSERIFY_FLAGS = -d
|
||||
OUTPUT_DIR = .
|
||||
|
|
1
app.js
1
app.js
|
@ -3,6 +3,7 @@
|
|||
|
||||
import "babel-polyfill";
|
||||
import "jquery";
|
||||
import "jquery-contextmenu";
|
||||
import "jquery-ui";
|
||||
import "strophe";
|
||||
import "strophe-disco";
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -99,7 +99,8 @@
|
|||
"mute": "Participant is muted",
|
||||
"kick": "Kick out",
|
||||
"muted": "Muted",
|
||||
"domute": "Mute"
|
||||
"domute": "Mute",
|
||||
"flip": "Flip"
|
||||
|
||||
},
|
||||
"connectionindicator":
|
||||
|
|
|
@ -168,6 +168,7 @@ class VideoContainer extends LargeContainer {
|
|||
super();
|
||||
this.stream = null;
|
||||
this.videoType = null;
|
||||
this.localFlipX = true;
|
||||
|
||||
this.isVisible = false;
|
||||
|
||||
|
@ -284,13 +285,25 @@ class VideoContainer extends LargeContainer {
|
|||
}
|
||||
|
||||
stream.attach(this.$video[0]);
|
||||
|
||||
let flipX = stream.isLocal() && !this.isScreenSharing();
|
||||
let flipX = stream.isLocal() && this.localFlipX;
|
||||
this.$video.css({
|
||||
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.
|
||||
* @returns {boolean}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,16 +4,15 @@ import UIUtil from "../util/UIUtil";
|
|||
import UIEvents from "../../../service/UI/UIEvents";
|
||||
import SmallVideo from "./SmallVideo";
|
||||
|
||||
var LargeVideo = require("./LargeVideo");
|
||||
|
||||
const RTCUIUtils = JitsiMeetJS.util.RTCUIHelper;
|
||||
const TrackEvents = JitsiMeetJS.events.track;
|
||||
|
||||
function LocalVideo(VideoLayout, emitter) {
|
||||
this.videoSpanId = "localVideoContainer";
|
||||
this.container = $("#localVideoContainer").get(0);
|
||||
this.localVideoId = null;
|
||||
this.bindHoverHandler();
|
||||
this.flipX = true;
|
||||
this._buildContextMenu();
|
||||
this.isLocal = true;
|
||||
this.emitter = emitter;
|
||||
Object.defineProperty(this, 'id', {
|
||||
|
@ -165,9 +164,8 @@ LocalVideo.prototype.changeVideo = function (stream) {
|
|||
localVideoContainerSelector.off('click');
|
||||
localVideoContainerSelector.on('click', localVideoClick);
|
||||
|
||||
this.flipX = stream.videoType != "desktop";
|
||||
let localVideo = document.createElement('video');
|
||||
localVideo.id = 'localVideo_' + stream.getId();
|
||||
localVideo.id = this.localVideoId = 'localVideo_' + stream.getId();
|
||||
|
||||
RTCUIUtils.setAutoPlay(localVideo, true);
|
||||
RTCUIUtils.setVolume(localVideo, 0);
|
||||
|
@ -182,9 +180,9 @@ LocalVideo.prototype.changeVideo = function (stream) {
|
|||
// onclick has to be used with Temasys plugin
|
||||
localVideo.onclick = localVideoClick;
|
||||
|
||||
if (this.flipX) {
|
||||
$(localVideo).addClass("flipVideoX");
|
||||
}
|
||||
let isVideo = stream.videoType != "desktop";
|
||||
this._enableDisableContextMenu(isVideo);
|
||||
this.setFlipX(isVideo? APP.settings.getLocalFlipX() : false);
|
||||
|
||||
// Attach WebRTC stream
|
||||
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;
|
||||
|
|
|
@ -165,9 +165,6 @@ SmallVideo.createStreamElement = function (stream) {
|
|||
console.log("(TIME) Render " + type + ":\t",
|
||||
now);
|
||||
};
|
||||
|
||||
element.oncontextmenu = function () { return false; };
|
||||
|
||||
return element;
|
||||
};
|
||||
|
||||
|
|
|
@ -33,6 +33,11 @@ var eventEmitter = null;
|
|||
*/
|
||||
var pinnedId = null;
|
||||
|
||||
/**
|
||||
* flipX state of the localVideo
|
||||
*/
|
||||
let localFlipX = null;
|
||||
|
||||
/**
|
||||
* On contact list item clicked.
|
||||
*/
|
||||
|
@ -92,6 +97,11 @@ let largeVideo;
|
|||
var VideoLayout = {
|
||||
init (emitter) {
|
||||
eventEmitter = emitter;
|
||||
eventEmitter.addListener(UIEvents.LOCAL_FLIPX_CHANGED, function (val) {
|
||||
localFlipX = val;
|
||||
if(largeVideo)
|
||||
largeVideo.onLocalFlipXChange(val);
|
||||
});
|
||||
localVideoThumbnail = new LocalVideo(VideoLayout, emitter);
|
||||
// sets default video type of local video
|
||||
localVideoThumbnail.setVideoType(VIDEO_CONTAINER_TYPE);
|
||||
|
@ -105,6 +115,9 @@ var VideoLayout = {
|
|||
|
||||
initLargeVideo (isSideBarVisible) {
|
||||
largeVideo = new LargeVideoManager();
|
||||
if(localFlipX) {
|
||||
largeVideo.onLocalFlipXChange(localFlipX);
|
||||
}
|
||||
largeVideo.updateContainerSize(isSideBarVisible);
|
||||
AudioLevels.init();
|
||||
},
|
||||
|
@ -1084,6 +1097,15 @@ var VideoLayout = {
|
|||
videoResolutionLabel.css({display: "block"});
|
||||
else if (!isResolutionHD && videoResolutionLabel.is(":visible"))
|
||||
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;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ let language = null;
|
|||
let cameraDeviceId = '';
|
||||
let micDeviceId = '';
|
||||
let welcomePageDisabled = false;
|
||||
let localFlipX = null;
|
||||
|
||||
function supportsLocalStorage() {
|
||||
try {
|
||||
|
@ -31,6 +32,7 @@ if (supportsLocalStorage()) {
|
|||
}
|
||||
|
||||
email = UIUtil.unescapeHtml(window.localStorage.email || '');
|
||||
localFlipX = JSON.parse(window.localStorage.localFlipX || true);
|
||||
displayName = UIUtil.unescapeHtml(window.localStorage.displayname || '');
|
||||
language = window.localStorage.language;
|
||||
cameraDeviceId = window.localStorage.cameraDeviceId || '';
|
||||
|
@ -87,6 +89,23 @@ export default {
|
|||
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.
|
||||
* Empty string stands for default device.
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
"jquery": "~2.1.1",
|
||||
"jQuery-Impromptu": "git+https://github.com/trentrichardson/jQuery-Impromptu.git#v6.0.0",
|
||||
"lib-jitsi-meet": "jitsi/lib-jitsi-meet",
|
||||
"jquery-contextmenu": "*",
|
||||
"jquery-ui": "^1.10.5",
|
||||
"jssha": "1.5.0",
|
||||
"retry": "0.6.1",
|
||||
|
@ -98,6 +99,9 @@
|
|||
"jQuery-Impromptu": {
|
||||
"depends": "jquery:jQuery"
|
||||
},
|
||||
"jquery-contextmenu": {
|
||||
"depends": "jquery:jQuery"
|
||||
},
|
||||
"autosize": {
|
||||
"depends": "jquery:jQuery"
|
||||
}
|
||||
|
|
|
@ -71,5 +71,9 @@ export default {
|
|||
* Notifies interested listeners that the follow-me feature is enabled or
|
||||
* 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"
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue