mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-01-01 12:22:28 +00:00
Compare commits
1 Commits
4567
...
remote-con
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9235000a7f |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -69,7 +69,6 @@ buck-out/
|
||||
*.framework
|
||||
android/app/debug
|
||||
android/app/release
|
||||
ios/sdk/out
|
||||
|
||||
# precommit-hook
|
||||
.jshintignore
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
/**
|
||||
* Notifies interested parties that hangup procedure will start.
|
||||
*/
|
||||
export const BEFORE_HANGUP = 'conference.before_hangup';
|
||||
@@ -25,5 +25,5 @@ android.enableDexingArtifactTransform.desugaring=false
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
appVersion=20.6.0
|
||||
sdkVersion=2.12.0
|
||||
appVersion=20.5.0
|
||||
sdkVersion=2.11.0
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.jitsi.meet.sdk">
|
||||
<!-- XXX ACCESS_NETWORK_STATE is required by WebRTC. -->
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
@@ -35,7 +34,7 @@
|
||||
android:launchMode="singleTask"
|
||||
android:resizeableActivity="true"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:windowSoftInputMode="adjustResize"/>
|
||||
android:windowSoftInputMode="adjustResize"></activity>
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||
|
||||
<service
|
||||
@@ -49,13 +48,6 @@
|
||||
<service
|
||||
android:name="org.jitsi.meet.sdk.JitsiMeetOngoingConferenceService"
|
||||
android:foregroundServiceType="mediaProjection" />
|
||||
|
||||
<provider
|
||||
android:name="com.reactnativecommunity.webview.RNCWebViewFileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:enabled="false"
|
||||
tools:replace="android:authorities">
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -16,11 +16,8 @@
|
||||
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.AudioDeviceInfo;
|
||||
import android.media.AudioFocusRequest;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
@@ -63,7 +60,7 @@ class AudioDeviceHandlerGeneric implements
|
||||
private AudioManager audioManager;
|
||||
|
||||
/**
|
||||
* {@link Runnable} for running audio device detection in the audio thread.
|
||||
* {@link Runnable} for running audio device detection the main thread.
|
||||
* This is only used on Android >= M.
|
||||
*/
|
||||
private final Runnable onAudioDeviceChangeRunner = new Runnable() {
|
||||
@@ -145,7 +142,7 @@ class AudioDeviceHandlerGeneric implements
|
||||
// Some other application potentially stole our audio focus
|
||||
// temporarily. Restore our mode.
|
||||
if (audioFocusLost) {
|
||||
module.resetAudioRoute();
|
||||
module.updateAudioRoute();
|
||||
}
|
||||
audioFocusLost = false;
|
||||
break;
|
||||
@@ -219,24 +216,8 @@ class AudioDeviceHandlerGeneric implements
|
||||
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
|
||||
audioManager.setMicrophoneMute(false);
|
||||
|
||||
int gotFocus;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
gotFocus = audioManager.requestAudioFocus(new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
|
||||
.setAudioAttributes(
|
||||
new AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||
.build()
|
||||
)
|
||||
.setAcceptsDelayedFocusGain(true)
|
||||
.setOnAudioFocusChangeListener(this)
|
||||
.build()
|
||||
);
|
||||
} else {
|
||||
gotFocus = audioManager.requestAudioFocus(this, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN);
|
||||
}
|
||||
|
||||
if (gotFocus == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
|
||||
if (audioManager.requestAudioFocus(this, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN)
|
||||
== AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
|
||||
JitsiMeetLogger.w(TAG + " Audio focus request failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
@@ -257,7 +256,7 @@ class AudioModeModule extends ReactContextBaseJavaModule {
|
||||
if (mode != -1) {
|
||||
JitsiMeetLogger.i(TAG + " User selected device set to: " + device);
|
||||
userSelectedDevice = device;
|
||||
updateAudioRoute(mode, false);
|
||||
updateAudioRoute(mode);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -277,22 +276,13 @@ class AudioModeModule extends ReactContextBaseJavaModule {
|
||||
return;
|
||||
}
|
||||
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
if (currentActivity != null) {
|
||||
if (mode == DEFAULT) {
|
||||
currentActivity.setVolumeControlStream(AudioManager.USE_DEFAULT_STREAM_TYPE);
|
||||
} else {
|
||||
currentActivity.setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
|
||||
}
|
||||
}
|
||||
|
||||
runInAudioThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
boolean success;
|
||||
|
||||
try {
|
||||
success = updateAudioRoute(mode, false);
|
||||
success = updateAudioRoute(mode);
|
||||
} catch (Throwable e) {
|
||||
success = false;
|
||||
JitsiMeetLogger.e(e, TAG + " Failed to update audio route for mode: " + mode);
|
||||
@@ -331,7 +321,7 @@ class AudioModeModule extends ReactContextBaseJavaModule {
|
||||
* @return {@code true} if the audio route was updated successfully;
|
||||
* {@code false}, otherwise.
|
||||
*/
|
||||
private boolean updateAudioRoute(int mode, boolean force) {
|
||||
private boolean updateAudioRoute(int mode) {
|
||||
JitsiMeetLogger.i(TAG + " Update audio route for mode: " + mode);
|
||||
|
||||
if (!audioDeviceHandler.setMode(mode)) {
|
||||
@@ -366,7 +356,7 @@ class AudioModeModule extends ReactContextBaseJavaModule {
|
||||
|
||||
// If the previously selected device and the current default one
|
||||
// match, do nothing.
|
||||
if (!force && selectedDevice != null && selectedDevice.equals(audioDevice)) {
|
||||
if (selectedDevice != null && selectedDevice.equals(audioDevice)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -431,16 +421,7 @@ class AudioModeModule extends ReactContextBaseJavaModule {
|
||||
*/
|
||||
void updateAudioRoute() {
|
||||
if (mode != -1) {
|
||||
updateAudioRoute(mode, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-sets the current audio route. Needed when focus is lost and regained.
|
||||
*/
|
||||
void resetAudioRoute() {
|
||||
if (mode != -1) {
|
||||
updateAudioRoute(mode, true);
|
||||
updateAudioRoute(mode);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2
app.js
2
app.js
@@ -17,7 +17,6 @@ import conference from './conference';
|
||||
import API from './modules/API';
|
||||
import UI from './modules/UI/UI';
|
||||
import keyboardshortcut from './modules/keyboardshortcut/keyboardshortcut';
|
||||
import remoteControl from './modules/remotecontrol/RemoteControl';
|
||||
import translation from './modules/translation/translation';
|
||||
|
||||
// Initialize Olm as early as possible.
|
||||
@@ -49,7 +48,6 @@ window.APP = {
|
||||
},
|
||||
|
||||
keyboardshortcut,
|
||||
remoteControl,
|
||||
translation,
|
||||
UI
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import EventEmitter from 'events';
|
||||
import Logger from 'jitsi-meet-logger';
|
||||
|
||||
import * as JitsiMeetConferenceEvents from './ConferenceEvents';
|
||||
import { openConnection } from './connection';
|
||||
import { ENDPOINT_TEXT_MESSAGE_NAME } from './modules/API/constants';
|
||||
import AuthHandler from './modules/UI/authentication/AuthHandler';
|
||||
@@ -86,7 +85,8 @@ import {
|
||||
participantMutedUs,
|
||||
participantPresenceChanged,
|
||||
participantRoleChanged,
|
||||
participantUpdated
|
||||
participantUpdated,
|
||||
updateRemoteParticipantFeatures
|
||||
} from './react/features/base/participants';
|
||||
import {
|
||||
getUserSelectedCameraDeviceId,
|
||||
@@ -122,14 +122,13 @@ import {
|
||||
isPrejoinPageVisible,
|
||||
makePrecallTest
|
||||
} from './react/features/prejoin';
|
||||
import { disableReceiver, stopReceiver } from './react/features/remote-control';
|
||||
import { toggleScreenshotCaptureEffect } from './react/features/screenshot-capture';
|
||||
import { setSharedVideoStatus } from './react/features/shared-video';
|
||||
import { AudioMixerEffect } from './react/features/stream-effects/audio-mixer/AudioMixerEffect';
|
||||
import { createPresenterEffect } from './react/features/stream-effects/presenter';
|
||||
import { endpointMessageReceived } from './react/features/subtitles';
|
||||
import UIEvents from './service/UI/UIEvents';
|
||||
import * as RemoteControlEvents
|
||||
from './service/remotecontrol/RemoteControlEvents';
|
||||
|
||||
const logger = Logger.getLogger(__filename);
|
||||
|
||||
@@ -671,7 +670,6 @@ export default {
|
||||
APP.connection = connection = con;
|
||||
|
||||
this._createRoom(tracks);
|
||||
APP.remoteControl.init();
|
||||
|
||||
// if user didn't give access to mic or camera or doesn't have
|
||||
// them at all, we mark corresponding toolbar buttons as muted,
|
||||
@@ -1428,11 +1426,8 @@ export default {
|
||||
async _turnScreenSharingOff(didHaveVideo) {
|
||||
this._untoggleScreenSharing = null;
|
||||
this.videoSwitchInProgress = true;
|
||||
const { receiver } = APP.remoteControl;
|
||||
|
||||
if (receiver) {
|
||||
receiver.stop();
|
||||
}
|
||||
APP.store.dispatch(stopReceiver());
|
||||
|
||||
this._stopProxyConnection();
|
||||
if (config.enableScreenshotCapture) {
|
||||
@@ -1855,8 +1850,9 @@ export default {
|
||||
(authEnabled, authLogin) =>
|
||||
APP.store.dispatch(authStatusChanged(authEnabled, authLogin)));
|
||||
|
||||
room.on(JitsiConferenceEvents.PARTCIPANT_FEATURES_CHANGED,
|
||||
user => APP.UI.onUserFeaturesChanged(user));
|
||||
room.on(JitsiConferenceEvents.PARTCIPANT_FEATURES_CHANGED, user => {
|
||||
APP.store.dispatch(updateRemoteParticipantFeatures(user));
|
||||
});
|
||||
room.on(JitsiConferenceEvents.USER_JOINED, (id, user) => {
|
||||
// The logic shared between RN and web.
|
||||
commonUserJoinedHandling(APP.store, room, user);
|
||||
@@ -1865,6 +1861,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
APP.store.dispatch(updateRemoteParticipantFeatures(user));
|
||||
logger.log(`USER ${id} connnected:`, user);
|
||||
APP.UI.addUser(user);
|
||||
});
|
||||
@@ -2035,30 +2032,6 @@ export default {
|
||||
JitsiConferenceEvents.LOCK_STATE_CHANGED,
|
||||
(...args) => APP.store.dispatch(lockStateChanged(room, ...args)));
|
||||
|
||||
APP.remoteControl.on(RemoteControlEvents.ACTIVE_CHANGED, isActive => {
|
||||
room.setLocalParticipantProperty(
|
||||
'remoteControlSessionStatus',
|
||||
isActive
|
||||
);
|
||||
APP.UI.setLocalRemoteControlActiveChanged();
|
||||
});
|
||||
|
||||
/* eslint-disable max-params */
|
||||
room.on(
|
||||
JitsiConferenceEvents.PARTICIPANT_PROPERTY_CHANGED,
|
||||
(participant, name, oldValue, newValue) => {
|
||||
switch (name) {
|
||||
case 'remoteControlSessionStatus':
|
||||
APP.UI.setRemoteControlActiveStatus(
|
||||
participant.getId(),
|
||||
newValue);
|
||||
break;
|
||||
default:
|
||||
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
|
||||
room.on(JitsiConferenceEvents.KICKED, participant => {
|
||||
APP.UI.hideStats();
|
||||
APP.store.dispatch(kickedOut(room, participant));
|
||||
@@ -2403,25 +2376,6 @@ export default {
|
||||
APP.UI.changeDisplayName('localVideoContainer', displayName);
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds any room listener.
|
||||
* @param {string} eventName one of the JitsiConferenceEvents
|
||||
* @param {Function} listener the function to be called when the event
|
||||
* occurs
|
||||
*/
|
||||
addConferenceListener(eventName, listener) {
|
||||
room.on(eventName, listener);
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes any room listener.
|
||||
* @param {string} eventName one of the JitsiConferenceEvents
|
||||
* @param {Function} listener the listener to be removed.
|
||||
*/
|
||||
removeConferenceListener(eventName, listener) {
|
||||
room.off(eventName, listener);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the list of current devices.
|
||||
* @param {boolean} setDeviceListChangeHandler - Whether to add the deviceList change handlers.
|
||||
@@ -2704,7 +2658,7 @@ export default {
|
||||
* requested
|
||||
*/
|
||||
hangup(requestFeedback = false) {
|
||||
eventEmitter.emit(JitsiMeetConferenceEvents.BEFORE_HANGUP);
|
||||
APP.store.dispatch(disableReceiver());
|
||||
|
||||
this._stopProxyConnection();
|
||||
|
||||
@@ -2721,7 +2675,6 @@ export default {
|
||||
}
|
||||
|
||||
APP.UI.removeAllListeners();
|
||||
APP.remoteControl.removeAllListeners();
|
||||
|
||||
let requestFeedbackPromise;
|
||||
|
||||
@@ -2904,29 +2857,6 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the desktop sharing source id or undefined if the desktop sharing
|
||||
* is not active at the moment.
|
||||
*
|
||||
* @returns {string|undefined} - The source id. If the track is not desktop
|
||||
* track or the source id is not available, undefined will be returned.
|
||||
*/
|
||||
getDesktopSharingSourceId() {
|
||||
return this.localVideo.sourceId;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the desktop sharing source type or undefined if the desktop
|
||||
* sharing is not active at the moment.
|
||||
*
|
||||
* @returns {'screen'|'window'|undefined} - The source type. If the track is
|
||||
* not desktop track or the source type is not available, undefined will be
|
||||
* returned.
|
||||
*/
|
||||
getDesktopSharingSourceType() {
|
||||
return this.localVideo.sourceType;
|
||||
},
|
||||
|
||||
/**
|
||||
* Callback invoked by the external api create or update a direct connection
|
||||
* from the local client to an external client.
|
||||
|
||||
@@ -94,11 +94,6 @@ var config = {
|
||||
// input and will suggest another valid device if one is present.
|
||||
enableNoAudioDetection: true,
|
||||
|
||||
// Enabling this will show a "Save Logs" link in the GSM popover that can be
|
||||
// used to collect debug information (XMPP IQs, SDP offer/answer cycles)
|
||||
// about the call.
|
||||
// enableSaveLogs: false,
|
||||
|
||||
// Enabling this will run the lib-jitsi-meet noise detection module which will
|
||||
// notify the user if there is noise, other than voice, coming from the current
|
||||
// selected microphone. The purpose it to let the user know that the input could
|
||||
@@ -398,9 +393,6 @@ var config = {
|
||||
// Document should be focused for this option to work
|
||||
// enableAutomaticUrlCopy: false,
|
||||
|
||||
// Base URL for a Gravatar-compatible service. Defaults to libravatar.
|
||||
// gravatarBaseURL: 'https://seccdn.libravatar.org/avatar/';
|
||||
|
||||
// Stats
|
||||
//
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
platform :ios, '11.0'
|
||||
workspace 'jitsi-meet'
|
||||
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
|
||||
install! 'cocoapods', :deterministic_uuids => false
|
||||
|
||||
target 'jitsi-meet' do
|
||||
project 'app/app.xcodeproj'
|
||||
|
||||
@@ -293,7 +293,7 @@ PODS:
|
||||
- React
|
||||
- react-native-splash-screen (3.2.0):
|
||||
- React
|
||||
- react-native-webrtc (1.87.1):
|
||||
- react-native-webrtc (1.84.1):
|
||||
- React-Core
|
||||
- react-native-webview (10.9.0):
|
||||
- React
|
||||
@@ -562,7 +562,7 @@ SPEC CHECKSUMS:
|
||||
react-native-keep-awake: eba3137546b10003361b37c761f6c429b59814ae
|
||||
react-native-netinfo: 8d8db463bcc5db66a8ac5c48a7d86beb3b92f61a
|
||||
react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865
|
||||
react-native-webrtc: 40eca4cac200fda34fb843da07e3402211bbbd10
|
||||
react-native-webrtc: edd689b0d5a462d7a6f6f52bca3f9414fc0ee11c
|
||||
react-native-webview: 6ee7868ca8eba635dbf7963986d1ab7959da0391
|
||||
React-RCTActionSheet: bcbc311dc3b47bc8efb2737ff0940239a45789a9
|
||||
React-RCTAnimation: 65f61080ce632f6dea23d52e354ffac9948396c6
|
||||
@@ -582,6 +582,6 @@ SPEC CHECKSUMS:
|
||||
RNWatch: a5320c959c75e72845c07985f3e935e58998f1d3
|
||||
Yoga: 96b469c5e81ff51b917b92e8c3390642d4ded30c
|
||||
|
||||
PODFILE CHECKSUM: f6626cd705333112182cedbe175ae2f9006e8874
|
||||
PODFILE CHECKSUM: f2400f8e5a52c4d91697cbacba6956569efc5ab8
|
||||
|
||||
COCOAPODS: 1.10.0
|
||||
COCOAPODS: 1.9.3
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 52;
|
||||
objectVersion = 46;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -13,6 +13,8 @@
|
||||
0B412F211EDEE95300B1A0A6 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0B412F201EDEE95300B1A0A6 /* Main.storyboard */; };
|
||||
0B5418471F7C5D8C00A2DD86 /* MeetingRowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B5418461F7C5D8C00A2DD86 /* MeetingRowController.swift */; };
|
||||
0B7001701F7C51CC005944F4 /* InCallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B70016F1F7C51CC005944F4 /* InCallController.swift */; };
|
||||
0BD6B4371EF82A6B00D1F4CD /* WebRTC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BD6B4361EF82A6B00D1F4CD /* WebRTC.framework */; };
|
||||
0BD6B4381EF82A6B00D1F4CD /* WebRTC.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0BD6B4361EF82A6B00D1F4CD /* WebRTC.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
0BEA5C291F7B8F73000D0AB4 /* Interface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0BEA5C271F7B8F73000D0AB4 /* Interface.storyboard */; };
|
||||
0BEA5C2B1F7B8F73000D0AB4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0BEA5C2A1F7B8F73000D0AB4 /* Assets.xcassets */; };
|
||||
0BEA5C321F7B8F73000D0AB4 /* JitsiMeetCompanion Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 0BEA5C311F7B8F73000D0AB4 /* JitsiMeetCompanion Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
@@ -26,8 +28,6 @@
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
||||
695AF3ED6F686F9C5EE40F9A /* libPods-jitsi-meet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 489E8EFE2C720D10F5961AEF /* libPods-jitsi-meet.a */; };
|
||||
DE050389256E904600DEE3A5 /* WebRTC.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE050388256E904600DEE3A5 /* WebRTC.xcframework */; };
|
||||
DE05038A256E904600DEE3A5 /* WebRTC.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE050388256E904600DEE3A5 /* WebRTC.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
DE4C456121DE1E4E00EA0709 /* FIRUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = DE4C455F21DE1E4E00EA0709 /* FIRUtilities.m */; };
|
||||
E588011722789D43008B0561 /* JitsiMeetContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = E58801132278944E008B0561 /* JitsiMeetContext.swift */; };
|
||||
E5C97B63227A1EB400199214 /* JitsiMeetCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5C97B62227A1EB400199214 /* JitsiMeetCommands.swift */; };
|
||||
@@ -57,8 +57,8 @@
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
DE05038A256E904600DEE3A5 /* WebRTC.xcframework in Embed Frameworks */,
|
||||
0B26BE6F1EC5BC3C00EEFB41 /* JitsiMeet.framework in Embed Frameworks */,
|
||||
0BD6B4381EF82A6B00D1F4CD /* WebRTC.framework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -117,10 +117,8 @@
|
||||
4670A512A688E2DC34528282 /* Pods-jitsi-meet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-jitsi-meet.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-jitsi-meet/Pods-jitsi-meet.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
489E8EFE2C720D10F5961AEF /* libPods-jitsi-meet.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-jitsi-meet.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
B3B083EB1D4955FF0069CEE7 /* app.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = app.entitlements; sourceTree = "<group>"; };
|
||||
DE050388256E904600DEE3A5 /* WebRTC.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = WebRTC.xcframework; path = "../../node_modules/react-native-webrtc/apple/WebRTC.xcframework"; sourceTree = "<group>"; };
|
||||
DE4C455F21DE1E4E00EA0709 /* FIRUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRUtilities.m; sourceTree = "<group>"; };
|
||||
DE4C456021DE1E4E00EA0709 /* FIRUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FIRUtilities.h; sourceTree = "<group>"; };
|
||||
DEFDBBDB25656E3B00344B23 /* WebRTC.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = WebRTC.xcframework; path = "../../node_modules/react-native-webrtc/ios/WebRTC.xcframework"; sourceTree = "<group>"; };
|
||||
E58801132278944E008B0561 /* JitsiMeetContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JitsiMeetContext.swift; sourceTree = "<group>"; };
|
||||
E5C97B62227A1EB400199214 /* JitsiMeetCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JitsiMeetCommands.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
@@ -138,8 +136,8 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
0B26BE6E1EC5BC3C00EEFB41 /* JitsiMeet.framework in Frameworks */,
|
||||
0BD6B4371EF82A6B00D1F4CD /* WebRTC.framework in Frameworks */,
|
||||
695AF3ED6F686F9C5EE40F9A /* libPods-jitsi-meet.a in Frameworks */,
|
||||
DE050389256E904600DEE3A5 /* WebRTC.xcframework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -156,9 +154,7 @@
|
||||
0B26BE711EC5BC4D00EEFB41 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DE050388256E904600DEE3A5 /* WebRTC.xcframework */,
|
||||
0B26BE6D1EC5BC3C00EEFB41 /* JitsiMeet.framework */,
|
||||
DEFDBBDB25656E3B00344B23 /* WebRTC.xcframework */,
|
||||
0BD6B4361EF82A6B00D1F4CD /* WebRTC.framework */,
|
||||
489E8EFE2C720D10F5961AEF /* libPods-jitsi-meet.a */,
|
||||
);
|
||||
@@ -294,6 +290,8 @@
|
||||
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
||||
13B07F8E1A680F5B00A75B9A /* Resources */,
|
||||
0B26BE701EC5BC3C00EEFB41 /* Embed Frameworks */,
|
||||
B35383AD1DDA0083008F406A /* Adjust embedded framework architectures */,
|
||||
DE3A859324C701EA009B7D76 /* Copy WebRTC dSYM */,
|
||||
0BB7DA181EC9E695007AAE98 /* Adjust ATS */,
|
||||
DEF4813D224925A2002AD03A /* Copy Google Plist file */,
|
||||
DE11877A21EE09640078D059 /* Setup Google reverse URL handler */,
|
||||
@@ -422,6 +420,20 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "../scripts/run-packager.sh\n";
|
||||
};
|
||||
B35383AD1DDA0083008F406A /* Adjust embedded framework architectures */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Adjust embedded framework architectures";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "../scripts/fixup-frameworks.sh\n";
|
||||
};
|
||||
B6607F42A5CF0C76E98929E2 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -462,6 +474,24 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "INFO_PLIST=\"$BUILT_PRODUCTS_DIR/$INFOPLIST_PATH\"\nGOOGLE_PLIST=\"$PROJECT_DIR/GoogleService-Info.plist\"\n\nif [[ -f $GOOGLE_PLIST ]]; then\n REVERSED_CLIENT_ID=$(/usr/libexec/PlistBuddy -c \"Print :REVERSED_CLIENT_ID:\" $GOOGLE_PLIST)\n /usr/libexec/PlistBuddy -c \"Set :CFBundleURLTypes:1:CFBundleURLSchemes:0 $REVERSED_CLIENT_ID\" $INFO_PLIST\nfi\n";
|
||||
};
|
||||
DE3A859324C701EA009B7D76 /* Copy WebRTC dSYM */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Copy WebRTC dSYM";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "set -x\n\nif [[ \"${CONFIGURATION}\" != \"Debug\" ]]; then\n cp -r ../../node_modules/react-native-webrtc/ios/WebRTC.dSYM ${DWARF_DSYM_FOLDER_PATH}/\nfi\n";
|
||||
};
|
||||
DE4F6D6E22005C0400DE699E /* Setup Dropbox */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -622,8 +652,7 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = watchos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 4.0;
|
||||
@@ -650,11 +679,7 @@
|
||||
DEVELOPMENT_TEAM = FC967L3QRG;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = watchos/extension/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.meet.watchkit.extension;
|
||||
PRODUCT_NAME = "${TARGET_NAME}";
|
||||
SDKROOT = watchos;
|
||||
@@ -688,17 +713,12 @@
|
||||
DEVELOPMENT_TEAM = FC967L3QRG;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = watchos/extension/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.meet.watchkit.extension;
|
||||
PRODUCT_NAME = "${TARGET_NAME}";
|
||||
SDKROOT = watchos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 4.0;
|
||||
@@ -709,6 +729,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 4670A512A688E2DC34528282 /* Pods-jitsi-meet.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIconDebug;
|
||||
CODE_SIGN_ENTITLEMENTS = app.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
@@ -717,11 +738,16 @@
|
||||
DEAD_CODE_STRIPPING = NO;
|
||||
DEVELOPMENT_TEAM = FC967L3QRG;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = src/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"../../node_modules/react-native-webrtc/ios",
|
||||
);
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
||||
);
|
||||
INFOPLIST_FILE = src/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = "$(inherited)";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
@@ -738,6 +764,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 09AA3B93E4CC62D84B424690 /* Pods-jitsi-meet.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIconRelease;
|
||||
CODE_SIGN_ENTITLEMENTS = app.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
@@ -745,11 +772,16 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = FC967L3QRG;
|
||||
ENABLE_BITCODE = YES;
|
||||
INFOPLIST_FILE = src/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"../../node_modules/react-native-webrtc/ios",
|
||||
);
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
||||
);
|
||||
INFOPLIST_FILE = src/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = "$(inherited)";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>20.6.0</string>
|
||||
<string>20.5.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>20.6.0</string>
|
||||
<string>20.5.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>20.6.0</string>
|
||||
<string>20.5.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>CLKComplicationPrincipalClass</key>
|
||||
|
||||
15
ios/scripts/bitcode.sh
Executable file
15
ios/scripts/bitcode.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script will download a bitcode build of the WebRTC framework, if needed.
|
||||
|
||||
if [[ ! "$CONFIGURATION" = "Debug" ]]; then
|
||||
RN_WEBRTC="$SRCROOT/../../node_modules/react-native-webrtc"
|
||||
|
||||
if otool -arch arm64 -l $RN_WEBRTC/ios/WebRTC.framework/WebRTC | grep -q LLVM; then
|
||||
echo "WebRTC framework has bitcode"
|
||||
else
|
||||
echo "WebRTC framework has NO bitcode"
|
||||
$RN_WEBRTC/tools/downloadBitcode.sh
|
||||
fi
|
||||
fi
|
||||
|
||||
39
ios/scripts/fixup-frameworks.sh
Executable file
39
ios/scripts/fixup-frameworks.sh
Executable file
@@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script gets executed from Xcode to fixup the embedded frameworks and
|
||||
# bundle the necessary architectures.
|
||||
|
||||
|
||||
APP_PATH="${TARGET_BUILD_DIR}/${WRAPPER_NAME}"
|
||||
|
||||
# This script loops through the frameworks embedded in the application and
|
||||
# removes unused architectures.
|
||||
find "$APP_PATH" -name '*.framework' -type d | while read -r FRAMEWORK
|
||||
do
|
||||
FRAMEWORK_EXECUTABLE_NAME=$(defaults read "$FRAMEWORK/Info.plist" CFBundleExecutable)
|
||||
FRAMEWORK_EXECUTABLE_PATH="$FRAMEWORK/$FRAMEWORK_EXECUTABLE_NAME"
|
||||
echo "Executable is $FRAMEWORK_EXECUTABLE_PATH"
|
||||
|
||||
EXTRACTED_ARCHS=()
|
||||
|
||||
for ARCH in $ARCHS
|
||||
do
|
||||
if lipo -info "$FRAMEWORK_EXECUTABLE_PATH" | grep -q -v "^Non-fat"
|
||||
then
|
||||
echo "Extracting $ARCH from $FRAMEWORK_EXECUTABLE_NAME"
|
||||
lipo -extract "$ARCH" "$FRAMEWORK_EXECUTABLE_PATH" -o "$FRAMEWORK_EXECUTABLE_PATH-$ARCH"
|
||||
EXTRACTED_ARCHS+=("$FRAMEWORK_EXECUTABLE_PATH-$ARCH")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$EXTRACTED_ARCHS" ]
|
||||
then
|
||||
echo "Merging extracted architectures: ${ARCHS}"
|
||||
lipo -o "$FRAMEWORK_EXECUTABLE_PATH-merged" -create "${EXTRACTED_ARCHS[@]}"
|
||||
rm "${EXTRACTED_ARCHS[@]}"
|
||||
|
||||
echo "Replacing original executable with thinned version"
|
||||
rm "$FRAMEWORK_EXECUTABLE_PATH"
|
||||
mv "$FRAMEWORK_EXECUTABLE_PATH-merged" "$FRAMEWORK_EXECUTABLE_PATH"
|
||||
fi
|
||||
done
|
||||
@@ -24,36 +24,8 @@ popd
|
||||
|
||||
# Build the SDK
|
||||
pushd ${PROJECT_REPO}
|
||||
rm -rf ios/sdk/out
|
||||
xcodebuild clean \
|
||||
-workspace ios/jitsi-meet.xcworkspace \
|
||||
-scheme JitsiMeet
|
||||
xcodebuild archive \
|
||||
-workspace ios/jitsi-meet.xcworkspace \
|
||||
-scheme JitsiMeet \
|
||||
-configuration Release \
|
||||
-sdk iphonesimulator \
|
||||
-destination='generic/platform=iOS Simulator' \
|
||||
-archivePath ios/sdk/out/ios-simulator \
|
||||
VALID_ARCHS=x86_64 \
|
||||
ENABLE_BITCODE=NO \
|
||||
SKIP_INSTALL=NO \
|
||||
BUILD_LIBRARY_FOR_DISTRIBUTION=YES
|
||||
xcodebuild archive \
|
||||
-workspace ios/jitsi-meet.xcworkspace \
|
||||
-scheme JitsiMeet \
|
||||
-configuration Release \
|
||||
-sdk iphoneos \
|
||||
-destination='generic/platform=iOS' \
|
||||
-archivePath ios/sdk/out/ios-device \
|
||||
VALID_ARCHS=arm64 \
|
||||
ENABLE_BITCODE=NO \
|
||||
SKIP_INSTALL=NO \
|
||||
BUILD_LIBRARY_FOR_DISTRIBUTION=YES
|
||||
xcodebuild -create-xcframework \
|
||||
-framework ios/sdk/out/ios-device.xcarchive/Products/Library/Frameworks/JitsiMeet.framework \
|
||||
-framework ios/sdk/out/ios-simulator.xcarchive/Products/Library/Frameworks/JitsiMeet.framework \
|
||||
-output ios/sdk/out/JitsiMeet.xcframework
|
||||
rm -rf ios/sdk/JitsiMeet.framework
|
||||
xcodebuild -workspace ios/jitsi-meet.xcworkspace -scheme JitsiMeet -destination='generic/platform=iOS' -configuration Release ENABLE_BITCODE=NO clean archive
|
||||
if [[ $DO_GIT_TAG == 1 ]]; then
|
||||
git tag ios-sdk-${SDK_VERSION}
|
||||
fi
|
||||
@@ -62,8 +34,12 @@ popd
|
||||
pushd ${RELEASE_REPO}
|
||||
|
||||
# Put the new files in the repo
|
||||
cp -a ${PROJECT_REPO}/ios/sdk/out/JitsiMeet.xcframework Frameworks/
|
||||
cp -a ${PROJECT_REPO}/node_modules/react-native-webrtc/apple/WebRTC.xcframework Frameworks/
|
||||
cp -r ${PROJECT_REPO}/ios/sdk/JitsiMeet.framework Frameworks/
|
||||
cp -r ${PROJECT_REPO}/node_modules/react-native-webrtc/ios/WebRTC.framework Frameworks/
|
||||
|
||||
# Strip bitcode
|
||||
xcrun bitcode_strip -r Frameworks/JitsiMeet.framework/JitsiMeet -o Frameworks/JitsiMeet.framework/JitsiMeet
|
||||
xcrun bitcode_strip -r Frameworks/WebRTC.framework/WebRTC -o Frameworks/WebRTC.framework/WebRTC
|
||||
|
||||
# Add all files to git
|
||||
if [[ $DO_GIT_TAG == 1 ]]; then
|
||||
|
||||
@@ -321,6 +321,7 @@
|
||||
buildConfigurationList = 0BD906ED1EC0C00300C8C18E /* Build configuration list for PBXNativeTarget "JitsiMeet" */;
|
||||
buildPhases = (
|
||||
26796D8589142D80C8AFDA51 /* [CP] Check Pods Manifest.lock */,
|
||||
DE3D81D6228B50FB00A6C149 /* Bitcode */,
|
||||
0BD906E01EC0C00300C8C18E /* Sources */,
|
||||
0BD906E11EC0C00300C8C18E /* Frameworks */,
|
||||
0BD906E21EC0C00300C8C18E /* Headers */,
|
||||
@@ -453,6 +454,24 @@
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
DE3D81D6228B50FB00A6C149 /* Bitcode */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = Bitcode;
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "../scripts/bitcode.sh\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -616,6 +635,7 @@
|
||||
baseConfigurationReference = 98E09B5C73D9036B4ED252FC /* Pods-JitsiMeet.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO;
|
||||
BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
|
||||
@@ -626,6 +646,7 @@
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = src/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.JitsiMeetSDK.ios;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -634,6 +655,7 @@
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
@@ -643,6 +665,7 @@
|
||||
baseConfigurationReference = 9C77CA3CC919B081F1A52982 /* Pods-JitsiMeet.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO;
|
||||
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
|
||||
@@ -653,6 +676,7 @@
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
ENABLE_BITCODE = YES;
|
||||
INFOPLIST_FILE = src/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.JitsiMeetSDK.ios;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -660,6 +684,7 @@
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1020"
|
||||
version = "1.3">
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
@@ -72,5 +72,23 @@
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
<PostActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Run Script"
|
||||
scriptText = "exec > /tmp/${PROJECT_NAME}_archive.log 2>&1 UNIVERSAL_OUTPUTFOLDER=${BUILD_DIR}/${CONFIGURATION}-universal if [ "true" == ${ALREADYINVOKED:-false} ] then echo "RECURSION: Detected, stopping" else export ALREADYINVOKED="true" # make sure the output directory exists mkdir -p "${UNIVERSAL_OUTPUTFOLDER}" echo "Building for iPhoneSimulator" xcodebuild -workspace "${WORKSPACE_PATH}" -scheme "${TARGET_NAME}" -configuration ${CONFIGURATION} -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 8' ONLY_ACTIVE_ARCH=NO ARCHS='x86_64' BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" ENABLE_BITCODE=YES OTHER_CFLAGS="-fembed-bitcode" BITCODE_GENERATION_MODE=bitcode build # Step 1. Copy the framework structure (from iphoneos build) to the universal folder echo "Copying to output folder" cp -R "${BUILD_DIR}/${CONFIGURATION}-iphoneos/${FULL_PRODUCT_NAME}" "${UNIVERSAL_OUTPUTFOLDER}/" # Step 2. Copy Swift modules from iphonesimulator build (if it exists) to the copied framework directory SIMULATOR_SWIFT_MODULES_DIR="${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework/Modules/${TARGET_NAME}.swiftmodule/." if [ -d "${SIMULATOR_SWIFT_MODULES_DIR}" ]; then cp -R "${SIMULATOR_SWIFT_MODULES_DIR}" "${UNIVERSAL_OUTPUTFOLDER}/${TARGET_NAME}.framework/Modules/${TARGET_NAME}.swiftmodule" fi # Step 3. Create universal binary file using lipo and place the combined executable in the copied framework directory echo "Combining executables" lipo -create -output "${UNIVERSAL_OUTPUTFOLDER}/${EXECUTABLE_PATH}" "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${EXECUTABLE_PATH}" "${BUILD_DIR}/${CONFIGURATION}-iphoneos/${EXECUTABLE_PATH}" fi # Step 4. Convenience step to copy the framework to the project&apos;s directory echo "Copying to project dir&quot" yes | cp -Rf ${UNIVERSAL_OUTPUTFOLDER}/${FULL_PRODUCT_NAME} ${PROJECT_DIR} ">
|
||||
<EnvironmentBuildable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "0BD906E41EC0C00300C8C18E"
|
||||
BuildableName = "JitsiMeet.framework"
|
||||
BlueprintName = "JitsiMeet"
|
||||
ReferencedContainer = "container:sdk.xcodeproj">
|
||||
</BuildableReference>
|
||||
</EnvironmentBuildable>
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PostActions>
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.12.0</string>
|
||||
<string>2.11.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"kab": "Kabyle",
|
||||
"ko": "Korean",
|
||||
"lt": "Lithuanian",
|
||||
"lv": "Latvian",
|
||||
"nl": "Dutch",
|
||||
"oc": "Occitan",
|
||||
"pl": "Polish",
|
||||
|
||||
@@ -707,7 +707,7 @@
|
||||
"kick": "Teilnehmer entfernen",
|
||||
"lobbyButton": "Lobbymodus ein-/ausschalten",
|
||||
"localRecording": "Lokale Aufzeichnungssteuerelemente ein-/ausschalten",
|
||||
"lockRoom": "Konferenzpasswort ein-/ausschalten",
|
||||
"lockRoom": "Konferenzpasswort ein-/auschalten",
|
||||
"moreActions": "Menü „Weitere Aktionen“ ein-/ausschalten",
|
||||
"moreActionsMenu": "Menü „Weitere Aktionen“",
|
||||
"moreOptions": "Menü „Weitere Optionen“",
|
||||
|
||||
@@ -1,43 +1,28 @@
|
||||
{
|
||||
"addPeople": {
|
||||
"add": "Invita",
|
||||
"addContacts": "Invita tuoi contatti",
|
||||
"copyInvite": "Copia invito della riunione",
|
||||
"copyLink": "Copia collegamento della riunione",
|
||||
"copyStream": "Copia collegamento della diretta",
|
||||
"countryNotSupported": "Non supportiamo ancora questa destinazione.",
|
||||
"countryReminder": "Stai chiamando fuori dagli Stati Uniti? Assicurati d'inserire il prefisso internazionale!",
|
||||
"defaultEmail": "Tua Email di default",
|
||||
"disabled": "Non puoi invitare persone.",
|
||||
"failedToAdd": "L'aggiunta di nuove persone è fallita",
|
||||
"footerText": "La chiamata all'esterno è disabilitata.",
|
||||
"googleEmail": "Email Google",
|
||||
"inviteMoreHeader": "Sei l'unico presente alla riunione",
|
||||
"inviteMoreMailSubject": "Unisciti alla riunione {{appName}}",
|
||||
"inviteMorePrompt": "Invita altra gente",
|
||||
"linkCopied": "Collegamento copiato negli appunti",
|
||||
"loading": "Sto cercando persone e numeri di telefono",
|
||||
"loadingNumber": "Sto validando il numero di telefono",
|
||||
"loadingPeople": "Sto cercando le persone da invitare",
|
||||
"noResults": "Nessun risultato corrispondente",
|
||||
"noValidNumbers": "Per favore inserire un numero di telefono",
|
||||
"outlookEmail": "Email Outlook",
|
||||
"searchNumbers": "Aggiungi numeri di telefono",
|
||||
"searchPeople": "Cerca persone",
|
||||
"searchPeopleAndNumbers": "Cerca persone o aggiungi i loro numeri di telefono",
|
||||
"shareInvite": "Condividi invito alla riunione",
|
||||
"shareLink": "Condividi il collegamento alla riunione per invitare altri",
|
||||
"shareStream": "Condividi il collegamento alla diretta",
|
||||
"telephone": "Telefono: {{number}}",
|
||||
"title": "Invita persone a questa riunione",
|
||||
"yahooEmail": "Email Yahoo"
|
||||
"title": "Invita persone a questo meeting"
|
||||
},
|
||||
"audioDevices": {
|
||||
"bluetooth": "Bluetooth",
|
||||
"headphones": "Cuffie",
|
||||
"phone": "Telefono",
|
||||
"speaker": "Vivavoce",
|
||||
"none": "Nessun disposistivo audio esistente"
|
||||
"none": ""
|
||||
},
|
||||
"audioOnly": {
|
||||
"audioOnly": "Solo audio"
|
||||
@@ -62,25 +47,20 @@
|
||||
},
|
||||
"chat": {
|
||||
"error": "Errore: il tuo messaggio “{{originalText}}” non e’ stato inviato. Motivo: {{error}}",
|
||||
"fieldPlaceHolder": "Scrivi qui il tuo messaggio",
|
||||
"messagebox": "Digitare un messaggio",
|
||||
"messageTo": "Messaggio privato per {{recipient}}",
|
||||
"noMessagesMessage": "Non ci sono ancora messaggi nella riunione. Comincia una conversazione, qui!",
|
||||
"nickname": {
|
||||
"popover": "Scegli un nickname",
|
||||
"title": "Inserire un nickname per utilizzare la chat"
|
||||
},
|
||||
"privateNotice": "Messaggio privato per {{recipient}}",
|
||||
"title": "Chat",
|
||||
"you": "tu"
|
||||
},
|
||||
"chromeExtensionBanner": {
|
||||
"installExtensionText": "Installa un'estensione per integrare Google Calendar e Office 365",
|
||||
"buttonText": "Installa l'estensione Chrome",
|
||||
"dontShowAgain": "Non mostrare più questo messaggio"
|
||||
"you": "",
|
||||
"privateNotice": "",
|
||||
"noMessagesMessage": "",
|
||||
"messageTo": "",
|
||||
"fieldPlaceHolder": ""
|
||||
},
|
||||
"connectingOverlay": {
|
||||
"joiningRoom": "Collegamento alla riunione in corso…"
|
||||
"joiningRoom": "Collegamento al tuo meeting in corso…"
|
||||
},
|
||||
"connection": {
|
||||
"ATTACHED": "Collegato",
|
||||
@@ -92,27 +72,24 @@
|
||||
"DISCONNECTED": "Disconnesso",
|
||||
"DISCONNECTING": "Disconnessione in corso",
|
||||
"ERROR": "Errore",
|
||||
"FETCH_SESSION_ID": "Sto ottenendo ID di sessione...",
|
||||
"GET_SESSION_ID_ERROR": "Id dell'errore di sessione: {{code}}",
|
||||
"GOT_SESSION_ID": "Ricevuto ID di sessione",
|
||||
"LOW_BANDWIDTH": "Il video per {{displayName}} è stato interrotto per risparmiare banda"
|
||||
"RECONNECTING": "Si è verificato un problema di rete. Riconnessione...",
|
||||
"LOW_BANDWIDTH": "",
|
||||
"GOT_SESSION_ID": "",
|
||||
"GET_SESSION_ID_ERROR": "",
|
||||
"FETCH_SESSION_ID": ""
|
||||
},
|
||||
"connectionindicator": {
|
||||
"address": "Indirizzo:",
|
||||
"audio_ssrc": "Audio SSRC:",
|
||||
"bandwidth": "Banda stimata:",
|
||||
"bitrate": "Bitrate:",
|
||||
"bridgeCount": "Contatore server:",
|
||||
"codecs": "Codec (A/V): ",
|
||||
"connectedTo": "Connesso a:",
|
||||
"e2e_rtt": "E2E RTT:",
|
||||
"framerate": "Fotogrammi al secondo:",
|
||||
"less": "Mostra meno",
|
||||
"localaddress": "Indirizzo locale:",
|
||||
"localaddress_plural": "Indirizzi locali:",
|
||||
"localport": "Porta locale:",
|
||||
"localport_plural": "Porte locali:",
|
||||
"maxEnabledResolution": "manda max",
|
||||
"more": "Mostra di più",
|
||||
"packetloss": "Perdita pacchetti:",
|
||||
"quality": {
|
||||
@@ -127,12 +104,11 @@
|
||||
"remoteport": "Porta remota:",
|
||||
"remoteport_plural": "Porte remote:",
|
||||
"resolution": "Risoluzione:",
|
||||
"savelogs": "Salva log",
|
||||
"participant_id": "Id participante:",
|
||||
"status": "Connessione:",
|
||||
"transport": "Trasporto:",
|
||||
"transport_plural": "Trasporti:",
|
||||
"video_ssrc": "Video SSRC:"
|
||||
"turn": "(ruota)",
|
||||
"e2e_rtt": ""
|
||||
},
|
||||
"dateUtils": {
|
||||
"earlier": "Prima",
|
||||
@@ -142,7 +118,7 @@
|
||||
"deepLinking": {
|
||||
"appNotInstalled": "Per partecipare a questo meeting sul tuo telefono ti serve l'app mobile di {{app}}",
|
||||
"description": "Non è successo nulla? Abbiamo provato ad avviare la tua videoconferenza sull'app desktop di {{app}}. Prova di nuovo o avviala nell'app web di {{app}}.",
|
||||
"descriptionWithoutWeb": "Non è successo niente? Abbiamo provato ad avviare la riunione nell'app per desktop {{app}}",
|
||||
"descriptionWithoutWeb": "",
|
||||
"downloadApp": "Scarica l'app",
|
||||
"ifDoNotHaveApp": "Se non hai ancora l'app:",
|
||||
"ifHaveApp": "Se hai già l'app:",
|
||||
@@ -152,7 +128,6 @@
|
||||
"tryAgainButton": "Prova di nuovo sul desktop"
|
||||
},
|
||||
"defaultLink": "es. {{url}}",
|
||||
"defaultNickname": "es. Anna Rossi",
|
||||
"deviceError": {
|
||||
"cameraError": "Impossibile accedere alla videocamera",
|
||||
"cameraPermission": "Errore nell'ottenere i permessi per la videocamera",
|
||||
@@ -167,9 +142,8 @@
|
||||
},
|
||||
"dialog": {
|
||||
"accessibilityLabel": {
|
||||
"liveStreaming": "Diretta streaming"
|
||||
"liveStreaming": "Diretta"
|
||||
},
|
||||
"add": "Aggiungi",
|
||||
"allow": "Consenti",
|
||||
"alreadySharedVideoMsg": "Un altro utente sta condividendo un video. Questa conferenza permette di condividere un solo video alla volta.",
|
||||
"alreadySharedVideoTitle": "È permesso un solo video alla volta",
|
||||
@@ -195,26 +169,25 @@
|
||||
"connectErrorWithMsg": "Oops! Qualcosa è andato storto e non ti puoi collegare alla conferenza: {{msg}}",
|
||||
"connecting": "Connessione",
|
||||
"contactSupport": "Contatta il supporto",
|
||||
"copied": "Copiato",
|
||||
"copy": "Copia",
|
||||
"dismiss": "Scarta",
|
||||
"displayNameRequired": "Tutti devono avere un nome",
|
||||
"done": "Fatto",
|
||||
"e2eeDescription": "La crittografia punto-a-punto al momento è SPERIMENTALE. Tieni presente che attivandola disabiliterai i servizi svolti dal server, come: la registrazione su Dropobox, le dirette streaming e la partecipazione usando solo telefoni. Tieni anche presente che la riunione funzionerà solo per chi si collega usando browser che supportano flussi inseribili (insertable streams).",
|
||||
"e2eeLabel": "Attiva la crittografia punto-a-punto",
|
||||
"e2eeWarning": "ATTENZIONE: non tutti i partecipanti a questa riunione sembrano supportare le funzionalità di crittografia punto-a-punto. Se la attivi, non potranno sentirti, o vederti.",
|
||||
"enterDisplayName": "Inserisci qui il tuo nome",
|
||||
"enterDisplayName": "Inserisci il nome da visualizzare",
|
||||
"error": "Errore",
|
||||
"externalInstallationMsg": "Devi installare la nostra estensione per la condivisione desktop.",
|
||||
"externalInstallationTitle": "Richiesta estensione",
|
||||
"goToStore": "Vai al negozio on-line",
|
||||
"gracefulShutdown": "Il nostro servizio è al momento spento per manutenzione. Si prega di riprovare più tardi.",
|
||||
"grantModeratorDialog": "Sei sicuro di voler rendere moderatore questo partecipante?",
|
||||
"grantModeratorTitle": "Autorizza moderatore",
|
||||
"IamHost": "Sono l'organizzatore",
|
||||
"incorrectRoomLockPassword": "Password errata",
|
||||
"incorrectRoomLockPassword": "",
|
||||
"incorrectPassword": "Nome utente o password errati",
|
||||
"inlineInstallationMsg": "Devi installare la nostra estensione per la condivisione desktop.",
|
||||
"inlineInstallExtension": "Installa adesso",
|
||||
"internalError": "Ops! Qualcosa è andato storto. Questo è l'errore: {{error}}",
|
||||
"internalErrorTitle": "Errore interno",
|
||||
"kickMessage": "Acc! Sei stato espulso dal meeting!",
|
||||
"kickParticipantButton": "Butta fuori",
|
||||
"kickParticipantButton": "Espelli",
|
||||
"kickParticipantDialog": "Sei sicuro di voler espellere questo partecipante?",
|
||||
"kickParticipantTitle": "Espellere questi partecipante?",
|
||||
"kickTitle": "Espulso dal meeting",
|
||||
@@ -234,24 +207,17 @@
|
||||
"micNotSendingDataTitle": "Impossibile accedere al microfono",
|
||||
"micPermissionDeniedError": "Non hai concesso il permesso di usare il microfono. Puoi comunque partecipare alla conferenza ma gli altri non potranno sentirti. Usa il bottone a forma di telecamera nella barra degli indirizzi per cambiare impostazioni.",
|
||||
"micUnknownError": "Impossibile usare il microfono per un motivo sconosciuto.",
|
||||
"muteEveryoneElseDialog": "Una volta zittiti, non potrai riattivargli i microfoni, ma loro potranno farlo in qualsiasi momento.",
|
||||
"muteEveryoneElseTitle": "Zittisco tutti eccetto {{whom}}?",
|
||||
"muteEveryoneDialog": "Sei sicuro di voler zittire tutti? Non potrai riattivar loro il microfono, ma loro potranno farlo in qualsiasi momento.",
|
||||
"muteEveryoneTitle": "Zittisco tutti?",
|
||||
"muteEveryoneSelf": "te stesso",
|
||||
"muteEveryoneStartMuted": "Tutti cominciano a microfono spento, d'adessp in avanti",
|
||||
"muteParticipantBody": "Non sarai in grado di riattivare il loro microfono, ma loro potranno riattivarlo in qualsiasi momento.",
|
||||
"muteParticipantButton": "Zittisci",
|
||||
"muteParticipantDialog": "Sei sicuro di voler zittire questo partecipante? Saranno lui a doversi riattivare l'audio, per parlare.",
|
||||
"muteParticipantTitle": "Zittisco questo partecipante?",
|
||||
"Ok": "OK",
|
||||
"passwordLabel": "La riunione è stata bloccata da un partecipante. Immetti la $t(lockRoomPassword) per collegarti, per favore.",
|
||||
"passwordNotSupported": "Le password per le riunioni non sono supportate.",
|
||||
"passwordNotSupportedTitle": "$t(lockRoomPasswordUppercase) non supportato",
|
||||
"passwordRequired": "E' richiesto $t(lockRoomPasswordUppercase)",
|
||||
"popupError": "Il tuo browser sta bloccando i pop-up da questo sito. Per favore abilita i pop-up dalle impostazioni di sicurezza del browser e riprova.",
|
||||
"muteParticipantBody": "Tu non sarai in grado di riattivare il loro audio, ma loro potranno riattivarlo in qualsiasi momento.",
|
||||
"muteParticipantButton": "Silenzia partecipante",
|
||||
"muteParticipantDialog": "Sei sicuro di voler disattivare l'audio di questo partecipante? Saranno loro a doversi riattivare l'audio, per parlare.",
|
||||
"muteParticipantTitle": "Silenzio questo partecipante?",
|
||||
"Ok": "Ok",
|
||||
"passwordLabel": "",
|
||||
"passwordNotSupported": "Le password per le videoconferenze non sono supportate.",
|
||||
"passwordNotSupportedTitle": "",
|
||||
"passwordRequired": "",
|
||||
"popupError": "Il tuo browser sta bloccando i pop-up da questo sito. Per favore abilità i pop-up dalle impostazioni di sicurezza del browser e riprova.",
|
||||
"popupErrorTitle": "Pop-up bloccato",
|
||||
"readMore": "altro",
|
||||
"recording": "Registrazione",
|
||||
"recordingDisabledForGuestTooltip": "Gli ospiti non possono avviare una registrazione.",
|
||||
"recordingDisabledTooltip": "Registrazione disabilitata.",
|
||||
@@ -270,14 +236,11 @@
|
||||
"reservationError": "Errore di sistema in prenotazione",
|
||||
"reservationErrorMsg": "Codice di errore: {{code}}, messaggio: {{msg}}",
|
||||
"retry": "Riprova",
|
||||
"screenSharingAudio": "Condividi audio",
|
||||
"screenSharingFailed": "Ops! Non è stato possibile avviare la condivisione dello schermo!",
|
||||
"screenSharingFailedTitle": "Condivisione dello schermo fallita!",
|
||||
"screenSharingPermissionDeniedError": "Qualcosa non funziona nei permessi di condivisione dello schermo. Ricarica e prova ancora, autorizzando la condivisione.",
|
||||
"sendPrivateMessage": "Hai ricevuto un messaggio privato, poco fa. Vorresti rispondergli privatamente, o vuoi mandare la risposta al gruppo?",
|
||||
"sendPrivateMessageCancel": "Invia al gruppo",
|
||||
"sendPrivateMessageOk": "Invia privatamente",
|
||||
"sendPrivateMessageTitle": "Mando privatamente?",
|
||||
"screenSharingFailedToInstall": "Oh! Non è stato possibile installare l'estensione per la condivisione schermo.",
|
||||
"screenSharingFailedToInstallTitle": "Impossibile installare l'estensione per la condivisione schermo",
|
||||
"screenSharingFirefoxPermissionDeniedError": "Qualcosa è andato storto mentre cercavamo di condividere il tuo schermo. Assicurati di averci dato il premesso di condivisione.",
|
||||
"screenSharingFirefoxPermissionDeniedTitle": "Ops! Non siamo stati in grado di avviare la condivisione schermo!",
|
||||
"screenSharingPermissionDeniedError": "Oops! Qualcosa è andato storto con le impostazioni dell'estensione per la condivisione dello schermo. Ricarica la pagina e prova di nuovo.",
|
||||
"serviceUnavailable": "Servizio non disponibile",
|
||||
"sessTerminated": "Chiamata terminata",
|
||||
"Share": "Condividi",
|
||||
@@ -285,6 +248,7 @@
|
||||
"shareVideoTitle": "Condividi un video",
|
||||
"shareYourScreen": "Condividi schermo",
|
||||
"shareYourScreenDisabled": "Condivisione schermo disabilitata.",
|
||||
"shareYourScreenDisabledForGuest": "Gli ospiti non possono condividere lo schermo.",
|
||||
"startLiveStreaming": "Inizia una diretta",
|
||||
"startRecording": "Inizia a registrare",
|
||||
"startRemoteControlErrorMessage": "Si è verificato un errore cercando di avviare la sessione di controllo remoto!",
|
||||
@@ -305,19 +269,21 @@
|
||||
"WaitForHostMsgWOk": "La conferenza <b>{{room}}</b> non è ancora cominciata. Se sei l'organizzatore, allora premi OK per autenticarti. Altrimenti, aspetta l'arrivo dell'organizzatore.",
|
||||
"WaitingForHost": "In attesa dell'organizzatore ...",
|
||||
"Yes": "Sì",
|
||||
"yourEntireScreen": "Schermo intero"
|
||||
"yourEntireScreen": "Schermo intero",
|
||||
"sendPrivateMessageTitle": "",
|
||||
"sendPrivateMessageOk": "",
|
||||
"sendPrivateMessageCancel": "",
|
||||
"sendPrivateMessage": "",
|
||||
"screenSharingAudio": "",
|
||||
"muteEveryoneStartMuted": "",
|
||||
"muteEveryoneSelf": "",
|
||||
"muteEveryoneTitle": "",
|
||||
"muteEveryoneDialog": "",
|
||||
"muteEveryoneElseTitle": "",
|
||||
"muteEveryoneElseDialog": ""
|
||||
},
|
||||
"dialOut": {
|
||||
"statusMessage": "è {{status}}"
|
||||
},
|
||||
"documentSharing": {
|
||||
"title": "Documento condiviso"
|
||||
},
|
||||
"e2ee": {
|
||||
"labelToolTip": "Le comunicazioni audio e video di questa chiamata, sono crittografate dall'origine alla destinazione"
|
||||
},
|
||||
"embedMeeting": {
|
||||
"title": "Incorpora questa riunione altrove"
|
||||
"statusMessage": "è ora {{status}}"
|
||||
},
|
||||
"feedback": {
|
||||
"average": "Media",
|
||||
@@ -349,8 +315,8 @@
|
||||
"dialInTollFree": "Numero verde",
|
||||
"genericError": "Ops, qualcosa è andato storto.",
|
||||
"inviteLiveStream": "Per vedere la diretta di questo meeting, clicca su questo link: {{url}}",
|
||||
"invitePhone": "ATTENZIONE E' UNA CHIAMATA INTERNAZIONALE A PAGAMENTO! NON E' GRATUITA. Per seguire solo telefonicamente, clicca: {{number}},,{{conferenceID}}#",
|
||||
"invitePhoneAlternatives": "Cerchi un numero diverso da chiamare?\nEcco dei numeri telefonici per collegarsi alle riunioni: {{url}}\n\n\nSe entri in riunione anche col computer, entra senza attivare l'audio: {{silentUrl}}",
|
||||
"invitePhone": "Per seguire solo telefonicamente, clicca: {{number}},,{{conferenceID}}#",
|
||||
"invitePhoneAlternatives": "",
|
||||
"inviteURLFirstPartGeneral": "Invito a connettersi ad una conferenza.",
|
||||
"inviteURLFirstPartPersonal": "{{name}} ti sta invitando ad un meeting.\n",
|
||||
"inviteURLSecondPart": "\nPartecipa al meeting:\n{{url}}\n",
|
||||
@@ -366,12 +332,12 @@
|
||||
"label": "Informazioni meeting"
|
||||
},
|
||||
"inviteDialog": {
|
||||
"alertText": "Errore nell'invitare alcuni partecipanti.",
|
||||
"alertText": "",
|
||||
"header": "Invita",
|
||||
"searchCallOnlyPlaceholder": "Inserisci numero di telefono",
|
||||
"searchPeopleOnlyPlaceholder": "Cerca partecipanti",
|
||||
"searchPlaceholder": "Partecipante o numero di telefono",
|
||||
"send": "Invia"
|
||||
"searchPeopleOnlyPlaceholder": "",
|
||||
"searchPlaceholder": "",
|
||||
"send": ""
|
||||
},
|
||||
"inlineDialogFailure": {
|
||||
"msg": "Un piccolo inconveniente.",
|
||||
@@ -394,11 +360,9 @@
|
||||
"toggleScreensharing": "Cambia modalità tra videocamera e condivisione schermo",
|
||||
"toggleShortcuts": "Mostra o nascondi le scorciatoie",
|
||||
"videoMute": "Accendo o spegni la videocamera",
|
||||
"videoQuality": "Imposta qualità della telefonata"
|
||||
"videoQuality": ""
|
||||
},
|
||||
"liveStreaming": {
|
||||
"limitNotificationDescriptionWeb": "Data l'alta domanda la tua diretta sarà limitata a {{limit}} minuti. Per dirette illimitate, prova <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
|
||||
"limitNotificationDescriptionNative": "La tua diretta sarà limitata a {{limit}} minuti. Per dirette illimitate, prova {{app}}.",
|
||||
"busy": "Stiamo cercando di liberare risorse per la diretta. Riprova tra qualche minuto.",
|
||||
"busyTitle": "Tutti gli streamer sono impegnati al momento",
|
||||
"changeSignIn": "Cambia account",
|
||||
@@ -415,9 +379,7 @@
|
||||
"getStreamKeyManually": "Non siamo stati in grado di trovare nessuna trasmissione dal vivo. Prova ad ottenere una chiave stream da Youtube",
|
||||
"invalidStreamKey": "La chiave stream potrebbe non essere corretta.",
|
||||
"off": "La diretta si è interrotta",
|
||||
"offBy": "{{name}} ha fermato la diretta",
|
||||
"on": "Trasmissione in diretta",
|
||||
"onBy": "{{name}} ha iniziato la diretta",
|
||||
"pending": "Avvio diretta...",
|
||||
"serviceName": "Servizio live streaming",
|
||||
"signedInAs": "Sei attualmente collegato come:",
|
||||
@@ -427,8 +389,10 @@
|
||||
"start": "Inizia una diretta",
|
||||
"streamIdHelp": "Cos'è questo?",
|
||||
"unavailableTitle": "La diretta non è disponibile",
|
||||
"youtubeTerms": "YouTube terms of services",
|
||||
"googlePrivacyPolicy": "Google Privacy Policy"
|
||||
"onBy": "",
|
||||
"offBy": "",
|
||||
"googlePrivacyPolicy": "Google Privacy Policy",
|
||||
"youtubeTerms": "YouTube terms of services"
|
||||
},
|
||||
"localRecording": {
|
||||
"clientState": {
|
||||
@@ -479,84 +443,24 @@
|
||||
"muted": "Hai iniziato la conversazione con l'audio disattivato.",
|
||||
"mutedTitle": "Hai l'audio disattivato!",
|
||||
"mutedRemotelyTitle": "Ti è stato disattivato l'audio da {{participantDisplayName}}!",
|
||||
"mutedRemotelyDescription": "Puoi sempre attivare il microfono, quando vuoi parlare. Spegni il microfono quando hai finito, per non introdurre rumori di fondo nella riunione.",
|
||||
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) è stata tolta da un altro partecipante",
|
||||
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) è stata messa da un altro partecipante",
|
||||
"mutedRemotelyDescription": "",
|
||||
"passwordRemovedRemotely": "",
|
||||
"passwordSetRemotely": "",
|
||||
"raisedHand": "{{name}} vorrebbe intervenire.",
|
||||
"somebody": "Qualcuno",
|
||||
"startSilentTitle": "Sei entrato in riunione senza aver scelto un dispositivo audio per sentire!",
|
||||
"startSilentDescription": "Entra di nuovo in riunione, per attivare l'audio",
|
||||
"startSilentTitle": "",
|
||||
"startSilentDescription": "",
|
||||
"suboptimalExperienceDescription": "Ehm... temiamo che la tua esperienza con {{appName}} non sarà granché su questo browser. Stiamo cercando di migliorare la situazione ma, per il momento, prova ad utilizzare uno di questi <a href='{{recommendedBrowserPageLink}}' target='_blank'>browser supportati</a>.",
|
||||
"suboptimalExperienceTitle": "Problemi con il browser",
|
||||
"unmute": "Accendi microfono",
|
||||
"unmute": "",
|
||||
"newDeviceCameraTitle": "Trovata nuova videocamera",
|
||||
"newDeviceAudioTitle": "Trovata nuova origine audio",
|
||||
"newDeviceAction": "OK, usala",
|
||||
"OldElectronAPPTitle": "Falla di sicurezza!",
|
||||
"oldElectronClientDescription1": "Sembri stare usando una versione obsoleta di Jitsi Meet che ha delle falle di sicurezza note. Assicurati di aggiornarla presso il nostro ",
|
||||
"oldElectronClientDescription2": "ultima build",
|
||||
"oldElectronClientDescription3": " ora!"
|
||||
"newDeviceAction": "Usala",
|
||||
"suboptimalBrowserWarning": "Ci spiace che la tua videoconferenza non sarà ottimale, qui. Stiamo cercando modi per migliorare, ma fino ad allora per favore prova ad usare <a href='{{recommendedBrowserPageLink}}' target='_blank'>browser supportati completamente/a>."
|
||||
},
|
||||
"passwordSetRemotely": "definita da altro utente",
|
||||
"passwordDigitsOnly": "Fino a {{number}} cifre",
|
||||
"poweredby": "offerto da",
|
||||
"prejoin": {
|
||||
"audioAndVideoError": "Errore audio e video:",
|
||||
"audioDeviceProblem": "C'è un problema con il tuo microfono",
|
||||
"audioOnlyError": "Errore audio:",
|
||||
"audioTrackError": "Impossibile creare traccia audio.",
|
||||
"calling": "Chiamando",
|
||||
"callMe": "Chiamami",
|
||||
"callMeAtNumber": "Chiamami a questo numero:",
|
||||
"configuringDevices": "Configurazione dispositivi...",
|
||||
"connectedWithAudioQ": "Sei connesso con l'audio?",
|
||||
"connection": {
|
||||
"good": "La tua connessione Internet sembra buona!",
|
||||
"nonOptimal": "La tua connessione Internet non è ottimale",
|
||||
"poor": "La tua connessione Internet è scarsa"
|
||||
},
|
||||
"connectionDetails": {
|
||||
"audioClipping": "Ci aspettiamo che il tuo audio sarà a sighiozzo.",
|
||||
"audioHighQuality": "Ci aspettiamo che il tuo audio sarà di eccellente qualità.",
|
||||
"audioLowNoVideo": "Ci aspettiamo una bassa qualità audio e che il video sia assente.",
|
||||
"goodQuality": "Ottimo! La tue funzioni multimediali andranno alla grande.",
|
||||
"noMediaConnectivity": "Non siamo riusciti a stabilire una connessione multimediale per fare questo test. Questo è tipicamente causato da un firewall o dal NAT.",
|
||||
"noVideo": "Ci aspettiamo una pessima qualità video.",
|
||||
"undetectable": "Se non riesci ancora a fare chiamate nel browser, ti consigliamo di verificare che il microfono e la tua videocamera siano configurate correttamente, che tu abbia dato al browser i permessi di usare microfono e videocamera, e che il tuo browser sia aggiornato all'ultima versione. Se hai ancora problemi, dovresti contattare lo sviluppatore dell'applicazione web.",
|
||||
"veryPoorConnection": "Ci aspettiamo una qualità della chiamata davvero terribile.",
|
||||
"videoFreezing": "Ci aspettiamo che il video si blocchi, sparisca, o sia molto pixelato.",
|
||||
"videoHighQuality": "Ci aspettiamo che il video sia di buona qualità.",
|
||||
"videoLowQuality": "Ci aspettiamo che il video abbia pochi fotogrammi al secondo e sia a bassa risoluzione.",
|
||||
"videoTearing": "Ci aspettiamo che il video sia pixelato e abbia deformazioni visive."
|
||||
},
|
||||
"copyAndShare": "Copia e condividi il collegamento della riunione",
|
||||
"dialInMeeting": "Chiama e collegati alla riunione",
|
||||
"dialInPin": "Chiama e inserisci il PIN per entrare nella riunione:",
|
||||
"dialing": "Chiamata",
|
||||
"doNotShow": "Non mostrare più questa finestra",
|
||||
"errorDialOut": "Impossibile fare la chiamata",
|
||||
"errorDialOutDisconnected": "Impossibile fare la chiamata. Occupato",
|
||||
"errorDialOutFailed": "Impossibile fare la chiamata. Chiamata fallita",
|
||||
"errorDialOutStatus": "Errore nel ricevere lo stato della rete",
|
||||
"errorMissingName": "Inserire il proprio nome, per accedere alla riunione",
|
||||
"errorStatusCode": "Errore nella chiamata, codice: {{status}}",
|
||||
"errorValidation": "Numero inesistente",
|
||||
"iWantToDialIn": "Voglio chiamare il numero",
|
||||
"joinAudioByPhone": "Collegati usando un telefono, per parlare",
|
||||
"joinMeeting": "Collegati alla riunione",
|
||||
"joinWithoutAudio": "Collegati senza poter parlare",
|
||||
"initiated": "Chiamata avviata",
|
||||
"linkCopied": "Collegamento copiato negli appunti",
|
||||
"lookGood": "Sembra che il tuo microfono funzioni correttamente",
|
||||
"or": "o",
|
||||
"premeeting": "Attesa riunione",
|
||||
"showScreen": "Avvia la schermata d'attesa della riunione",
|
||||
"startWithPhone": "Avvia usando il telefono, per parlare",
|
||||
"screenSharingError": "Errore di condivisione dello schermo:",
|
||||
"videoOnlyError": "Errore video:",
|
||||
"videoTrackError": "Impossibile creare la traccia video.",
|
||||
"viewAllNumbers": "vedi tutti i numeri"
|
||||
},
|
||||
"presenceStatus": {
|
||||
"busy": "Occupato",
|
||||
"calling": "Chiamata…",
|
||||
@@ -577,10 +481,7 @@
|
||||
"setEmailLabel": "Imposta la mail gravatar",
|
||||
"title": "Profilo"
|
||||
},
|
||||
"raisedHand": "Vorrebbe parlare",
|
||||
"recording": {
|
||||
"limitNotificationDescriptionWeb": "Data l'alta domanda la tua registrazione sarà limitata a {{limit}} minuti. Per registrazioni illimitate, prova <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
|
||||
"limitNotificationDescriptionNative": "La tua registrazione sarà limitata a {{limit}} minuti. Per registrazioni illimitate, prova <3>{{app}}</3>.",
|
||||
"authDropboxText": "Carica su Dropbox",
|
||||
"availableSpace": "Spazio disponibile: {{spaceLeft}} MB (rimangono approssimativamente {{duration}} minuti di registrazione)",
|
||||
"beta": "BETA",
|
||||
@@ -591,31 +492,25 @@
|
||||
"expandedOn": "La registrazione della conferenza è attiva.",
|
||||
"expandedPending": "La registrazione è in fase di avvio…",
|
||||
"failedToStart": "Non è stato possibile avviare la registrazione",
|
||||
"fileSharingdescription": "Condividi la registrazione con i partecipanti alla riunione",
|
||||
"fileSharingdescription": "",
|
||||
"live": "DIRETTA",
|
||||
"loggedIn": "Accesso effettuato come {{userName}}",
|
||||
"off": "Registrazione interrotta",
|
||||
"offBy": "{{name}} registrazione fermata",
|
||||
"on": "Registrazione",
|
||||
"onBy": "{{name}} registrazione iniziata",
|
||||
"pending": "In preparazione alla registrazione della conferenza…",
|
||||
"rec": "REC",
|
||||
"serviceDescription": "La tua registrazione verrà salvata dal servizio di registrazione che hai scelto",
|
||||
"serviceDescription": "",
|
||||
"serviceName": "Servizio di registrazione",
|
||||
"signIn": "Entra",
|
||||
"signOut": "Esci",
|
||||
"unavailable": "Ops! Il {{serviceName}} non è al momento disponibile. Stiamo lavorando per risolvere il problema. Riprova più tardi.",
|
||||
"unavailableTitle": "Registrazione non disponibile"
|
||||
"unavailableTitle": "Registrazione non disponibile",
|
||||
"onBy": "{{name}} registrazione iniziata",
|
||||
"offBy": "{{name}} registrazione fermata"
|
||||
},
|
||||
"sectionList": {
|
||||
"pullToRefresh": "Trascina per aggiornare"
|
||||
},
|
||||
"security": {
|
||||
"about": "Puoi aggiungere alla riunione una $t(lockRoomPassword). I partecipanti dovranno fornire la $t(lockRoomPassword) per essere autorizzati a partecipare alla riunione.",
|
||||
"aboutReadOnly": "I moderatori della riunione possono aggiungere $t(lockRoomPassword). I partecipanti dovranno fornire la $t(lockRoomPassword) per essere autorizzati a partecipare alla riunione.",
|
||||
"insecureRoomNameWarning": "La riunione non è protetta. Dei partecipanti indesiderati potrebbero unirsi alla riunione. Puoi proteggere l'accesso alla riunione col bottone sicurezza.",
|
||||
"securityOptions": "Impostazioni sicurezza"
|
||||
},
|
||||
"settings": {
|
||||
"calendar": {
|
||||
"about": "L’integrazione del calendario con {{appName}} e’ consigliata per accedere in sicurezza al proprio calendario per poter leggere i prossimi appuntamenti ",
|
||||
@@ -642,26 +537,23 @@
|
||||
"microphones": "Microfoni"
|
||||
},
|
||||
"settingsView": {
|
||||
"advanced": "Avanzate",
|
||||
"alertOk": "OK",
|
||||
"alertCancel": "Annulla",
|
||||
"alertTitle": "Attenzione",
|
||||
"alertURLText": "L'URL del server inserito non è valido",
|
||||
"buildInfoSection": "Versione",
|
||||
"conferenceSection": "Conferenza",
|
||||
"disableCallIntegration": "Disattiva l'integrazione delle chiamate native",
|
||||
"disableP2P": "Disattiva la modalità punto-punto",
|
||||
"disableCrashReporting": "Disattiva la diagnostica dei crash",
|
||||
"disableCrashReportingWarning": "Sei sicuro di voler disattivare la diagnostica dei crash? Quest'impostazione verrà eseguita al prossimo avvio dell'app.",
|
||||
"displayName": "Nome visualizzato",
|
||||
"email": "Email",
|
||||
"header": "Impostazioni",
|
||||
"profileSection": "Profilo",
|
||||
"serverURL": "URL del server",
|
||||
"showAdvanced": "Impostazioni avanzate",
|
||||
"startWithAudioMuted": "Inizia con l'audio disattivato",
|
||||
"startWithVideoMuted": "Avvia con il video disattivato",
|
||||
"version": "Versione"
|
||||
"version": "Versione",
|
||||
"showAdvanced": "Mostra impostazioni avanzate",
|
||||
"disableP2P": "Disabilita modalità uno-a-uno",
|
||||
"disableCallIntegration": "Disabilita integrazione nativa delle chiamate",
|
||||
"advanced": "Avanzate"
|
||||
},
|
||||
"share": {
|
||||
"dialInfoText": "\n\n=====\n\nVuoi solo ascoltare la conferenza da un telefono?\n\n{{defaultDialInNumber}}Clicca questo link per vedere i numeri telefonici di questo meeting\n{{dialInfoPageUrl}}",
|
||||
@@ -673,13 +565,12 @@
|
||||
"minutes": "{{count}}m",
|
||||
"name": "Nome",
|
||||
"seconds": "{{count}}s",
|
||||
"speakerStats": "Statistiche",
|
||||
"speakerTime": "Tempo"
|
||||
"speakerStats": "Statistiche del relatore",
|
||||
"speakerTime": "Tempo del relatore"
|
||||
},
|
||||
"startupoverlay": {
|
||||
"policyText": " ",
|
||||
"genericTitle": "Per la riunione devono essere usati il tuo microfono e la tua videocamera.",
|
||||
"title": "{{app}} ha bisogno di usare il tuo microfono e la tua videocamera."
|
||||
"title": "{{app}} chiede di usare il tuo microfono e la tua videocamera."
|
||||
},
|
||||
"suspendedoverlay": {
|
||||
"rejoinKeyTitle": "Ricollegati",
|
||||
@@ -690,96 +581,74 @@
|
||||
"accessibilityLabel": {
|
||||
"audioOnly": "Attiva/disattiva solo audio",
|
||||
"audioRoute": "Scegli l'uscita audio",
|
||||
"callQuality": "Imposta qualità della chiamata",
|
||||
"callQuality": "Gestisci qualità della chiamata",
|
||||
"cc": "Attiva/disattiva sottotitoli",
|
||||
"chat": "Attiva/disattiva la chat",
|
||||
"document": "Attiva/disattiva documento condiviso",
|
||||
"download": "Scarica le nostre app",
|
||||
"embedMeeting": "Incorpora riunione altrove",
|
||||
"feedback": "Lascia un feedback",
|
||||
"fullScreen": "Attiva/disattiva schermo intero",
|
||||
"grantModerator": "Autorizza Moderator",
|
||||
"hangup": "Lascia la conferenza",
|
||||
"help": "Aiuto",
|
||||
"invite": "Invita persone",
|
||||
"kick": "Espelli partecipante",
|
||||
"lobbyButton": "Attiva/disattiva sala d'attesa",
|
||||
"localRecording": "Abilita controlli di registrazione locale",
|
||||
"lockRoom": "Attiva o disattiva password",
|
||||
"moreActions": "Attiva o disattiva menu avanzato",
|
||||
"moreActionsMenu": "Menu avanzato",
|
||||
"moreOptions": "Più opzioni",
|
||||
"mute": "Attiva/disattiva audio",
|
||||
"muteEveryone": "Zittisci tutti",
|
||||
"pip": "Attiva/disattiva immagine nell’immagine",
|
||||
"privateMessage": "Invia messaggio privato",
|
||||
"profile": "Modifica profilo",
|
||||
"raiseHand": "Attiva/disattiva alzata di mano",
|
||||
"recording": "Attiva/disattiva registrazione",
|
||||
"remoteMute": "Zittisci partecipante",
|
||||
"security": "Impostazioni sicurezza",
|
||||
"remoteMute": "Disattiva audio partecipante",
|
||||
"Settings": "Attiva/disattiva impostazioni",
|
||||
"sharedvideo": "Attiva/disattiva condivisione YouTube",
|
||||
"shareRoom": "Invita qualcuno",
|
||||
"shareYourScreen": "Attiva/disattiva condivisione schermo",
|
||||
"shortcuts": "Attiva/disattiva scorciatoie",
|
||||
"show": "Mostra in primo piano",
|
||||
"show": "",
|
||||
"speakerStats": "Attiva/disattiva statistiche relatore",
|
||||
"tileView": "Vedi tutti i partecipanti insieme, o uno solo",
|
||||
"toggleCamera": "Cambia videocamera",
|
||||
"toggleFilmstrip": "Attiva/disattiva pellicola",
|
||||
"videomute": "Attiva/disattiva videocamera",
|
||||
"videoblur": "Attiva/disattiva offuscamento video"
|
||||
"videoblur": "Attiva/disattiva offuscamento video",
|
||||
"privateMessage": "Invia messaggio privato",
|
||||
"muteEveryone": "Zittisci tutti",
|
||||
"moreOptions": "Mostra più opzioni",
|
||||
"help": "Aiuto",
|
||||
"download": "Scarica altre app"
|
||||
},
|
||||
"addPeople": "Aggiungi persone alla chiamata",
|
||||
"audioOnlyOff": "Anche video",
|
||||
"audioOnlyOn": "Solo audio",
|
||||
"audioRoute": "Scegli l'uscita audio",
|
||||
"authenticate": "Autenticazione",
|
||||
"callQuality": "Imposta qualità della chiamata",
|
||||
"callQuality": "Gestisci qualità della chiamata",
|
||||
"chat": "Apri / Chiudi chat",
|
||||
"closeChat": "Chiudi chat",
|
||||
"documentClose": "Chiudi documento condiviso",
|
||||
"documentOpen": "Apri documento condiviso",
|
||||
"download": "Scarica le nostre app",
|
||||
"e2ee": "Crittografia punto-punto",
|
||||
"embedMeeting": "Incorpora riunione altrove",
|
||||
"enterFullScreen": "Visualizza a schermo intero",
|
||||
"enterTileView": "Vedi tutti i partecipanti",
|
||||
"exitFullScreen": "Esci da schermo intero",
|
||||
"exitTileView": "Vedi una persona sola",
|
||||
"feedback": "Lascia un feedback",
|
||||
"hangup": "Butta giù",
|
||||
"help": "Aiuto",
|
||||
"hangup": "Esci",
|
||||
"invite": "Invita persone",
|
||||
"lobbyButtonDisable": "Disabilita sala d'attesa",
|
||||
"lobbyButtonEnable": "Abilita sala d'attesa",
|
||||
"login": "Login",
|
||||
"logout": "Logout",
|
||||
"lowerYourHand": "Abbassa la mano",
|
||||
"moreActions": "Più azioni",
|
||||
"moreOptions": "Più opzioni",
|
||||
"mute": "Attiva / Disattiva microfono",
|
||||
"muteEveryone": "Zittisci tutti",
|
||||
"noAudioSignalTitle": "Non arrivano suoni dal tuo microfono!",
|
||||
"noAudioSignalDesc": "Se non l'hai disabilitato intenzionalmente nelle impostazioni, prova a cambiare dispositivo di input.",
|
||||
"noAudioSignalDescSuggestion": "Se non l'hai disabilitato intenzionalmente nelle impostazioni, prova a scegliere il dispositivo consigliato.",
|
||||
"noAudioSignalDialInDesc": "Puoi anche chiamare usando:",
|
||||
"noAudioSignalDialInLinkDesc": "Numberi di telefono",
|
||||
"noisyAudioInputTitle": "Il tuo microfono sembra rumoroso!",
|
||||
"noisyAudioInputDesc": "Sembra che il tuo microfono faccia dei rumori, prova a spegnerlo o cambiarlo per favore.",
|
||||
"mute": "Microfono Attiva / Disattiva",
|
||||
"openChat": "Apri una chat",
|
||||
"pip": "Abilita visualizzazione immagine nell’immagine",
|
||||
"privateMessage": "invia un messaggio privato",
|
||||
"profile": "Modifica profilo",
|
||||
"raiseHand": "Alza / Abbassa la mano",
|
||||
"raiseYourHand": "Alza la mano",
|
||||
"security": "Impostazioni sicurezza",
|
||||
"Settings": "Impostazioni",
|
||||
"sharedvideo": "Condividi un video Youtube",
|
||||
"shareRoom": "Invita partecipante",
|
||||
"shortcuts": "Visualizza scorciatoie",
|
||||
"speakerStats": "Statistiche",
|
||||
"speakerStats": "Statistiche dell'interlocutore",
|
||||
"startScreenSharing": "Inizia la condivisione dello schermo",
|
||||
"startSubtitles": "Avvia sottotitoli",
|
||||
"stopScreenSharing": "Ferma la condivisione dello schermo",
|
||||
@@ -831,34 +700,31 @@
|
||||
"audioOnlyExpanded": "Hai attivato la modalità solo audio. Questa modalità permette di rispamiare banda, ma non vedrai gli altri partecipanti.",
|
||||
"callQuality": "Qualità video",
|
||||
"hd": "HD",
|
||||
"hdTooltip": "Stai vedendo in alta definizione",
|
||||
"highDefinition": "Alta definizione",
|
||||
"labelTooiltipNoVideo": "Nessun video",
|
||||
"labelTooltipAudioOnly": "Hai attivato la modalità solo audio",
|
||||
"ld": "LD",
|
||||
"ldTooltip": "Stai vedendo a bassa definizione",
|
||||
"lowDefinition": "Bassa definizione",
|
||||
"onlyAudioAvailable": "È disponibile solo l'audio",
|
||||
"onlyAudioSupported": "Per questo browser è supportato solo l'audio.",
|
||||
"sd": "SD",
|
||||
"sdTooltip": "Stai vedendo a definizione standard",
|
||||
"standardDefinition": "Definizione standard"
|
||||
},
|
||||
"videothumbnail": {
|
||||
"domute": "Disattiva audio",
|
||||
"domuteOthers": "Zittisci tutti gli altri",
|
||||
"flip": "Rifletti",
|
||||
"grantModerator": "Autorizza moderatore",
|
||||
"kick": "Butta fuori",
|
||||
"kick": "Espelli",
|
||||
"moderator": "Moderatore",
|
||||
"mute": "Il partecipante ha il microfono spento",
|
||||
"mute": "Il partecipante è in muto",
|
||||
"muted": "Audio disattivato",
|
||||
"remoteControl": "Controllo remoto",
|
||||
"show": "Mostra in primo piano",
|
||||
"videomute": "Il partecipante ha la videocamera spenta"
|
||||
"show": "",
|
||||
"videomute": "Silenzia il video"
|
||||
},
|
||||
"welcomepage": {
|
||||
"accessibilityLabel": {
|
||||
"join": "Tap per accedere",
|
||||
"roomname": "Inserisci nome stanza"
|
||||
"roomname": "Inserisci Nome Stanza"
|
||||
},
|
||||
"appDescription": "Avvia una videochiamata con tutto il team. Invita tutti quelli che conosci. {{app}} è una soluzione per effettuare videoconferenze totalmente crittografata, 100% open source, che puoi usare sempre, ogni giorno, gratuitamente – senza bisogno di un account.",
|
||||
"audioVideoSwitch": {
|
||||
@@ -867,68 +733,34 @@
|
||||
},
|
||||
"calendar": "Calendario",
|
||||
"connectCalendarButton": "Collega calendario",
|
||||
"connectCalendarText": "Connetti il tuo calendario per vedere tutte le riunione dentro {{app}}. Poi, aggiungi {{provider}} di riunioni al tuo calendario, per avviarle con un clic.",
|
||||
"connectCalendarText": "",
|
||||
"enterRoomTitle": "Avvia una nuova conferenza",
|
||||
"getHelp": "Trova aiuto",
|
||||
"go": "VAI",
|
||||
"goSmall": "VAI",
|
||||
"headerTitle": "Jitsi Meet",
|
||||
"headerSubtitle": "Secure and high quality meetings",
|
||||
"info": "Informazioni chiamata",
|
||||
"join": "UNISCITI",
|
||||
"jitsiOnMobile": "Jitsi su mobile – scarica le nostre app e dai inizio ad una riunione dovunque tu sia",
|
||||
"moderatedMessage": "O <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">prepara una URL</a> in anticipo, per le riunioni di cui sei il moderatore.",
|
||||
"info": "Informazioni",
|
||||
"privacy": "Privacy",
|
||||
"recentList": "Recente",
|
||||
"recentListDelete": "Cancella",
|
||||
"recentListEmpty": "La tua lista è vuota. Chatta con qualcuno del tuo team e lo vedrai apparire nella lista di meeting recenti.",
|
||||
"reducedUIText": "Benvenuto in {{app}}!",
|
||||
"roomNameAllowedChars": "Il nome della riunione non deve contenere questi caratteri: ?, &, :, ', \", %, #.",
|
||||
"roomname": "Inserisci nome stanza",
|
||||
"reducedUIText": "",
|
||||
"roomname": "Inserisci Nome Stanza",
|
||||
"roomnameHint": "Inserisci il nome o l'URL della stanza alla quale vuoi accedere. Puoi anche inventarti un nome, assicurati solo che le persone che vuoi contattare lo sappiano, così che possano inserire lo stesso nome.",
|
||||
"sendFeedback": "Invia feedback",
|
||||
"startMeeting": "Inizia riunione",
|
||||
"terms": "Termini di utilizzo",
|
||||
"title": "Il sistema di videoconferenza sicuro, funzionale e completamente gratuito."
|
||||
"title": "Il sistema di conferenza sicuro, funzionale e completamente gratuito."
|
||||
},
|
||||
"documentSharing": {
|
||||
"title": ""
|
||||
},
|
||||
"defaultNickname": "",
|
||||
"chromeExtensionBanner": {
|
||||
"dontShowAgain": "",
|
||||
"buttonText": "",
|
||||
"installExtensionText": ""
|
||||
},
|
||||
"raisedHand": "Vorrebbe parlare",
|
||||
"lonelyMeetingExperience": {
|
||||
"button": "invita altri",
|
||||
"button": "Invita altri",
|
||||
"youAreAlone": "Sei l'unico in riunione"
|
||||
},
|
||||
"helpView": {
|
||||
"header": "Aiuto"
|
||||
},
|
||||
"lobby": {
|
||||
"knockingParticipantList": "Lista dei partecipanti in attesa",
|
||||
"allow": "Autorizza",
|
||||
"backToKnockModeButton": "No password, ask to join instead",
|
||||
"dialogTitle": "Sala d'attesa",
|
||||
"disableDialogContent": "Sala d'attesa attiva. Questa funzione ti permette di non dare accesso alla riunione a partecipanti indesiderati. Vuoi disattivarla?",
|
||||
"disableDialogSubmit": "Disattiva",
|
||||
"emailField": "Inserisci il tuo indirizzo Email",
|
||||
"enableDialogPasswordField": "Imposta password (opzionale)",
|
||||
"enableDialogSubmit": "Attiva",
|
||||
"enableDialogText": "La sala d'attesa ti permette di proteggere la tua riunione concedendo l'ingresso solo alle persone autorizzate da un moderatore.",
|
||||
"enterPasswordButton": "Inserisci password riunione",
|
||||
"enterPasswordTitle": "Inserisci la password per entrare nella riunione",
|
||||
"invalidPassword": "Password errata",
|
||||
"joiningMessage": "Entrerai nella riunione, non appena qualcuno approva la tua richiesta",
|
||||
"joinWithPasswordMessage": "Ho inviato la password per entrare, attendi...",
|
||||
"joinRejectedMessage": "La tua richiesta d'accesso è stata respinta da un moderatore.",
|
||||
"joinTitle": "Entra in riunione",
|
||||
"joiningTitle": "Richiesta inviata...",
|
||||
"joiningWithPasswordTitle": "Entrando con la password...",
|
||||
"knockButton": "Chiedi d'entrare",
|
||||
"knockTitle": "Qualcuno vuole entrare in riunione",
|
||||
"nameField": "Scrivi il tuo nome",
|
||||
"notificationLobbyAccessDenied": "{{targetParticipantName}} è stato respinto da {{originParticipantName}}",
|
||||
"notificationLobbyAccessGranted": "{{targetParticipantName}} è stato autorizzato ad entrare da {{originParticipantName}}",
|
||||
"notificationLobbyDisabled": "La sala d'attesa è stata disattivata da {{originParticipantName}}",
|
||||
"notificationLobbyEnabled": "La sala d'attesa è stata attivata da {{originParticipantName}}",
|
||||
"notificationTitle": "Sala d'attesa",
|
||||
"passwordField": "Inserisci la password della riunione",
|
||||
"passwordJoinButton": "Entra",
|
||||
"reject": "Respingi",
|
||||
"toggleLabel": "Attiva sala d'attesa"
|
||||
}
|
||||
}
|
||||
|
||||
14
modules/API/external/electronPopupsConfig.json
vendored
Normal file
14
modules/API/external/electronPopupsConfig.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"google-auth": {
|
||||
"matchPatterns": {
|
||||
"url": "accounts.google.com"
|
||||
},
|
||||
"target": "electron"
|
||||
},
|
||||
"dropbox-auth": {
|
||||
"matchPatterns": {
|
||||
"url": "dropbox.com/oauth2/authorize"
|
||||
},
|
||||
"target": "electron"
|
||||
}
|
||||
}
|
||||
13
modules/API/external/external_api.js
vendored
13
modules/API/external/external_api.js
vendored
@@ -7,6 +7,7 @@ import {
|
||||
Transport
|
||||
} from '../../transport';
|
||||
|
||||
import electronPopupsConfig from './electronPopupsConfig.json';
|
||||
import {
|
||||
getAvailableDevices,
|
||||
getCurrentDevices,
|
||||
@@ -1069,4 +1070,16 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
stopRecording(mode) {
|
||||
this.executeCommand('startRecording', mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configuration for electron for the windows that are open
|
||||
* from Jitsi Meet.
|
||||
*
|
||||
* @returns {Promise<Object>}
|
||||
*
|
||||
* NOTE: For internal use only.
|
||||
*/
|
||||
_getElectronPopupsConfig() {
|
||||
return Promise.resolve(electronPopupsConfig);
|
||||
}
|
||||
}
|
||||
|
||||
106
modules/UI/UI.js
106
modules/UI/UI.js
@@ -59,14 +59,6 @@ UI.isFullScreen = function() {
|
||||
return UIUtil.isFullScreen();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if the etherpad window is currently visible.
|
||||
* @returns {Boolean} - true if the etherpad window is currently visible.
|
||||
*/
|
||||
UI.isEtherpadVisible = function() {
|
||||
return Boolean(etherpadManager && etherpadManager.isVisible());
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if there is a shared video which is being shown (?).
|
||||
* @returns {boolean} - true if there is a shared video which is being shown.
|
||||
@@ -305,44 +297,6 @@ UI.toggleFilmstrip = function() {
|
||||
*/
|
||||
UI.toggleChat = () => APP.store.dispatch(toggleChat());
|
||||
|
||||
/**
|
||||
* Handle new user display name.
|
||||
*/
|
||||
UI.inputDisplayNameHandler = function(newDisplayName) {
|
||||
eventEmitter.emit(UIEvents.NICKNAME_CHANGED, newDisplayName);
|
||||
};
|
||||
|
||||
// FIXME check if someone user this
|
||||
UI.showLoginPopup = function(callback) {
|
||||
logger.log('password is required');
|
||||
|
||||
const message
|
||||
= `<input name="username" type="text"
|
||||
placeholder="user@domain.net"
|
||||
class="input-control" autofocus>
|
||||
<input name="password" type="password"
|
||||
data-i18n="[placeholder]dialog.userPassword"
|
||||
class="input-control"
|
||||
placeholder="user password">`
|
||||
|
||||
;
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
const submitFunction = (e, v, m, f) => {
|
||||
if (v && f.username && f.password) {
|
||||
callback(f.username, f.password);
|
||||
}
|
||||
};
|
||||
|
||||
messageHandler.openTwoButtonDialog({
|
||||
titleKey: 'dialog.passwordRequired',
|
||||
msgString: message,
|
||||
leftButtonKey: 'dialog.Ok',
|
||||
submitFunction,
|
||||
focus: ':input:first'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets muted audio state for participant
|
||||
*/
|
||||
@@ -499,14 +453,6 @@ UI.notifyTokenAuthFailed = function() {
|
||||
});
|
||||
};
|
||||
|
||||
UI.notifyInternalError = function(error) {
|
||||
messageHandler.showError({
|
||||
descriptionArguments: { error },
|
||||
descriptionKey: 'dialog.internalError',
|
||||
titleKey: 'dialog.internalErrorTitle'
|
||||
});
|
||||
};
|
||||
|
||||
UI.notifyFocusDisconnected = function(focus, retrySec) {
|
||||
messageHandler.participantNotification(
|
||||
null, 'notify.focus',
|
||||
@@ -516,16 +462,6 @@ UI.notifyFocusDisconnected = function(focus, retrySec) {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Notifies interested listeners that the raise hand property has changed.
|
||||
*
|
||||
* @param {boolean} isRaisedHand indicates the current state of the
|
||||
* "raised hand"
|
||||
*/
|
||||
UI.onLocalRaiseHandChanged = function(isRaisedHand) {
|
||||
eventEmitter.emit(UIEvents.LOCAL_RAISE_HAND_CHANGED, isRaisedHand);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update list of available physical devices.
|
||||
*/
|
||||
@@ -585,48 +521,6 @@ UI.onSharedVideoStop = function(id, attributes) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles user's features changes.
|
||||
*/
|
||||
UI.onUserFeaturesChanged = user => VideoLayout.onUserFeaturesChanged(user);
|
||||
|
||||
/**
|
||||
* Returns the number of known remote videos.
|
||||
*
|
||||
* @returns {number} The number of remote videos.
|
||||
*/
|
||||
UI.getRemoteVideosCount = () => VideoLayout.getRemoteVideosCount();
|
||||
|
||||
/**
|
||||
* Returns the video type of the remote participant's video.
|
||||
* This is needed for the torture clients to determine the video type of the
|
||||
* remote participants.
|
||||
*
|
||||
* @param {string} participantID - The id of the remote participant.
|
||||
* @returns {string} The video type "camera" or "desktop".
|
||||
*/
|
||||
UI.getRemoteVideoType = participantID => VideoLayout.getRemoteVideoType(participantID);
|
||||
|
||||
/**
|
||||
* Sets the remote control active status for a remote participant.
|
||||
*
|
||||
* @param {string} participantID - The id of the remote participant.
|
||||
* @param {boolean} isActive - The new remote control active status.
|
||||
* @returns {void}
|
||||
*/
|
||||
UI.setRemoteControlActiveStatus = function(participantID, isActive) {
|
||||
VideoLayout.setRemoteControlActiveStatus(participantID, isActive);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the remote control active status for the local participant.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
UI.setLocalRemoteControlActiveChanged = function() {
|
||||
VideoLayout.setLocalRemoteControlActiveChanged();
|
||||
};
|
||||
|
||||
// TODO: Export every function separately. For now there is no point of doing
|
||||
// this because we are importing everything.
|
||||
export default UI;
|
||||
|
||||
@@ -6,48 +6,6 @@ import UIUtil from '../util/UIUtil';
|
||||
* Responsible for drawing audio levels.
|
||||
*/
|
||||
const AudioLevels = {
|
||||
/**
|
||||
* Fills the dot(s) with the specified "index", with as much opacity as
|
||||
* indicated by "opacity".
|
||||
*
|
||||
* @param {string} elementID the parent audio indicator span element
|
||||
* @param {number} index the index of the dots to fill, where 0 indicates
|
||||
* the middle dot and the following increments point toward the
|
||||
* corresponding pair of dots.
|
||||
* @param {number} opacity the opacity to set for the specified dot.
|
||||
*/
|
||||
_setDotLevel(elementID, index, opacity) {
|
||||
let audioSpan
|
||||
= document.getElementById(elementID)
|
||||
.getElementsByClassName('audioindicator');
|
||||
|
||||
// Make sure the audio span is still around.
|
||||
if (audioSpan && audioSpan.length > 0) {
|
||||
audioSpan = audioSpan[0];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const audioTopDots
|
||||
= audioSpan.getElementsByClassName('audiodot-top');
|
||||
const audioDotMiddle
|
||||
= audioSpan.getElementsByClassName('audiodot-middle');
|
||||
const audioBottomDots
|
||||
= audioSpan.getElementsByClassName('audiodot-bottom');
|
||||
|
||||
// First take care of the middle dot case.
|
||||
if (index === 0) {
|
||||
audioDotMiddle[0].style.opacity = opacity;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Index > 0 : we are setting non-middle dots.
|
||||
index--;// eslint-disable-line no-param-reassign
|
||||
audioBottomDots[index].style.opacity = opacity;
|
||||
audioTopDots[this.sideDotsCount - index - 1].style.opacity = opacity;
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the audio level of the large video.
|
||||
*
|
||||
|
||||
@@ -12,19 +12,10 @@ import { i18next } from '../../../react/features/base/i18n';
|
||||
import {
|
||||
JitsiParticipantConnectionStatus
|
||||
} from '../../../react/features/base/lib-jitsi-meet';
|
||||
import { MEDIA_TYPE } from '../../../react/features/base/media';
|
||||
import {
|
||||
getParticipantById,
|
||||
getPinnedParticipant,
|
||||
pinParticipant
|
||||
} from '../../../react/features/base/participants';
|
||||
import { isRemoteTrackMuted } from '../../../react/features/base/tracks';
|
||||
import { getParticipantById } from '../../../react/features/base/participants';
|
||||
import { PresenceLabel } from '../../../react/features/presence-status';
|
||||
import {
|
||||
REMOTE_CONTROL_MENU_STATES,
|
||||
RemoteVideoMenuTriggerButton
|
||||
} from '../../../react/features/remote-video-menu';
|
||||
import { LAYOUTS, getCurrentLayout } from '../../../react/features/video-layout';
|
||||
import { stopController, requestRemoteControl } from '../../../react/features/remote-control';
|
||||
import { RemoteVideoMenuTriggerButton } from '../../../react/features/remote-video-menu';
|
||||
/* eslint-enable no-unused-vars */
|
||||
import UIUtils from '../util/UIUtil';
|
||||
|
||||
@@ -81,7 +72,6 @@ export default class RemoteVideo extends SmallVideo {
|
||||
this.videoSpanId = `participant_${this.id}`;
|
||||
|
||||
this._audioStreamElement = null;
|
||||
this._supportsRemoteControl = false;
|
||||
this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left bottom' : 'top center';
|
||||
this.addRemoteVideoContainer();
|
||||
this.updateIndicators();
|
||||
@@ -89,7 +79,6 @@ export default class RemoteVideo extends SmallVideo {
|
||||
this.bindHoverHandler();
|
||||
this.flipX = false;
|
||||
this.isLocal = false;
|
||||
this._isRemoteControlSessionActive = false;
|
||||
|
||||
/**
|
||||
* The flag is set to <tt>true</tt> after the 'canplay' event has been
|
||||
@@ -103,10 +92,7 @@ export default class RemoteVideo extends SmallVideo {
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
// TODO The event handlers should be turned into actions so changes can be
|
||||
// handled through reducers and middleware.
|
||||
this._requestRemoteControlPermissions
|
||||
= this._requestRemoteControlPermissions.bind(this);
|
||||
this._setAudioVolume = this._setAudioVolume.bind(this);
|
||||
this._stopRemoteControl = this._stopRemoteControl.bind(this);
|
||||
|
||||
this.container.onclick = this._onContainerClick;
|
||||
}
|
||||
@@ -142,40 +128,11 @@ export default class RemoteVideo extends SmallVideo {
|
||||
return;
|
||||
}
|
||||
|
||||
const { controller } = APP.remoteControl;
|
||||
let remoteControlState = null;
|
||||
let onRemoteControlToggle;
|
||||
|
||||
if (this._supportsRemoteControl
|
||||
&& ((!APP.remoteControl.active && !this._isRemoteControlSessionActive)
|
||||
|| APP.remoteControl.controller.activeParticipant === this.id)) {
|
||||
if (controller.getRequestedParticipant() === this.id) {
|
||||
remoteControlState = REMOTE_CONTROL_MENU_STATES.REQUESTING;
|
||||
} else if (controller.isStarted()) {
|
||||
onRemoteControlToggle = this._stopRemoteControl;
|
||||
remoteControlState = REMOTE_CONTROL_MENU_STATES.STARTED;
|
||||
} else {
|
||||
onRemoteControlToggle = this._requestRemoteControlPermissions;
|
||||
remoteControlState = REMOTE_CONTROL_MENU_STATES.NOT_STARTED;
|
||||
}
|
||||
}
|
||||
|
||||
const initialVolumeValue = this._audioStreamElement && this._audioStreamElement.volume;
|
||||
|
||||
// hide volume when in silent mode
|
||||
const onVolumeChange
|
||||
= APP.store.getState()['features/base/config'].startSilent ? undefined : this._setAudioVolume;
|
||||
const participantID = this.id;
|
||||
const currentLayout = getCurrentLayout(APP.store.getState());
|
||||
let remoteMenuPosition;
|
||||
|
||||
if (currentLayout === LAYOUTS.TILE_VIEW) {
|
||||
remoteMenuPosition = 'left top';
|
||||
} else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
|
||||
remoteMenuPosition = 'left bottom';
|
||||
} else {
|
||||
remoteMenuPosition = 'top center';
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
@@ -183,13 +140,10 @@ export default class RemoteVideo extends SmallVideo {
|
||||
<AtlasKitThemeProvider mode = 'dark'>
|
||||
<RemoteVideoMenuTriggerButton
|
||||
initialVolumeValue = { initialVolumeValue }
|
||||
menuPosition = { remoteMenuPosition }
|
||||
onMenuDisplay
|
||||
= {this._onRemoteVideoMenuDisplay.bind(this)}
|
||||
onRemoteControlToggle = { onRemoteControlToggle }
|
||||
onVolumeChange = { onVolumeChange }
|
||||
participantID = { participantID }
|
||||
remoteControlState = { remoteControlState } />
|
||||
participantID = { this.id } />
|
||||
</AtlasKitThemeProvider>
|
||||
</I18nextProvider>
|
||||
</Provider>,
|
||||
@@ -203,76 +157,6 @@ export default class RemoteVideo extends SmallVideo {
|
||||
this.updateRemoteVideoMenu();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the remote control active status for the remote video.
|
||||
*
|
||||
* @param {boolean} isActive - The new remote control active status.
|
||||
* @returns {void}
|
||||
*/
|
||||
setRemoteControlActiveStatus(isActive) {
|
||||
this._isRemoteControlSessionActive = isActive;
|
||||
this.updateRemoteVideoMenu();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the remote control supported value and initializes or updates the menu
|
||||
* depending on the remote control is supported or not.
|
||||
* @param {boolean} isSupported
|
||||
*/
|
||||
setRemoteControlSupport(isSupported = false) {
|
||||
if (this._supportsRemoteControl === isSupported) {
|
||||
return;
|
||||
}
|
||||
this._supportsRemoteControl = isSupported;
|
||||
this.updateRemoteVideoMenu();
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests permissions for remote control session.
|
||||
*/
|
||||
_requestRemoteControlPermissions() {
|
||||
APP.remoteControl.controller.requestPermissions(this.id, this.VideoLayout.getLargeVideoWrapper())
|
||||
.then(result => {
|
||||
if (result === null) {
|
||||
return;
|
||||
}
|
||||
this.updateRemoteVideoMenu();
|
||||
APP.UI.messageHandler.notify(
|
||||
'dialog.remoteControlTitle',
|
||||
result === false ? 'dialog.remoteControlDeniedMessage' : 'dialog.remoteControlAllowedMessage',
|
||||
{ user: this.user.getDisplayName() || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME }
|
||||
);
|
||||
if (result === true) {
|
||||
// the remote control permissions has been granted
|
||||
// pin the controlled participant
|
||||
const pinnedParticipant = getPinnedParticipant(APP.store.getState()) || {};
|
||||
const pinnedId = pinnedParticipant.id;
|
||||
|
||||
if (pinnedId !== this.id) {
|
||||
APP.store.dispatch(pinParticipant(this.id));
|
||||
}
|
||||
}
|
||||
}, error => {
|
||||
logger.error(error);
|
||||
this.updateRemoteVideoMenu();
|
||||
APP.UI.messageHandler.notify(
|
||||
'dialog.remoteControlTitle',
|
||||
'dialog.remoteControlErrorMessage',
|
||||
{ user: this.user.getDisplayName() || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME }
|
||||
);
|
||||
});
|
||||
this.updateRemoteVideoMenu();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops remote control session.
|
||||
*/
|
||||
_stopRemoteControl() {
|
||||
// send message about stopping
|
||||
APP.remoteControl.controller.stop();
|
||||
this.updateRemoteVideoMenu();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the remote participant's volume level.
|
||||
*
|
||||
|
||||
@@ -293,7 +293,6 @@ const VideoLayout = {
|
||||
const jitsiParticipant = APP.conference.getParticipantById(id);
|
||||
const remoteVideo = new RemoteVideo(jitsiParticipant, VideoLayout);
|
||||
|
||||
this._setRemoteControlProperties(jitsiParticipant, remoteVideo);
|
||||
this.addRemoteVideoContainer(id, remoteVideo);
|
||||
|
||||
this.updateMutedForNoTracks(id, 'audio');
|
||||
@@ -654,33 +653,6 @@ const VideoLayout = {
|
||||
this.localFlipX = val;
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles user's features changes.
|
||||
*/
|
||||
onUserFeaturesChanged(user) {
|
||||
const video = this.getSmallVideo(user.getId());
|
||||
|
||||
if (!video) {
|
||||
return;
|
||||
}
|
||||
this._setRemoteControlProperties(user, video);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the remote control properties (checks whether remote control
|
||||
* is supported and executes remoteVideo.setRemoteControlSupport).
|
||||
* @param {JitsiParticipant} user the user that will be checked for remote
|
||||
* control support.
|
||||
* @param {RemoteVideo} remoteVideo the remoteVideo on which the properties
|
||||
* will be set.
|
||||
*/
|
||||
_setRemoteControlProperties(user, remoteVideo) {
|
||||
APP.remoteControl.checkUserRemoteControlSupport(user)
|
||||
.then(result => remoteVideo.setRemoteControlSupport(result))
|
||||
.catch(error =>
|
||||
logger.warn(`could not get remote control properties for: ${user.getJid()}`, error));
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the wrapper jquery selector for the largeVideo
|
||||
* @returns {JQuerySelector} the wrapper jquery selector for the largeVideo
|
||||
@@ -698,28 +670,6 @@ const VideoLayout = {
|
||||
return Object.keys(remoteVideos).length;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the remote control active status for a remote participant.
|
||||
*
|
||||
* @param {string} participantID - The id of the remote participant.
|
||||
* @param {boolean} isActive - The new remote control active status.
|
||||
* @returns {void}
|
||||
*/
|
||||
setRemoteControlActiveStatus(participantID, isActive) {
|
||||
remoteVideos[participantID].setRemoteControlActiveStatus(isActive);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the remote control active status for the local participant.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
setLocalRemoteControlActiveChanged() {
|
||||
Object.values(remoteVideos).forEach(
|
||||
remoteVideo => remoteVideo.updateRemoteVideoMenu()
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper method to invoke when the video layout has changed and elements
|
||||
* have to be re-arranged and resized.
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
'extends': '../../react/.eslintrc.js'
|
||||
};
|
||||
@@ -1,474 +0,0 @@
|
||||
/* @flow */
|
||||
|
||||
import { getLogger } from 'jitsi-meet-logger';
|
||||
|
||||
import {
|
||||
JitsiConferenceEvents
|
||||
} from '../../react/features/base/lib-jitsi-meet';
|
||||
import UIEvents from '../../service/UI/UIEvents';
|
||||
import {
|
||||
EVENTS,
|
||||
PERMISSIONS_ACTIONS,
|
||||
REMOTE_CONTROL_MESSAGE_NAME
|
||||
} from '../../service/remotecontrol/Constants';
|
||||
import * as RemoteControlEvents
|
||||
from '../../service/remotecontrol/RemoteControlEvents';
|
||||
import * as KeyCodes from '../keycode/keycode';
|
||||
|
||||
import RemoteControlParticipant from './RemoteControlParticipant';
|
||||
|
||||
declare var $: Function;
|
||||
declare var APP: Object;
|
||||
|
||||
const logger = getLogger(__filename);
|
||||
|
||||
/**
|
||||
* Extract the keyboard key from the keyboard event.
|
||||
*
|
||||
* @param {KeyboardEvent} event - The event.
|
||||
* @returns {KEYS} The key that is pressed or undefined.
|
||||
*/
|
||||
function getKey(event) {
|
||||
return KeyCodes.keyboardEventToKey(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the modifiers from the keyboard event.
|
||||
*
|
||||
* @param {KeyboardEvent} event - The event.
|
||||
* @returns {Array} With possible values: "shift", "control", "alt", "command".
|
||||
*/
|
||||
function getModifiers(event) {
|
||||
const modifiers = [];
|
||||
|
||||
if (event.shiftKey) {
|
||||
modifiers.push('shift');
|
||||
}
|
||||
|
||||
if (event.ctrlKey) {
|
||||
modifiers.push('control');
|
||||
}
|
||||
|
||||
|
||||
if (event.altKey) {
|
||||
modifiers.push('alt');
|
||||
}
|
||||
|
||||
if (event.metaKey) {
|
||||
modifiers.push('command');
|
||||
}
|
||||
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class represents the controller party for a remote controller session.
|
||||
* It listens for mouse and keyboard events and sends them to the receiver
|
||||
* party of the remote control session.
|
||||
*/
|
||||
export default class Controller extends RemoteControlParticipant {
|
||||
_area: ?Object;
|
||||
_controlledParticipant: string | null;
|
||||
_isCollectingEvents: boolean;
|
||||
_largeVideoChangedListener: Function;
|
||||
_requestedParticipant: string | null;
|
||||
_stopListener: Function;
|
||||
_userLeftListener: Function;
|
||||
|
||||
/**
|
||||
* Creates new instance.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this._isCollectingEvents = false;
|
||||
this._controlledParticipant = null;
|
||||
this._requestedParticipant = null;
|
||||
this._stopListener = this._handleRemoteControlStoppedEvent.bind(this);
|
||||
this._userLeftListener = this._onUserLeft.bind(this);
|
||||
this._largeVideoChangedListener
|
||||
= this._onLargeVideoIdChanged.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current active participant's id.
|
||||
*
|
||||
* @returns {string|null} - The id of the current active participant.
|
||||
*/
|
||||
get activeParticipant(): string | null {
|
||||
return this._requestedParticipant || this._controlledParticipant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests permissions from the remote control receiver side.
|
||||
*
|
||||
* @param {string} userId - The user id of the participant that will be
|
||||
* requested.
|
||||
* @param {JQuerySelector} eventCaptureArea - The area that is going to be
|
||||
* used mouse and keyboard event capture.
|
||||
* @returns {Promise<boolean>} Resolve values - true(accept), false(deny),
|
||||
* null(the participant has left).
|
||||
*/
|
||||
requestPermissions(
|
||||
userId: string,
|
||||
eventCaptureArea: Object
|
||||
): Promise<boolean | null> {
|
||||
if (!this._enabled) {
|
||||
return Promise.reject(new Error('Remote control is disabled!'));
|
||||
}
|
||||
this.emit(RemoteControlEvents.ACTIVE_CHANGED, true);
|
||||
this._area = eventCaptureArea;// $("#largeVideoWrapper")
|
||||
logger.log(`Requsting remote control permissions from: ${userId}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let onUserLeft, permissionsReplyListener;
|
||||
|
||||
const clearRequest = () => {
|
||||
this._requestedParticipant = null;
|
||||
APP.conference.removeConferenceListener(
|
||||
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
|
||||
permissionsReplyListener);
|
||||
APP.conference.removeConferenceListener(
|
||||
JitsiConferenceEvents.USER_LEFT,
|
||||
onUserLeft);
|
||||
};
|
||||
|
||||
permissionsReplyListener = (participant, event) => {
|
||||
let result = null;
|
||||
|
||||
try {
|
||||
result = this._handleReply(participant, event);
|
||||
} catch (e) {
|
||||
clearRequest();
|
||||
this.emit(RemoteControlEvents.ACTIVE_CHANGED, false);
|
||||
reject(e);
|
||||
}
|
||||
if (result !== null) {
|
||||
clearRequest();
|
||||
if (result === false) {
|
||||
this.emit(RemoteControlEvents.ACTIVE_CHANGED, false);
|
||||
}
|
||||
resolve(result);
|
||||
}
|
||||
};
|
||||
onUserLeft = id => {
|
||||
if (id === this._requestedParticipant) {
|
||||
clearRequest();
|
||||
this.emit(RemoteControlEvents.ACTIVE_CHANGED, false);
|
||||
resolve(null);
|
||||
}
|
||||
};
|
||||
|
||||
APP.conference.addConferenceListener(
|
||||
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
|
||||
permissionsReplyListener);
|
||||
APP.conference.addConferenceListener(
|
||||
JitsiConferenceEvents.USER_LEFT,
|
||||
onUserLeft);
|
||||
this._requestedParticipant = userId;
|
||||
this.sendRemoteControlEndpointMessage(
|
||||
userId,
|
||||
{
|
||||
type: EVENTS.permissions,
|
||||
action: PERMISSIONS_ACTIONS.request
|
||||
},
|
||||
e => {
|
||||
clearRequest();
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the reply of the permissions request.
|
||||
*
|
||||
* @param {JitsiParticipant} participant - The participant that has sent the
|
||||
* reply.
|
||||
* @param {RemoteControlEvent} event - The remote control event.
|
||||
* @returns {boolean|null}
|
||||
*/
|
||||
_handleReply(participant: Object, event: Object) {
|
||||
const userId = participant.getId();
|
||||
|
||||
if (this._enabled
|
||||
&& event.name === REMOTE_CONTROL_MESSAGE_NAME
|
||||
&& event.type === EVENTS.permissions
|
||||
&& userId === this._requestedParticipant) {
|
||||
if (event.action !== PERMISSIONS_ACTIONS.grant) {
|
||||
this._area = undefined;
|
||||
}
|
||||
switch (event.action) {
|
||||
case PERMISSIONS_ACTIONS.grant: {
|
||||
this._controlledParticipant = userId;
|
||||
logger.log('Remote control permissions granted to:', userId);
|
||||
this._start();
|
||||
|
||||
return true;
|
||||
}
|
||||
case PERMISSIONS_ACTIONS.deny:
|
||||
return false;
|
||||
case PERMISSIONS_ACTIONS.error:
|
||||
throw new Error('Error occurred on receiver side');
|
||||
default:
|
||||
throw new Error('Unknown reply received!');
|
||||
}
|
||||
} else {
|
||||
// different message type or another user -> ignoring the message
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles remote control stopped.
|
||||
*
|
||||
* @param {JitsiParticipant} participant - The participant that has sent the
|
||||
* event.
|
||||
* @param {Object} event - EndpointMessage event from the data channels.
|
||||
* @property {string} type - The function process only events with
|
||||
* name REMOTE_CONTROL_MESSAGE_NAME.
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleRemoteControlStoppedEvent(participant: Object, event: Object) {
|
||||
if (this._enabled
|
||||
&& event.name === REMOTE_CONTROL_MESSAGE_NAME
|
||||
&& event.type === EVENTS.stop
|
||||
&& participant.getId() === this._controlledParticipant) {
|
||||
this._stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts processing the mouse and keyboard events. Sets conference
|
||||
* listeners. Disables keyboard events.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_start() {
|
||||
logger.log('Starting remote control controller.');
|
||||
APP.UI.addListener(UIEvents.LARGE_VIDEO_ID_CHANGED,
|
||||
this._largeVideoChangedListener);
|
||||
APP.conference.addConferenceListener(
|
||||
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
|
||||
this._stopListener);
|
||||
APP.conference.addConferenceListener(JitsiConferenceEvents.USER_LEFT,
|
||||
this._userLeftListener);
|
||||
this.resume();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the keyboatd shortcuts. Starts collecting remote control
|
||||
* events. It can be used to resume an active remote control session wchich
|
||||
* was paused with this.pause().
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
resume() {
|
||||
let area;
|
||||
|
||||
if (!this._enabled
|
||||
|| this._isCollectingEvents
|
||||
|| !(area = this._area)) {
|
||||
return;
|
||||
}
|
||||
logger.log('Resuming remote control controller.');
|
||||
this._isCollectingEvents = true;
|
||||
APP.keyboardshortcut.enable(false);
|
||||
|
||||
area.mousemove(event => {
|
||||
const area = this._area; // eslint-disable-line no-shadow
|
||||
|
||||
if (!area) {
|
||||
return;
|
||||
}
|
||||
|
||||
const position = area.position();
|
||||
|
||||
this.sendRemoteControlEndpointMessage(this._controlledParticipant, {
|
||||
type: EVENTS.mousemove,
|
||||
x: (event.pageX - position.left) / area.width(),
|
||||
y: (event.pageY - position.top) / area.height()
|
||||
});
|
||||
});
|
||||
|
||||
area.mousedown(this._onMouseClickHandler.bind(this, EVENTS.mousedown));
|
||||
area.mouseup(this._onMouseClickHandler.bind(this, EVENTS.mouseup));
|
||||
|
||||
area.dblclick(
|
||||
this._onMouseClickHandler.bind(this, EVENTS.mousedblclick));
|
||||
|
||||
area.contextmenu(() => false);
|
||||
|
||||
area[0].onmousewheel = event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.sendRemoteControlEndpointMessage(this._controlledParticipant, {
|
||||
type: EVENTS.mousescroll,
|
||||
x: event.deltaX,
|
||||
y: event.deltaY
|
||||
});
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
$(window).keydown(this._onKeyPessHandler.bind(this,
|
||||
EVENTS.keydown));
|
||||
$(window).keyup(this._onKeyPessHandler.bind(this, EVENTS.keyup));
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops processing the mouse and keyboard events. Removes added listeners.
|
||||
* Enables the keyboard shortcuts. Displays dialog to notify the user that
|
||||
* remote control session has ended.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_stop() {
|
||||
if (!this._controlledParticipant) {
|
||||
return;
|
||||
}
|
||||
logger.log('Stopping remote control controller.');
|
||||
APP.UI.removeListener(UIEvents.LARGE_VIDEO_ID_CHANGED,
|
||||
this._largeVideoChangedListener);
|
||||
APP.conference.removeConferenceListener(
|
||||
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
|
||||
this._stopListener);
|
||||
APP.conference.removeConferenceListener(JitsiConferenceEvents.USER_LEFT,
|
||||
this._userLeftListener);
|
||||
this.pause();
|
||||
this._controlledParticipant = null;
|
||||
this._area = undefined;
|
||||
this.emit(RemoteControlEvents.ACTIVE_CHANGED, false);
|
||||
APP.UI.messageHandler.notify(
|
||||
'dialog.remoteControlTitle',
|
||||
'dialog.remoteControlStopMessage'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes this._stop() mehtod which stops processing the mouse and
|
||||
* keyboard events, removes added listeners, enables the keyboard shortcuts,
|
||||
* displays dialog to notify the user that remote control session has ended.
|
||||
* In addition sends stop message to the controlled participant.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
stop() {
|
||||
if (!this._controlledParticipant) {
|
||||
return;
|
||||
}
|
||||
this.sendRemoteControlEndpointMessage(this._controlledParticipant, {
|
||||
type: EVENTS.stop
|
||||
});
|
||||
this._stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the collecting of events and enables the keyboard shortcus. But
|
||||
* it doesn't removes any other listeners. Basically the remote control
|
||||
* session will be still active after this.pause(), but no events from the
|
||||
* controller side will be captured and sent. You can resume the collecting
|
||||
* of the events with this.resume().
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
pause() {
|
||||
if (!this._controlledParticipant) {
|
||||
return;
|
||||
}
|
||||
logger.log('Pausing remote control controller.');
|
||||
this._isCollectingEvents = false;
|
||||
APP.keyboardshortcut.enable(true);
|
||||
|
||||
const area = this._area;
|
||||
|
||||
if (area) {
|
||||
area.off('contextmenu');
|
||||
area.off('dblclick');
|
||||
area.off('mousedown');
|
||||
area.off('mousemove');
|
||||
area.off('mouseup');
|
||||
|
||||
area[0].onmousewheel = undefined;
|
||||
}
|
||||
|
||||
$(window).off('keydown');
|
||||
$(window).off('keyup');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for mouse click events.
|
||||
*
|
||||
* @param {string} type - The type of event ("mousedown"/"mouseup").
|
||||
* @param {Event} event - The mouse event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onMouseClickHandler(type: string, event: Object) {
|
||||
this.sendRemoteControlEndpointMessage(this._controlledParticipant, {
|
||||
type,
|
||||
button: event.which
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the remote control session is started.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isStarted() {
|
||||
return this._controlledParticipant !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the id of the requested participant.
|
||||
*
|
||||
* @returns {string} The id of the requested participant.
|
||||
* NOTE: This id should be the result of JitsiParticipant.getId() call.
|
||||
*/
|
||||
getRequestedParticipant() {
|
||||
return this._requestedParticipant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for key press events.
|
||||
*
|
||||
* @param {string} type - The type of event ("keydown"/"keyup").
|
||||
* @param {Event} event - The key event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onKeyPessHandler(type: string, event: Object) {
|
||||
this.sendRemoteControlEndpointMessage(this._controlledParticipant, {
|
||||
type,
|
||||
key: getKey(event),
|
||||
modifiers: getModifiers(event)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the stop method if the other side have left.
|
||||
*
|
||||
* @param {string} id - The user id for the participant that have left.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onUserLeft(id: string) {
|
||||
if (this._controlledParticipant === id) {
|
||||
this._stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles changes of the participant displayed on the large video.
|
||||
*
|
||||
* @param {string} id - The user id for the participant that is displayed.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onLargeVideoIdChanged(id: string) {
|
||||
if (!this._controlledParticipant) {
|
||||
return;
|
||||
}
|
||||
if (this._controlledParticipant === id) {
|
||||
this.resume();
|
||||
} else {
|
||||
this.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,331 +0,0 @@
|
||||
/* @flow */
|
||||
|
||||
import { getLogger } from 'jitsi-meet-logger';
|
||||
|
||||
import * as JitsiMeetConferenceEvents from '../../ConferenceEvents';
|
||||
import {
|
||||
JitsiConferenceEvents
|
||||
} from '../../react/features/base/lib-jitsi-meet';
|
||||
import {
|
||||
openRemoteControlAuthorizationDialog
|
||||
} from '../../react/features/remote-control';
|
||||
import {
|
||||
DISCO_REMOTE_CONTROL_FEATURE,
|
||||
EVENTS,
|
||||
PERMISSIONS_ACTIONS,
|
||||
REMOTE_CONTROL_MESSAGE_NAME,
|
||||
REQUESTS
|
||||
} from '../../service/remotecontrol/Constants';
|
||||
import * as RemoteControlEvents
|
||||
from '../../service/remotecontrol/RemoteControlEvents';
|
||||
import { Transport, PostMessageTransportBackend } from '../transport';
|
||||
|
||||
import RemoteControlParticipant from './RemoteControlParticipant';
|
||||
|
||||
declare var APP: Object;
|
||||
declare var config: Object;
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
const logger = getLogger(__filename);
|
||||
|
||||
/**
|
||||
* The transport instance used for communication with external apps.
|
||||
*
|
||||
* @type {Transport}
|
||||
*/
|
||||
const transport = new Transport({
|
||||
backend: new PostMessageTransportBackend({
|
||||
postisOptions: { scope: 'jitsi-remote-control' }
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* This class represents the receiver party for a remote controller session.
|
||||
* It handles "remote-control-event" events and sends them to the
|
||||
* API module. From there the events can be received from wrapper application
|
||||
* and executed.
|
||||
*/
|
||||
export default class Receiver extends RemoteControlParticipant {
|
||||
_controller: ?string;
|
||||
_enabled: boolean;
|
||||
_hangupListener: Function;
|
||||
_remoteControlEventsListener: Function;
|
||||
_userLeftListener: Function;
|
||||
|
||||
/**
|
||||
* Creates new instance.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this._controller = null;
|
||||
this._remoteControlEventsListener
|
||||
= this._onRemoteControlMessage.bind(this);
|
||||
this._userLeftListener = this._onUserLeft.bind(this);
|
||||
this._hangupListener = this._onHangup.bind(this);
|
||||
|
||||
// We expect here that even if we receive the supported event earlier
|
||||
// it will be cached and we'll receive it.
|
||||
transport.on('event', event => {
|
||||
if (event.name === REMOTE_CONTROL_MESSAGE_NAME) {
|
||||
this._onRemoteControlAPIEvent(event);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables / Disables the remote control.
|
||||
*
|
||||
* @param {boolean} enabled - The new state.
|
||||
* @returns {void}
|
||||
*/
|
||||
_enable(enabled: boolean) {
|
||||
if (this._enabled === enabled) {
|
||||
return;
|
||||
}
|
||||
this._enabled = enabled;
|
||||
if (enabled === true) {
|
||||
logger.log('Remote control receiver enabled.');
|
||||
|
||||
// Announce remote control support.
|
||||
APP.connection.addFeature(DISCO_REMOTE_CONTROL_FEATURE, true);
|
||||
APP.conference.addConferenceListener(
|
||||
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
|
||||
this._remoteControlEventsListener);
|
||||
APP.conference.addListener(JitsiMeetConferenceEvents.BEFORE_HANGUP,
|
||||
this._hangupListener);
|
||||
} else {
|
||||
logger.log('Remote control receiver disabled.');
|
||||
this._stop(true);
|
||||
APP.connection.removeFeature(DISCO_REMOTE_CONTROL_FEATURE);
|
||||
APP.conference.removeConferenceListener(
|
||||
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
|
||||
this._remoteControlEventsListener);
|
||||
APP.conference.removeListener(
|
||||
JitsiMeetConferenceEvents.BEFORE_HANGUP,
|
||||
this._hangupListener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listener for JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED
|
||||
* events. Sends stop message to the wrapper application. Optionally
|
||||
* displays dialog for informing the user that remote control session
|
||||
* ended.
|
||||
*
|
||||
* @param {boolean} [dontNotify] - If true - a notification about stopping
|
||||
* the remote control won't be displayed.
|
||||
* @returns {void}
|
||||
*/
|
||||
_stop(dontNotify: boolean = false) {
|
||||
if (!this._controller) {
|
||||
return;
|
||||
}
|
||||
logger.log('Remote control receiver stop.');
|
||||
this._controller = null;
|
||||
APP.conference.removeConferenceListener(
|
||||
JitsiConferenceEvents.USER_LEFT,
|
||||
this._userLeftListener);
|
||||
transport.sendEvent({
|
||||
name: REMOTE_CONTROL_MESSAGE_NAME,
|
||||
type: EVENTS.stop
|
||||
});
|
||||
this.emit(RemoteControlEvents.ACTIVE_CHANGED, false);
|
||||
if (!dontNotify) {
|
||||
APP.UI.messageHandler.notify(
|
||||
'dialog.remoteControlTitle',
|
||||
'dialog.remoteControlStopMessage'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls this._stop() and sends stop message to the controller participant.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
stop() {
|
||||
if (!this._controller) {
|
||||
return;
|
||||
}
|
||||
this.sendRemoteControlEndpointMessage(this._controller, {
|
||||
type: EVENTS.stop
|
||||
});
|
||||
this._stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens for data channel EndpointMessage. Handles only remote control
|
||||
* messages. Sends the remote control messages to the external app that
|
||||
* will execute them.
|
||||
*
|
||||
* @param {JitsiParticipant} participant - The controller participant.
|
||||
* @param {Object} message - EndpointMessage from the data channels.
|
||||
* @param {string} message.name - The function processes only messages with
|
||||
* name REMOTE_CONTROL_MESSAGE_NAME.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onRemoteControlMessage(participant: Object, message: Object) {
|
||||
if (message.name !== REMOTE_CONTROL_MESSAGE_NAME) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._enabled) {
|
||||
if (this._controller === null
|
||||
&& message.type === EVENTS.permissions
|
||||
&& message.action === PERMISSIONS_ACTIONS.request) {
|
||||
const userId = participant.getId();
|
||||
|
||||
this.emit(RemoteControlEvents.ACTIVE_CHANGED, true);
|
||||
APP.store.dispatch(
|
||||
openRemoteControlAuthorizationDialog(userId));
|
||||
} else if (this._controller === participant.getId()) {
|
||||
if (message.type === EVENTS.stop) {
|
||||
this._stop();
|
||||
} else { // forward the message
|
||||
transport.sendEvent(message);
|
||||
}
|
||||
} // else ignore
|
||||
} else {
|
||||
logger.log('Remote control message is ignored because remote '
|
||||
+ 'control is disabled', message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Denies remote control access for user associated with the passed user id.
|
||||
*
|
||||
* @param {string} userId - The id associated with the user who sent the
|
||||
* request for remote control authorization.
|
||||
* @returns {void}
|
||||
*/
|
||||
deny(userId: string) {
|
||||
this.emit(RemoteControlEvents.ACTIVE_CHANGED, false);
|
||||
this.sendRemoteControlEndpointMessage(userId, {
|
||||
type: EVENTS.permissions,
|
||||
action: PERMISSIONS_ACTIONS.deny
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants remote control access to user associated with the passed user id.
|
||||
*
|
||||
* @param {string} userId - The id associated with the user who sent the
|
||||
* request for remote control authorization.
|
||||
* @returns {void}
|
||||
*/
|
||||
grant(userId: string) {
|
||||
APP.conference.addConferenceListener(JitsiConferenceEvents.USER_LEFT,
|
||||
this._userLeftListener);
|
||||
this._controller = userId;
|
||||
logger.log(`Remote control permissions granted to: ${userId}`);
|
||||
|
||||
let promise;
|
||||
|
||||
if (APP.conference.isSharingScreen
|
||||
&& APP.conference.getDesktopSharingSourceType() === 'screen') {
|
||||
promise = this._sendStartRequest();
|
||||
} else {
|
||||
promise = APP.conference.toggleScreenSharing(
|
||||
true,
|
||||
{
|
||||
desktopSharingSources: [ 'screen' ]
|
||||
})
|
||||
.then(() => this._sendStartRequest());
|
||||
}
|
||||
|
||||
promise
|
||||
.then(() =>
|
||||
this.sendRemoteControlEndpointMessage(userId, {
|
||||
type: EVENTS.permissions,
|
||||
action: PERMISSIONS_ACTIONS.grant
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
logger.error(error);
|
||||
|
||||
this.sendRemoteControlEndpointMessage(userId, {
|
||||
type: EVENTS.permissions,
|
||||
action: PERMISSIONS_ACTIONS.error
|
||||
});
|
||||
|
||||
APP.UI.messageHandler.notify(
|
||||
'dialog.remoteControlTitle',
|
||||
'dialog.startRemoteControlErrorMessage'
|
||||
);
|
||||
|
||||
this._stop(true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends remote control start request.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_sendStartRequest() {
|
||||
return transport.sendRequest({
|
||||
name: REMOTE_CONTROL_MESSAGE_NAME,
|
||||
type: REQUESTS.start,
|
||||
sourceId: APP.conference.getDesktopSharingSourceId()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles remote control events from the external app. Currently only
|
||||
* events with type EVENTS.supported and EVENTS.stop are
|
||||
* supported.
|
||||
*
|
||||
* @param {RemoteControlEvent} event - The remote control event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onRemoteControlAPIEvent(event: Object) {
|
||||
switch (event.type) {
|
||||
case EVENTS.supported:
|
||||
this._onRemoteControlSupported();
|
||||
break;
|
||||
case EVENTS.stop:
|
||||
this.stop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles events for support for executing remote control events into
|
||||
* the wrapper application.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onRemoteControlSupported() {
|
||||
logger.log('Remote Control supported.');
|
||||
if (config.disableRemoteControl) {
|
||||
logger.log('Remote Control disabled.');
|
||||
} else {
|
||||
this._enable(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the stop method if the other side have left.
|
||||
*
|
||||
* @param {string} id - The user id for the participant that have left.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onUserLeft(id: string) {
|
||||
if (this._controller === id) {
|
||||
this._stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles hangup events. Disables the receiver.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onHangup() {
|
||||
this._enable(false);
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
/* @flow */
|
||||
|
||||
import EventEmitter from 'events';
|
||||
import { getLogger } from 'jitsi-meet-logger';
|
||||
|
||||
import JitsiMeetJS from '../../react/features/base/lib-jitsi-meet';
|
||||
import { DISCO_REMOTE_CONTROL_FEATURE }
|
||||
from '../../service/remotecontrol/Constants';
|
||||
import * as RemoteControlEvents
|
||||
from '../../service/remotecontrol/RemoteControlEvents';
|
||||
|
||||
import Controller from './Controller';
|
||||
import Receiver from './Receiver';
|
||||
|
||||
const logger = getLogger(__filename);
|
||||
|
||||
declare var APP: Object;
|
||||
declare var config: Object;
|
||||
|
||||
/**
|
||||
* Implements the remote control functionality.
|
||||
*/
|
||||
class RemoteControl extends EventEmitter {
|
||||
_active: boolean;
|
||||
_initialized: boolean;
|
||||
controller: Controller;
|
||||
receiver: Receiver;
|
||||
|
||||
/**
|
||||
* Constructs new instance. Creates controller and receiver properties.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this.controller = new Controller();
|
||||
this._active = false;
|
||||
this._initialized = false;
|
||||
|
||||
this.controller.on(RemoteControlEvents.ACTIVE_CHANGED, active => {
|
||||
this.active = active;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the remote control session active status.
|
||||
*
|
||||
* @param {boolean} isActive - True - if the controller or the receiver is
|
||||
* currently in remote control session and false otherwise.
|
||||
* @returns {void}
|
||||
*/
|
||||
set active(isActive: boolean) {
|
||||
this._active = isActive;
|
||||
this.emit(RemoteControlEvents.ACTIVE_CHANGED, isActive);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the remote control session active status.
|
||||
*
|
||||
* @returns {boolean} - True - if the controller or the receiver is
|
||||
* currently in remote control session and false otherwise.
|
||||
*/
|
||||
get active(): boolean {
|
||||
return this._active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the remote control - checks if the remote control should be
|
||||
* enabled or not.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
init() {
|
||||
if (config.disableRemoteControl || this._initialized || !JitsiMeetJS.isDesktopSharingEnabled()) {
|
||||
return;
|
||||
}
|
||||
logger.log('Initializing remote control.');
|
||||
this._initialized = true;
|
||||
this.controller.enable(true);
|
||||
this.receiver = new Receiver();
|
||||
|
||||
this.receiver.on(RemoteControlEvents.ACTIVE_CHANGED, active => {
|
||||
this.active = active;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the passed user supports remote control or not.
|
||||
*
|
||||
* @param {JitsiParticipant} user - The user to be tested.
|
||||
* @returns {Promise<boolean>} The promise will be resolved with true if
|
||||
* the user supports remote control and with false if not.
|
||||
*/
|
||||
checkUserRemoteControlSupport(user: Object) {
|
||||
return user.getFeatures()
|
||||
.then(features => features.has(DISCO_REMOTE_CONTROL_FEATURE));
|
||||
}
|
||||
}
|
||||
|
||||
export default new RemoteControl();
|
||||
@@ -1,72 +0,0 @@
|
||||
/* @flow */
|
||||
|
||||
import EventEmitter from 'events';
|
||||
import { getLogger } from 'jitsi-meet-logger';
|
||||
|
||||
import {
|
||||
REMOTE_CONTROL_MESSAGE_NAME
|
||||
} from '../../service/remotecontrol/Constants';
|
||||
|
||||
const logger = getLogger(__filename);
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* Implements common logic for Receiver class and Controller class.
|
||||
*/
|
||||
export default class RemoteControlParticipant extends EventEmitter {
|
||||
_enabled: boolean;
|
||||
|
||||
/**
|
||||
* Creates new instance.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this._enabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables / Disables the remote control.
|
||||
*
|
||||
* @param {boolean} enabled - The new state.
|
||||
* @returns {void}
|
||||
*/
|
||||
enable(enabled: boolean) {
|
||||
this._enabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends remote control message to other participant trough data channel.
|
||||
*
|
||||
* @param {string} to - The participant who will receive the event.
|
||||
* @param {RemoteControlEvent} event - The remote control event.
|
||||
* @param {Function} onDataChannelFail - Handler for data channel failure.
|
||||
* @returns {void}
|
||||
*/
|
||||
sendRemoteControlEndpointMessage(
|
||||
to: ?string,
|
||||
event: Object,
|
||||
onDataChannelFail: ?Function) {
|
||||
if (!this._enabled || !to) {
|
||||
logger.warn(
|
||||
'Remote control: Skip sending remote control event. Params:',
|
||||
this.enable,
|
||||
to);
|
||||
|
||||
return;
|
||||
}
|
||||
try {
|
||||
APP.conference.sendEndpointMessage(to, {
|
||||
name: REMOTE_CONTROL_MESSAGE_NAME,
|
||||
...event
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'Failed to send EndpointMessage via the datachannels',
|
||||
e);
|
||||
if (typeof onDataChannelFail === 'function') {
|
||||
onDataChannelFail(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
package-lock.json
generated
42
package-lock.json
generated
@@ -3302,9 +3302,9 @@
|
||||
}
|
||||
},
|
||||
"@jitsi/js-utils": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@jitsi/js-utils/-/js-utils-1.0.3.tgz",
|
||||
"integrity": "sha512-m6mZz7R716mHP21lTKQffyM0nNFu3Fe/EHCaOVLFY/vdPsaUl9DhypJqtPIYzRUfPnmnugdaxcxrUeSZQXQzVA==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@jitsi/js-utils/-/js-utils-1.0.2.tgz",
|
||||
"integrity": "sha512-ls+X9tn9EemUQwPEBr7Z0UD4sjRtwcu1Bh4MUo0Hv4arp0KVzcCYCW+mofsvuZvHg8xJX12LLNVgUKi1X5XTGg==",
|
||||
"requires": {
|
||||
"bowser": "2.7.0",
|
||||
"js-md5": "0.7.3"
|
||||
@@ -6989,11 +6989,6 @@
|
||||
"gud": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"cross-os": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-os/-/cross-os-1.3.0.tgz",
|
||||
"integrity": "sha512-9kViqCcAwlPLTeSDPlyC2FdMQ5UVPtGZUnGV8vYDcBA3olJ/hDR7H6IfrNJft2DlKONleHf8CMhD+7Uv2tBnEw=="
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
|
||||
@@ -10593,8 +10588,8 @@
|
||||
"integrity": "sha512-+f/4OLeqY8RAmXnonI1ffeY1DR8kMNJPhv5WMFehchf7U71cjMQVKkOz1n6asz6kfVoAqKNWJz1A/18i18AcXA=="
|
||||
},
|
||||
"jitsi-meet-logger": {
|
||||
"version": "github:jitsi/jitsi-meet-logger#4add5bac2e4cea73a05f42b7596ee03c7f7a2567",
|
||||
"from": "github:jitsi/jitsi-meet-logger#v1.0.0"
|
||||
"version": "github:jitsi/jitsi-meet-logger#5ec92357570dc8f0b7ffc1528820721c84c6af8b",
|
||||
"from": "github:jitsi/jitsi-meet-logger#5ec92357570dc8f0b7ffc1528820721c84c6af8b"
|
||||
},
|
||||
"jquery": {
|
||||
"version": "3.5.1",
|
||||
@@ -10776,8 +10771,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "github:jitsi/lib-jitsi-meet#33307629947bf32055f2cbc1798add0871c9ec1e",
|
||||
"from": "github:jitsi/lib-jitsi-meet#33307629947bf32055f2cbc1798add0871c9ec1e",
|
||||
"version": "github:jitsi/lib-jitsi-meet#d2153eb404ddadef6d5b89ae8c499fa144280531",
|
||||
"from": "github:jitsi/lib-jitsi-meet#d2153eb404ddadef6d5b89ae8c499fa144280531",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "1.0.2",
|
||||
"@jitsi/sdp-interop": "1.0.3",
|
||||
@@ -10785,7 +10780,7 @@
|
||||
"async": "0.9.0",
|
||||
"base64-js": "1.3.1",
|
||||
"current-executing-script": "0.1.3",
|
||||
"jitsi-meet-logger": "github:jitsi/jitsi-meet-logger#v1.0.0",
|
||||
"jitsi-meet-logger": "github:jitsi/jitsi-meet-logger#5ec92357570dc8f0b7ffc1528820721c84c6af8b",
|
||||
"lodash.clonedeep": "4.5.0",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"lodash.isequal": "4.5.0",
|
||||
@@ -10797,20 +10792,6 @@
|
||||
"webrtc-adapter": "7.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jitsi/js-utils": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@jitsi/js-utils/-/js-utils-1.0.2.tgz",
|
||||
"integrity": "sha512-ls+X9tn9EemUQwPEBr7Z0UD4sjRtwcu1Bh4MUo0Hv4arp0KVzcCYCW+mofsvuZvHg8xJX12LLNVgUKi1X5XTGg==",
|
||||
"requires": {
|
||||
"bowser": "2.7.0",
|
||||
"js-md5": "0.7.3"
|
||||
}
|
||||
},
|
||||
"js-md5": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.7.3.tgz",
|
||||
"integrity": "sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ=="
|
||||
},
|
||||
"uuid": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.1.0.tgz",
|
||||
@@ -14284,12 +14265,11 @@
|
||||
"integrity": "sha512-iqdJ1KpZbR4XGahgVmaeibB7kDhyMT7wrylINgJaYBY38IAiI0LF32VX1umO4pko6n21YF5I/kSeNQ+OXGqqow=="
|
||||
},
|
||||
"react-native-webrtc": {
|
||||
"version": "1.87.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.87.1.tgz",
|
||||
"integrity": "sha512-XIztid40ohLUoOIDqpavskyAPzopWIjNOoC/y3AtTymt+o+W/rIHZ9Qw8JZCaIjWh2AIrcO2wtb/f1aMWSz2Zw==",
|
||||
"version": "1.84.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.84.1.tgz",
|
||||
"integrity": "sha512-ewZBgKE+YhLaivo9Wh6aiaEp8ZRvFMqblrkDl1nptQiNNH6CungoAzSOxGDnHWAxepRfiUrW5qnADrsYKmaNeQ==",
|
||||
"requires": {
|
||||
"base64-js": "^1.1.2",
|
||||
"cross-os": "^1.3.0",
|
||||
"event-target-shim": "^1.0.5",
|
||||
"prop-types": "^15.5.10",
|
||||
"uuid": "^3.3.2"
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@atlaskit/theme": "7.0.2",
|
||||
"@atlaskit/toggle": "5.0.14",
|
||||
"@atlaskit/tooltip": "12.1.13",
|
||||
"@jitsi/js-utils": "1.0.3",
|
||||
"@jitsi/js-utils": "1.0.2",
|
||||
"@microsoft/microsoft-graph-client": "1.1.0",
|
||||
"@react-native-community/async-storage": "1.3.4",
|
||||
"@react-native-community/google-signin": "3.0.1",
|
||||
@@ -50,13 +50,13 @@
|
||||
"i18next-browser-languagedetector": "3.0.1",
|
||||
"i18next-xhr-backend": "3.0.0",
|
||||
"jQuery-Impromptu": "github:trentrichardson/jQuery-Impromptu#v6.0.0",
|
||||
"jitsi-meet-logger": "github:jitsi/jitsi-meet-logger#v1.0.0",
|
||||
"jitsi-meet-logger": "github:jitsi/jitsi-meet-logger#5ec92357570dc8f0b7ffc1528820721c84c6af8b",
|
||||
"jquery": "3.5.1",
|
||||
"jquery-contextmenu": "2.4.5",
|
||||
"jquery-i18next": "1.2.1",
|
||||
"js-md5": "0.6.1",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#33307629947bf32055f2cbc1798add0871c9ec1e",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#d2153eb404ddadef6d5b89ae8c499fa144280531",
|
||||
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
|
||||
"lodash": "4.17.19",
|
||||
"moment": "2.19.4",
|
||||
@@ -84,7 +84,7 @@
|
||||
"react-native-svg-transformer": "0.14.3",
|
||||
"react-native-url-polyfill": "1.2.0",
|
||||
"react-native-watch-connectivity": "0.4.3",
|
||||
"react-native-webrtc": "1.87.1",
|
||||
"react-native-webrtc": "1.84.1",
|
||||
"react-native-webview": "10.9.0",
|
||||
"react-native-youtube-iframe": "1.2.3",
|
||||
"react-redux": "7.1.0",
|
||||
|
||||
@@ -37,6 +37,7 @@ import '../overlay/middleware';
|
||||
import '../recent-list/middleware';
|
||||
import '../recording/middleware';
|
||||
import '../rejoin/middleware';
|
||||
import '../remote-control/middleware';
|
||||
import '../room-lock/middleware';
|
||||
import '../rtcstats/middleware';
|
||||
import '../subtitles/middleware';
|
||||
|
||||
@@ -43,6 +43,7 @@ import '../notifications/reducer';
|
||||
import '../overlay/reducer';
|
||||
import '../recent-list/reducer';
|
||||
import '../recording/reducer';
|
||||
import '../remote-control/reducer';
|
||||
import '../settings/reducer';
|
||||
import '../subtitles/reducer';
|
||||
import '../toolbox/reducer';
|
||||
|
||||
@@ -90,6 +90,7 @@ export default [
|
||||
'disableRemoteMute',
|
||||
'disableRtx',
|
||||
'disableSimulcast',
|
||||
'disableSuspendVideo',
|
||||
'disableThirdPartyRequests',
|
||||
'displayJids',
|
||||
'doNotStoreRoom',
|
||||
|
||||
@@ -59,7 +59,7 @@ export function getInviteURL(stateOrGetState: Function | Object): string {
|
||||
|
||||
if (inviteDomain) {
|
||||
const meetingId
|
||||
= state['features/base/config'].brandingRoomAlias || urlWithoutParams.pathname.replace(/\//, '');
|
||||
= state['features/base/config'].brandingRoomAlias || urlWithoutParams.pathname.replace('/', '');
|
||||
|
||||
return `${inviteDomain}/${meetingId}`;
|
||||
}
|
||||
|
||||
@@ -68,13 +68,14 @@ function _updateLastN({ getState }) {
|
||||
return;
|
||||
}
|
||||
|
||||
let lastN = typeof config.channelLastN === 'undefined' ? -1 : config.channelLastN;
|
||||
const defaultLastN = typeof config.channelLastN === 'undefined' ? -1 : config.channelLastN;
|
||||
let lastN = defaultLastN;
|
||||
|
||||
// Apply last N limit based on the # of participants and channelLastN settings.
|
||||
// Apply last N limit based on the # of participants
|
||||
const limitedLastN = limitLastN(participantCount, lastNLimits);
|
||||
|
||||
if (limitedLastN !== undefined) {
|
||||
lastN = lastN === -1 ? limitedLastN : Math.min(limitedLastN, lastN);
|
||||
lastN = limitedLastN;
|
||||
}
|
||||
|
||||
if (typeof appState !== 'undefined' && appState !== 'active') {
|
||||
|
||||
@@ -16,12 +16,15 @@ import {
|
||||
PIN_PARTICIPANT,
|
||||
SET_LOADABLE_AVATAR_URL
|
||||
} from './actionTypes';
|
||||
import { DISCO_REMOTE_CONTROL_FEATURE } from './constants';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getNormalizedDisplayName,
|
||||
getParticipantDisplayName,
|
||||
figureOutMutedWhileDisconnectedStatus
|
||||
figureOutMutedWhileDisconnectedStatus,
|
||||
getParticipantById
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Create an action for when dominant speaker changes.
|
||||
@@ -218,14 +221,14 @@ export function muteRemoteParticipant(id) {
|
||||
*/
|
||||
export function participantConnectionStatusChanged(id, connectionStatus) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
return {
|
||||
type: PARTICIPANT_UPDATED,
|
||||
participant: {
|
||||
connectionStatus,
|
||||
id,
|
||||
mutedWhileDisconnected: figureOutMutedWhileDisconnectedStatus(getState(), id, connectionStatus)
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -276,6 +279,48 @@ export function participantJoined(participant) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the features of a remote participant.
|
||||
*
|
||||
* @param {JitsiParticipant} jitsiParticipant - The ID of the participant.
|
||||
* @returns {{
|
||||
* type: PARTICIPANT_UPDATED,
|
||||
* participant: Participant
|
||||
* }}
|
||||
*/
|
||||
export function updateRemoteParticipantFeatures(jitsiParticipant) {
|
||||
return (dispatch, getState) => {
|
||||
if (!jitsiParticipant) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = jitsiParticipant.getId();
|
||||
|
||||
jitsiParticipant.getFeatures()
|
||||
.then(features => {
|
||||
const supportsRemoteControl = features.has(DISCO_REMOTE_CONTROL_FEATURE);
|
||||
const participant = getParticipantById(getState(), id);
|
||||
|
||||
if (!participant || participant.local) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (participant?.supportsRemoteControl !== supportsRemoteControl) {
|
||||
return dispatch({
|
||||
type: PARTICIPANT_UPDATED,
|
||||
participant: {
|
||||
id,
|
||||
supportsRemoteControl
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error(`Failed to get participant features for ${id}!`, error);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to signal that a hidden participant has joined the conference.
|
||||
*
|
||||
@@ -499,3 +544,4 @@ export function setLoadableAvatarUrl(participantId, url) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,11 @@ import { IconPhone } from '../icons';
|
||||
*/
|
||||
export const DEFAULT_AVATAR_RELATIVE_PATH = 'images/avatar.png';
|
||||
|
||||
/**
|
||||
* The value for the "var" attribute of feature tag in disco-info packets.
|
||||
*/
|
||||
export const DISCO_REMOTE_CONTROL_FEATURE = 'http://jitsi.org/meet/remotecontrol';
|
||||
|
||||
/**
|
||||
* Icon URL for jigasi participants.
|
||||
*
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import { getGravatarURL } from '@jitsi/js-utils/avatar';
|
||||
import type { Store } from 'redux';
|
||||
|
||||
import { JitsiParticipantConnectionStatus } from '../lib-jitsi-meet';
|
||||
import { MEDIA_TYPE, shouldRenderVideoTrack } from '../media';
|
||||
@@ -24,39 +23,30 @@ declare var interfaceConfig: Object;
|
||||
*/
|
||||
const AVATAR_QUEUE = [];
|
||||
const AVATAR_CHECKED_URLS = new Map();
|
||||
/* eslint-disable arrow-body-style, no-unused-vars */
|
||||
/* eslint-disable arrow-body-style */
|
||||
const AVATAR_CHECKER_FUNCTIONS = [
|
||||
(participant, _) => {
|
||||
participant => {
|
||||
return participant && participant.isJigasi ? JIGASI_PARTICIPANT_ICON : null;
|
||||
},
|
||||
(participant, _) => {
|
||||
participant => {
|
||||
return participant && participant.avatarURL ? participant.avatarURL : null;
|
||||
},
|
||||
(participant, store) => {
|
||||
if (participant && participant.email) {
|
||||
// TODO: remove once libravatar has deployed their new scaled up infra. -saghul
|
||||
const gravatarBaseURL
|
||||
= store.getState()['features/base/config'].gravatarBaseURL ?? 'https://www.gravatar.com/avatar/';
|
||||
|
||||
return getGravatarURL(participant.email, gravatarBaseURL);
|
||||
}
|
||||
|
||||
return null;
|
||||
participant => {
|
||||
return participant && participant.email ? getGravatarURL(participant.email) : null;
|
||||
}
|
||||
];
|
||||
/* eslint-enable arrow-body-style, no-unused-vars */
|
||||
/* eslint-enable arrow-body-style */
|
||||
|
||||
/**
|
||||
* Resolves the first loadable avatar URL for a participant.
|
||||
*
|
||||
* @param {Object} participant - The participant to resolve avatars for.
|
||||
* @param {Store} store - Redux store.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getFirstLoadableAvatarUrl(participant: Object, store: Store<any, any>) {
|
||||
export function getFirstLoadableAvatarUrl(participant: Object) {
|
||||
const deferred = createDeferred();
|
||||
const fullPromise = deferred.promise
|
||||
.then(() => _getFirstLoadableAvatarUrl(participant, store))
|
||||
.then(() => _getFirstLoadableAvatarUrl(participant))
|
||||
.then(src => {
|
||||
|
||||
if (AVATAR_QUEUE.length) {
|
||||
@@ -412,12 +402,11 @@ export function figureOutMutedWhileDisconnectedStatus(
|
||||
* Resolves the first loadable avatar URL for a participant.
|
||||
*
|
||||
* @param {Object} participant - The participant to resolve avatars for.
|
||||
* @param {Store} store - Redux store.
|
||||
* @returns {?string}
|
||||
*/
|
||||
async function _getFirstLoadableAvatarUrl(participant, store) {
|
||||
async function _getFirstLoadableAvatarUrl(participant) {
|
||||
for (let i = 0; i < AVATAR_CHECKER_FUNCTIONS.length; i++) {
|
||||
const url = AVATAR_CHECKER_FUNCTIONS[i](participant, store);
|
||||
const url = AVATAR_CHECKER_FUNCTIONS[i](participant);
|
||||
|
||||
if (url) {
|
||||
if (AVATAR_CHECKED_URLS.has(url)) {
|
||||
|
||||
5
react/features/base/participants/logger.js
Normal file
5
react/features/base/participants/logger.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// @flow
|
||||
|
||||
import { getLogger } from '../logging/functions';
|
||||
|
||||
export default getLogger('features/base/participants');
|
||||
@@ -237,6 +237,13 @@ StateListenerRegistry.register(
|
||||
_raiseHandUpdated(store, conference, participant.getId(), newValue);
|
||||
break;
|
||||
}
|
||||
case 'remoteControlSessionStatus':
|
||||
store.dispatch(participantUpdated({
|
||||
conference,
|
||||
id: participant.getId(),
|
||||
remoteControlSessionStatus: newValue
|
||||
}));
|
||||
break;
|
||||
default:
|
||||
|
||||
// Ignore for now.
|
||||
@@ -365,8 +372,7 @@ function _maybePlaySounds({ getState, dispatch }, action) {
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _participantJoinedOrUpdated(store, next, action) {
|
||||
const { dispatch, getState } = store;
|
||||
function _participantJoinedOrUpdated({ dispatch, getState }, next, action) {
|
||||
const { participant: { avatarURL, e2eeEnabled, email, id, local, name, raisedHand } } = action;
|
||||
|
||||
// Send an external update of the local participant's raised hand state
|
||||
@@ -402,7 +408,7 @@ function _participantJoinedOrUpdated(store, next, action) {
|
||||
const participantId = !id && local ? getLocalParticipant(getState()).id : id;
|
||||
const updatedParticipant = getParticipantById(getState(), participantId);
|
||||
|
||||
getFirstLoadableAvatarUrl(updatedParticipant, store)
|
||||
getFirstLoadableAvatarUrl(updatedParticipant)
|
||||
.then(url => {
|
||||
dispatch(setLoadableAvatarUrl(participantId, url));
|
||||
});
|
||||
|
||||
@@ -84,12 +84,6 @@ type Props = AbstractProps & {
|
||||
*/
|
||||
dispatch: Dispatch<any>,
|
||||
|
||||
/**
|
||||
* Whether or not should display the "Save Logs" link in the local video
|
||||
* stats table.
|
||||
*/
|
||||
enableSaveLogs: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not clicking the indicator should display a popover for more
|
||||
* details.
|
||||
@@ -392,7 +386,6 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
|
||||
codec = { codec }
|
||||
connectionSummary = { this._getConnectionStatusTip() }
|
||||
e2eRtt = { e2eRtt }
|
||||
enableSaveLogs = { this.props.enableSaveLogs }
|
||||
framerate = { framerate }
|
||||
isLocalVideo = { this.props.isLocalVideo }
|
||||
maxEnabledResolution = { maxEnabledResolution }
|
||||
@@ -447,8 +440,7 @@ export function _mapStateToProps(state: Object, ownProps: Props) {
|
||||
const participant
|
||||
= typeof participantId === 'undefined' ? getLocalParticipant(state) : getParticipantById(state, participantId);
|
||||
const props = {
|
||||
_connectionStatus: participant?.connectionStatus,
|
||||
enableSaveLogs: state['features/base/config'].enableSaveLogs
|
||||
_connectionStatus: participant?.connectionStatus
|
||||
};
|
||||
|
||||
if (conference) {
|
||||
|
||||
@@ -54,11 +54,6 @@ type Props = {
|
||||
*/
|
||||
e2eRtt: number,
|
||||
|
||||
/**
|
||||
* Whether or not should display the "Save Logs" link.
|
||||
*/
|
||||
enableSaveLogs: boolean,
|
||||
|
||||
/**
|
||||
* The endpoint id of this client.
|
||||
*/
|
||||
@@ -158,13 +153,13 @@ class ConnectionStatsTable extends Component<Props> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { isLocalVideo, enableSaveLogs } = this.props;
|
||||
const { isLocalVideo } = this.props;
|
||||
|
||||
return (
|
||||
<div className = 'connection-info'>
|
||||
{ this._renderStatistics() }
|
||||
<div className = 'connection-actions'>
|
||||
{ isLocalVideo && enableSaveLogs ? this._renderSaveLogs() : null}
|
||||
{ isLocalVideo ? this._renderSaveLogs() : null}
|
||||
{ this._renderShowMoreLink() }
|
||||
</div>
|
||||
{ this.props.shouldShowMore ? this._renderAdditionalStats() : null }
|
||||
|
||||
@@ -61,20 +61,8 @@ export function selectParticipantInLargeVideo(participant: ?string) {
|
||||
const state = getState();
|
||||
const participantId = participant ?? _electParticipantInLargeVideo(state);
|
||||
const largeVideo = state['features/large-video'];
|
||||
const screenShares = state['features/video-layout'].screenShares;
|
||||
let latestScreenshareParticipantId;
|
||||
|
||||
if (screenShares && screenShares.length) {
|
||||
latestScreenshareParticipantId = screenShares[screenShares.length - 1];
|
||||
}
|
||||
|
||||
// When trying to auto pin screenshare, always select the endpoint even though it happens to be
|
||||
// the large video participant in redux (for the reasons listed above in the large video selection
|
||||
// logic above). The auto pin screenshare logic kicks in after the track is added
|
||||
// (which updates the large video participant and selects all endpoints because of the auto tile
|
||||
// view mode). If the screenshare endpoint is not among the forwarded endpoints from the bridge,
|
||||
// it needs to be selected again at this point.
|
||||
if (participantId !== largeVideo.participantId || participantId === latestScreenshareParticipantId) {
|
||||
if (participantId !== largeVideo.participantId) {
|
||||
dispatch({
|
||||
type: SELECT_LARGE_VIDEO_PARTICIPANT,
|
||||
participantId
|
||||
|
||||
@@ -144,13 +144,12 @@ function _conferenceJoined({ dispatch }, next, action) {
|
||||
* @param {Object} participant - The knocking participant.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _findLoadableAvatarForKnockingParticipant(store, { id }) {
|
||||
const { dispatch, getState } = store;
|
||||
function _findLoadableAvatarForKnockingParticipant({ dispatch, getState }, { id }) {
|
||||
const updatedParticipant = getState()['features/lobby'].knockingParticipants.find(p => p.id === id);
|
||||
const { disableThirdPartyRequests } = getState()['features/base/config'];
|
||||
|
||||
if (!disableThirdPartyRequests && updatedParticipant && !updatedParticipant.loadableAvatarUrl) {
|
||||
getFirstLoadableAvatarUrl(updatedParticipant, store).then(loadableAvatarUrl => {
|
||||
getFirstLoadableAvatarUrl(updatedParticipant).then(loadableAvatarUrl => {
|
||||
if (loadableAvatarUrl) {
|
||||
dispatch(participantIsKnockingOrUpdated({
|
||||
loadableAvatarUrl,
|
||||
|
||||
70
react/features/remote-control/actionTypes.js
Normal file
70
react/features/remote-control/actionTypes.js
Normal file
@@ -0,0 +1,70 @@
|
||||
// @flow
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that the controller is capturing mouse and keyboard events.
|
||||
*
|
||||
* {
|
||||
* type: CAPTURE_EVENTS,
|
||||
* isCapturingEvents: boolean
|
||||
* }
|
||||
*/
|
||||
export const CAPTURE_EVENTS = 'CAPTURE_EVENTS';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that a remote control active state has changed.
|
||||
*
|
||||
* {
|
||||
* type: REMOTE_CONTROL_ACTIVE,
|
||||
* active: boolean
|
||||
* }
|
||||
*/
|
||||
export const REMOTE_CONTROL_ACTIVE = 'REMOTE_CONTROL_ACTIVE';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets the receiver transport object.
|
||||
*
|
||||
* {
|
||||
* type: SET_RECEIVER_TRANSPORT,
|
||||
* transport: Transport
|
||||
* }
|
||||
*/
|
||||
export const SET_RECEIVER_TRANSPORT = 'SET_RECEIVER_TRANSPORT';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which enables the receiver.
|
||||
*
|
||||
* {
|
||||
* type: SET_RECEIVER_ENABLED,
|
||||
* enabled: boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_RECEIVER_ENABLED = 'SET_RECEIVER_ENABLED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets the controller participant on the receiver side.
|
||||
* {
|
||||
* type: SET_CONTROLLER,
|
||||
* controller: string
|
||||
* }
|
||||
*/
|
||||
export const SET_CONTROLLER = 'SET_CONTROLLER';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets the controlled participant on the controller side.
|
||||
* {
|
||||
* type: SET_CONTROLLED_PARTICIPANT,
|
||||
* controlled: string
|
||||
* }
|
||||
*/
|
||||
export const SET_CONTROLLED_PARTICIPANT = 'SET_CONTROLLED_PARTICIPANT';
|
||||
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets the requested participant on the controller side.
|
||||
* {
|
||||
* type: SET_REQUESTED_PARTICIPANT,
|
||||
* requestedParticipant: string
|
||||
* }
|
||||
*/
|
||||
export const SET_REQUESTED_PARTICIPANT = 'SET_REQUESTED_PARTICIPANT';
|
||||
|
||||
@@ -1,6 +1,44 @@
|
||||
import { openDialog } from '../base/dialog';
|
||||
// @flow
|
||||
|
||||
import { openDialog } from '../base/dialog';
|
||||
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||
import { getParticipantDisplayName, getPinnedParticipant, pinParticipant } from '../base/participants';
|
||||
import { getLocalVideoTrack } from '../base/tracks';
|
||||
import { showNotification } from '../notifications';
|
||||
|
||||
import {
|
||||
CAPTURE_EVENTS,
|
||||
REMOTE_CONTROL_ACTIVE,
|
||||
SET_REQUESTED_PARTICIPANT,
|
||||
SET_CONTROLLER,
|
||||
SET_RECEIVER_ENABLED,
|
||||
SET_RECEIVER_TRANSPORT,
|
||||
SET_CONTROLLED_PARTICIPANT
|
||||
} from './actionTypes';
|
||||
import { RemoteControlAuthorizationDialog } from './components';
|
||||
import {
|
||||
DISCO_REMOTE_CONTROL_FEATURE,
|
||||
EVENTS,
|
||||
REMOTE_CONTROL_MESSAGE_NAME,
|
||||
PERMISSIONS_ACTIONS,
|
||||
REQUESTS
|
||||
} from './constants';
|
||||
import {
|
||||
getKey,
|
||||
getModifiers,
|
||||
getRemoteConrolEventCaptureArea,
|
||||
isRemoteControlEnabled,
|
||||
sendRemoteControlEndpointMessage
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Listeners.
|
||||
*/
|
||||
let permissionsReplyListener, receiverEndpointMessageListener, stopListener;
|
||||
|
||||
declare var APP: Object;
|
||||
declare var $: Function;
|
||||
|
||||
/**
|
||||
* Signals that the remote control authorization dialog should be displayed.
|
||||
@@ -16,6 +54,700 @@ import { RemoteControlAuthorizationDialog } from './components';
|
||||
* }}
|
||||
* @public
|
||||
*/
|
||||
export function openRemoteControlAuthorizationDialog(participantId) {
|
||||
export function openRemoteControlAuthorizationDialog(participantId: string) {
|
||||
return openDialog(RemoteControlAuthorizationDialog, { participantId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the remote control active property.
|
||||
*
|
||||
* @param {boolean} active - The new value for the active property.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function setRemoteControlActive(active: boolean) {
|
||||
return (dispatch: Function, getState: Function) => {
|
||||
const state = getState();
|
||||
const { active: oldActive } = state['features/remote-control'];
|
||||
const { conference } = state['features/base/conference'];
|
||||
|
||||
if (active !== oldActive) {
|
||||
dispatch({
|
||||
type: REMOTE_CONTROL_ACTIVE,
|
||||
active
|
||||
});
|
||||
conference.setLocalParticipantProperty('remoteControlSessionStatus', active);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests permissions from the remote control receiver side.
|
||||
*
|
||||
* @param {string} userId - The user id of the participant that will be
|
||||
* requested.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function requestRemoteControl(userId: string) {
|
||||
return (dispatch: Function, getState: Function) => {
|
||||
const state = getState();
|
||||
const enabled = isRemoteControlEnabled(state);
|
||||
|
||||
if (!enabled) {
|
||||
return Promise.reject(new Error('Remote control is disabled!'));
|
||||
}
|
||||
|
||||
dispatch(setRemoteControlActive(true));
|
||||
|
||||
logger.log(`Requsting remote control permissions from: ${userId}`);
|
||||
|
||||
const { conference } = state['features/base/conference'];
|
||||
|
||||
|
||||
permissionsReplyListener = (participant, event) => {
|
||||
dispatch(processPermissionRequestReply(participant.getId(), event));
|
||||
};
|
||||
|
||||
conference.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, permissionsReplyListener);
|
||||
|
||||
dispatch({
|
||||
type: SET_REQUESTED_PARTICIPANT,
|
||||
requestedParticipant: userId
|
||||
});
|
||||
|
||||
if (!sendRemoteControlEndpointMessage(
|
||||
conference,
|
||||
userId,
|
||||
{
|
||||
type: EVENTS.permissions,
|
||||
action: PERMISSIONS_ACTIONS.request
|
||||
})) {
|
||||
dispatch(clearRequest());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles permission request replies on the controller side.
|
||||
*
|
||||
* @param {string} participantId - The participant that sent the request.
|
||||
* @param {EndpointMessage} event - The permission request event.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function processPermissionRequestReply(participantId: string, event: Object) {
|
||||
return (dispatch: Function, getState: Function) => {
|
||||
const state = getState();
|
||||
const { action, name, type } = event;
|
||||
const { requestedParticipant } = state['features/remote-control'].controller;
|
||||
|
||||
if (isRemoteControlEnabled(state) && name === REMOTE_CONTROL_MESSAGE_NAME && type === EVENTS.permissions
|
||||
&& participantId === requestedParticipant) {
|
||||
let descriptionKey, permissionGranted = false;
|
||||
|
||||
switch (action) {
|
||||
case PERMISSIONS_ACTIONS.grant: {
|
||||
dispatch({
|
||||
type: SET_CONTROLLED_PARTICIPANT,
|
||||
controlled: participantId
|
||||
});
|
||||
|
||||
logger.log('Remote control permissions granted!', participantId);
|
||||
logger.log('Starting remote control controller.');
|
||||
|
||||
const { conference } = state['features/base/conference'];
|
||||
|
||||
stopListener = (participant, stopEvent) => {
|
||||
dispatch(handleRemoteControlStoppedEvent(participant.getId(), stopEvent));
|
||||
};
|
||||
|
||||
conference.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, stopListener);
|
||||
|
||||
dispatch(resume());
|
||||
|
||||
permissionGranted = true;
|
||||
descriptionKey = 'dialog.remoteControlAllowedMessage';
|
||||
break;
|
||||
}
|
||||
case PERMISSIONS_ACTIONS.deny:
|
||||
logger.log('Remote control permissions denied!', participantId);
|
||||
descriptionKey = 'dialog.remoteControlDeniedMessage';
|
||||
break;
|
||||
case PERMISSIONS_ACTIONS.error:
|
||||
logger.error('Error occurred on receiver side');
|
||||
descriptionKey = 'dialog.remoteControlErrorMessage';
|
||||
break;
|
||||
default:
|
||||
logger.error('Unknown reply received!');
|
||||
descriptionKey = 'dialog.remoteControlErrorMessage';
|
||||
}
|
||||
|
||||
dispatch(clearRequest());
|
||||
|
||||
if (!permissionGranted) {
|
||||
dispatch(setRemoteControlActive(false));
|
||||
}
|
||||
|
||||
dispatch(showNotification({
|
||||
descriptionArguments: { user: getParticipantDisplayName(state, participantId) },
|
||||
descriptionKey,
|
||||
titleKey: 'dialog.remoteControlTitle'
|
||||
}));
|
||||
|
||||
if (permissionGranted) {
|
||||
// the remote control permissions has been granted
|
||||
// pin the controlled participant
|
||||
const pinnedParticipant = getPinnedParticipant(state);
|
||||
const pinnedId = pinnedParticipant?.id;
|
||||
|
||||
if (pinnedId !== participantId) {
|
||||
dispatch(pinParticipant(participantId));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// different message type or another user -> ignoring the message
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles remote control stopped.
|
||||
*
|
||||
* @param {string} participantId - The ID of the participant that has sent the event.
|
||||
* @param {EndpointMessage} event - EndpointMessage event from the data channels.
|
||||
* @property {string} type - The function process only events with name REMOTE_CONTROL_MESSAGE_NAME.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function handleRemoteControlStoppedEvent(participantId: Object, event: Object) {
|
||||
return (dispatch: Function, getState: Function) => {
|
||||
const state = getState();
|
||||
const { name, type } = event;
|
||||
const { controlled } = state['features/remote-control'].controller;
|
||||
|
||||
if (isRemoteControlEnabled(state) && name === REMOTE_CONTROL_MESSAGE_NAME && type === EVENTS.stop
|
||||
&& participantId === controlled) {
|
||||
dispatch(stopController());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops processing the mouse and keyboard events. Removes added listeners.
|
||||
* Enables the keyboard shortcuts. Displays dialog to notify the user that remote control session has ended.
|
||||
*
|
||||
* @param {boolean} notifyRemoteParty - If true a endpoint message to the controlled participant will be sent.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function stopController(notifyRemoteParty: boolean = false) {
|
||||
return (dispatch: Function, getState: Function) => {
|
||||
const state = getState();
|
||||
const { controlled } = state['features/remote-control'].controller;
|
||||
|
||||
if (!controlled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { conference } = state['features/base/conference'];
|
||||
|
||||
if (notifyRemoteParty) {
|
||||
sendRemoteControlEndpointMessage(conference, controlled, {
|
||||
type: EVENTS.stop
|
||||
});
|
||||
}
|
||||
|
||||
logger.log('Stopping remote control controller.');
|
||||
|
||||
conference.off(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, stopListener);
|
||||
stopListener = undefined;
|
||||
|
||||
dispatch(pause());
|
||||
|
||||
dispatch({
|
||||
type: SET_CONTROLLED_PARTICIPANT,
|
||||
controlled: undefined
|
||||
});
|
||||
|
||||
dispatch(setRemoteControlActive(false));
|
||||
dispatch(showNotification({
|
||||
descriptionKey: 'dialog.remoteControlStopMessage',
|
||||
titleKey: 'dialog.remoteControlTitle'
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears a pending permission request.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function clearRequest() {
|
||||
return (dispatch: Function, getState: Function) => {
|
||||
const { conference } = getState()['features/base/conference'];
|
||||
|
||||
dispatch({
|
||||
type: SET_REQUESTED_PARTICIPANT,
|
||||
requestedParticipant: undefined
|
||||
});
|
||||
|
||||
conference.off(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, permissionsReplyListener);
|
||||
permissionsReplyListener = undefined;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets that trasnport object that is used by the receiver to communicate with the native part of the remote control
|
||||
* implementation.
|
||||
*
|
||||
* @param {Transport} transport - The transport to be set.
|
||||
* @returns {{
|
||||
* type: SET_RECEIVER_TRANSPORT,
|
||||
* transport: Transport
|
||||
* }}
|
||||
*/
|
||||
export function setReceiverTransport(transport: Object) {
|
||||
return {
|
||||
type: SET_RECEIVER_TRANSPORT,
|
||||
transport
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the receiver functionality.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function enableReceiver() {
|
||||
return (dispatch: Function, getState: Function) => {
|
||||
const state = getState();
|
||||
const { enabled } = state['features/remote-control'].receiver;
|
||||
|
||||
if (enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { connection } = state['features/base/connection'];
|
||||
const { conference } = state['features/base/conference'];
|
||||
|
||||
if (!connection || !conference) {
|
||||
logger.error('Couldn\'t enable the remote receiver! The connection or conference instance is undefined!');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: SET_RECEIVER_ENABLED,
|
||||
enabled: true
|
||||
});
|
||||
|
||||
connection.addFeature(DISCO_REMOTE_CONTROL_FEATURE, true);
|
||||
receiverEndpointMessageListener = (participant, message) => {
|
||||
dispatch(endpointMessageReceived(participant.getId(), message));
|
||||
};
|
||||
conference.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, receiverEndpointMessageListener);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the receiver functionality.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function disableReceiver() {
|
||||
return (dispatch: Function, getState: Function) => {
|
||||
const state = getState();
|
||||
const { enabled } = state['features/remote-control'].receiver;
|
||||
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { connection } = state['features/base/connection'];
|
||||
const { conference } = state['features/base/conference'];
|
||||
|
||||
if (!connection || !conference) {
|
||||
logger.error('Couldn\'t enable the remote receiver! The connection or conference instance is undefined!');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log('Remote control receiver disabled.');
|
||||
|
||||
dispatch({
|
||||
type: SET_RECEIVER_ENABLED,
|
||||
enabled: false
|
||||
});
|
||||
|
||||
dispatch(stopReceiver(true));
|
||||
|
||||
connection.removeFeature(DISCO_REMOTE_CONTROL_FEATURE);
|
||||
conference.off(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, receiverEndpointMessageListener);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops a remote control session on the receiver side.
|
||||
*
|
||||
* @param {boolean} [dontNotifyLocalParty] - If true - a notification about stopping
|
||||
* the remote control won't be displayed.
|
||||
* @param {boolean} [dontNotifyRemoteParty] - If true a endpoint message to the controller participant will be sent.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function stopReceiver(dontNotifyLocalParty: boolean = false, dontNotifyRemoteParty: boolean = false) {
|
||||
return (dispatch: Function, getState: Function) => {
|
||||
const state = getState();
|
||||
const { receiver } = state['features/remote-control'];
|
||||
const { controller, transport } = receiver;
|
||||
|
||||
if (!controller) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { conference } = state['features/base/conference'];
|
||||
|
||||
if (!dontNotifyRemoteParty) {
|
||||
sendRemoteControlEndpointMessage(conference, controller, {
|
||||
type: EVENTS.stop
|
||||
});
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: SET_CONTROLLER,
|
||||
controller: undefined
|
||||
});
|
||||
|
||||
transport.sendEvent({
|
||||
name: REMOTE_CONTROL_MESSAGE_NAME,
|
||||
type: EVENTS.stop
|
||||
});
|
||||
|
||||
dispatch(setRemoteControlActive(false));
|
||||
|
||||
if (!dontNotifyLocalParty) {
|
||||
dispatch(showNotification({
|
||||
descriptionKey: 'dialog.remoteControlStopMessage',
|
||||
titleKey: 'dialog.remoteControlTitle'
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles only remote control endpoint messages.
|
||||
*
|
||||
* @param {string} participantId - The controller participant ID.
|
||||
* @param {Object} message - EndpointMessage from the data channels.
|
||||
* @param {string} message.name - The function processes only messages with
|
||||
* name REMOTE_CONTROL_MESSAGE_NAME.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function endpointMessageReceived(participantId: string, message: Object) {
|
||||
return (dispatch: Function, getState: Function) => {
|
||||
const { action, name, type } = message;
|
||||
|
||||
if (name !== REMOTE_CONTROL_MESSAGE_NAME) {
|
||||
return;
|
||||
}
|
||||
|
||||
const state = getState();
|
||||
const { receiver } = state['features/remote-control'];
|
||||
const { enabled, transport } = receiver;
|
||||
|
||||
if (enabled) {
|
||||
const { controller } = receiver;
|
||||
|
||||
if (!controller && type === EVENTS.permissions && action === PERMISSIONS_ACTIONS.request) {
|
||||
dispatch(setRemoteControlActive(true));
|
||||
dispatch(openRemoteControlAuthorizationDialog(participantId));
|
||||
} else if (controller === participantId) {
|
||||
if (type === EVENTS.stop) {
|
||||
dispatch(stopReceiver(false, true));
|
||||
} else { // forward the message
|
||||
transport.sendEvent(message);
|
||||
}
|
||||
} // else ignore
|
||||
} else {
|
||||
logger.log('Remote control message is ignored because remote control is disabled', message);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Denies remote control access for user associated with the passed user id.
|
||||
*
|
||||
* @param {string} participantId - The id associated with the user who sent the
|
||||
* request for remote control authorization.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function deny(participantId: string) {
|
||||
return (dispatch: Function, getState: Function) => {
|
||||
const state = getState();
|
||||
const { conference } = state['features/base/conference'];
|
||||
|
||||
dispatch(setRemoteControlActive(false));
|
||||
sendRemoteControlEndpointMessage(conference, participantId, {
|
||||
type: EVENTS.permissions,
|
||||
action: PERMISSIONS_ACTIONS.deny
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends start remote control request to the native implementation.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function sendStartRequest() {
|
||||
return (dispatch: Function, getState: Function) => {
|
||||
const state = getState();
|
||||
const tracks = state['features/base/tracks'];
|
||||
const track = getLocalVideoTrack(tracks);
|
||||
const { sourceId } = track?.jitsiTrack || {};
|
||||
const { transport } = state['features/remote-control'].receiver;
|
||||
|
||||
return transport.sendRequest({
|
||||
name: REMOTE_CONTROL_MESSAGE_NAME,
|
||||
type: REQUESTS.start,
|
||||
sourceId
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants remote control access to user associated with the passed user id.
|
||||
*
|
||||
* @param {string} participantId - The id associated with the user who sent the
|
||||
* request for remote control authorization.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function grant(participantId: string) {
|
||||
return (dispatch: Function, getState: Function) => {
|
||||
dispatch({
|
||||
type: SET_CONTROLLER,
|
||||
controller: participantId
|
||||
});
|
||||
logger.log(`Remote control permissions granted to: ${participantId}`);
|
||||
|
||||
let promise;
|
||||
const state = getState();
|
||||
const tracks = state['features/base/tracks'];
|
||||
const track = getLocalVideoTrack(tracks);
|
||||
const isScreenSharing = track?.videoType === 'desktop';
|
||||
const { sourceType } = track?.jitsiTrack || {};
|
||||
|
||||
if (isScreenSharing && sourceType === 'screen') {
|
||||
promise = dispatch(sendStartRequest());
|
||||
} else {
|
||||
// FIXME: Use action here once toggleScreenSharing is moved to redux.
|
||||
promise = APP.conference.toggleScreenSharing(
|
||||
true,
|
||||
{
|
||||
desktopSharingSources: [ 'screen' ]
|
||||
})
|
||||
.then(() => dispatch(sendStartRequest()));
|
||||
}
|
||||
|
||||
const { conference } = state['features/base/conference'];
|
||||
|
||||
promise
|
||||
.then(() => sendRemoteControlEndpointMessage(conference, participantId, {
|
||||
type: EVENTS.permissions,
|
||||
action: PERMISSIONS_ACTIONS.grant
|
||||
}))
|
||||
.catch(error => {
|
||||
logger.error(error);
|
||||
|
||||
sendRemoteControlEndpointMessage(conference, participantId, {
|
||||
type: EVENTS.permissions,
|
||||
action: PERMISSIONS_ACTIONS.error
|
||||
});
|
||||
|
||||
dispatch(showNotification({
|
||||
descriptionKey: 'dialog.startRemoteControlErrorMessage',
|
||||
titleKey: 'dialog.remoteControlTitle'
|
||||
}));
|
||||
|
||||
dispatch(stopReceiver(true));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for mouse click events on the controller side.
|
||||
*
|
||||
* @param {string} type - The type of event ("mousedown"/"mouseup").
|
||||
* @param {Event} event - The mouse event.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function mouseClicked(type: string, event: Object) {
|
||||
return (dispatch: Function, getState: Function) => {
|
||||
const state = getState();
|
||||
const { conference } = state['features/base/conference'];
|
||||
const { controller } = state['features/remote-control'];
|
||||
|
||||
sendRemoteControlEndpointMessage(conference, controller.controlled, {
|
||||
type,
|
||||
button: event.which
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mouse moved events on the controller side.
|
||||
*
|
||||
* @param {Event} event - The mouse event.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function mouseMoved(event: Object) {
|
||||
return (dispatch: Function, getState: Function) => {
|
||||
const area = getRemoteConrolEventCaptureArea();
|
||||
|
||||
if (!area) {
|
||||
return;
|
||||
}
|
||||
|
||||
const position = area.position();
|
||||
const state = getState();
|
||||
const { conference } = state['features/base/conference'];
|
||||
const { controller } = state['features/remote-control'];
|
||||
|
||||
sendRemoteControlEndpointMessage(conference, controller.controlled, {
|
||||
type: EVENTS.mousemove,
|
||||
x: (event.pageX - position.left) / area.width(),
|
||||
y: (event.pageY - position.top) / area.height()
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mouse scroll events on the controller side.
|
||||
*
|
||||
* @param {Event} event - The mouse event.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function mouseScrolled(event: Object) {
|
||||
return (dispatch: Function, getState: Function) => {
|
||||
const state = getState();
|
||||
const { conference } = state['features/base/conference'];
|
||||
const { controller } = state['features/remote-control'];
|
||||
|
||||
sendRemoteControlEndpointMessage(conference, controller.controlled, {
|
||||
type: EVENTS.mousescroll,
|
||||
x: event.deltaX,
|
||||
y: event.deltaY
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles key press events on the controller side..
|
||||
*
|
||||
* @param {string} type - The type of event ("keydown"/"keyup").
|
||||
* @param {Event} event - The key event.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function keyPressed(type: string, event: Object) {
|
||||
return (dispatch: Function, getState: Function) => {
|
||||
const state = getState();
|
||||
const { conference } = state['features/base/conference'];
|
||||
const { controller } = state['features/remote-control'];
|
||||
|
||||
sendRemoteControlEndpointMessage(conference, controller.controlled, {
|
||||
type,
|
||||
key: getKey(event),
|
||||
modifiers: getModifiers(event)
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the keyboatd shortcuts. Starts collecting remote control
|
||||
* events. It can be used to resume an active remote control session which
|
||||
* was paused with the pause action.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function resume() {
|
||||
return (dispatch: Function, getState: Function) => {
|
||||
const area = getRemoteConrolEventCaptureArea();
|
||||
const state = getState();
|
||||
const { controller } = state['features/remote-control'];
|
||||
const { controlled, isCapturingEvents } = controller;
|
||||
|
||||
if (!isRemoteControlEnabled(state) || !area || !controlled || isCapturingEvents) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log('Resuming remote control controller.');
|
||||
|
||||
// FIXME: Once the keyboard shortcuts are using react/redux.
|
||||
APP.keyboardshortcut.enable(false);
|
||||
|
||||
area.mousemove(event => {
|
||||
dispatch(mouseMoved(event));
|
||||
});
|
||||
area.mousedown(event => dispatch(mouseClicked(EVENTS.mousedown, event)));
|
||||
area.mouseup(event => dispatch(mouseClicked(EVENTS.mouseup, event)));
|
||||
area.dblclick(event => dispatch(mouseClicked(EVENTS.mousedblclick, event)));
|
||||
area.contextmenu(() => false);
|
||||
area[0].onmousewheel = event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
dispatch(mouseScrolled(event));
|
||||
|
||||
return false;
|
||||
};
|
||||
$(window).keydown(event => dispatch(keyPressed(EVENTS.keydown, event)));
|
||||
$(window).keyup(event => dispatch(keyPressed(EVENTS.keyup, event)));
|
||||
|
||||
dispatch({
|
||||
type: CAPTURE_EVENTS,
|
||||
isCapturingEvents: true
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Pauses the collecting of events and enables the keyboard shortcus. But
|
||||
* it doesn't removes any other listeners. Basically the remote control
|
||||
* session will be still active after the pause action, but no events from the
|
||||
* controller side will be captured and sent. You can resume the collecting
|
||||
* of the events with the resume action.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function pause() {
|
||||
return (dispatch: Function, getState: Function) => {
|
||||
const state = getState();
|
||||
const { controller } = state['features/remote-control'];
|
||||
const { controlled, isCapturingEvents } = controller;
|
||||
|
||||
if (!isRemoteControlEnabled(state) || !controlled || !isCapturingEvents) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log('Pausing remote control controller.');
|
||||
|
||||
// FIXME: Once the keyboard shortcuts are using react/redux.
|
||||
APP.keyboardshortcut.enable(true);
|
||||
|
||||
const area = getRemoteConrolEventCaptureArea();
|
||||
|
||||
if (area) {
|
||||
area.off('contextmenu');
|
||||
area.off('dblclick');
|
||||
area.off('mousedown');
|
||||
area.off('mousemove');
|
||||
area.off('mouseup');
|
||||
area[0].onmousewheel = undefined;
|
||||
}
|
||||
|
||||
$(window).off('keydown');
|
||||
$(window).off('keyup');
|
||||
|
||||
dispatch({
|
||||
type: CAPTURE_EVENTS,
|
||||
isCapturingEvents: false
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import { Dialog, hideDialog } from '../../base/dialog';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { getParticipantById } from '../../base/participants';
|
||||
import { connect } from '../../base/redux';
|
||||
import { getLocalVideoTrack } from '../../base/tracks';
|
||||
import { grant, deny } from '../actions';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
@@ -21,6 +23,9 @@ type Props = {
|
||||
*/
|
||||
_displayName: string,
|
||||
|
||||
_isScreenSharing: boolean,
|
||||
_sourceType: string,
|
||||
|
||||
/**
|
||||
* Used to show/hide the dialog on cancel.
|
||||
*/
|
||||
@@ -87,10 +92,9 @@ class RemoteControlAuthorizationDialog extends Component<Props> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_getAdditionalMessage() {
|
||||
// FIXME: Once we have this information in redux we should
|
||||
// start getting it from there.
|
||||
if (APP.conference.isSharingScreen
|
||||
&& APP.conference.getDesktopSharingSourceType() === 'screen') {
|
||||
const { _isScreenSharing, _sourceType } = this.props;
|
||||
|
||||
if (_isScreenSharing && _sourceType === 'screen') {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -112,8 +116,9 @@ class RemoteControlAuthorizationDialog extends Component<Props> {
|
||||
* @returns {boolean} Returns true to close the dialog.
|
||||
*/
|
||||
_onCancel() {
|
||||
// FIXME: This should be action one day.
|
||||
APP.remoteControl.receiver.deny(this.props.participantId);
|
||||
const { dispatch, participantId } = this.props;
|
||||
|
||||
dispatch(deny(participantId));
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -131,10 +136,10 @@ class RemoteControlAuthorizationDialog extends Component<Props> {
|
||||
* picker window, the action will be ignored).
|
||||
*/
|
||||
_onSubmit() {
|
||||
this.props.dispatch(hideDialog());
|
||||
const { dispatch, participantId } = this.props;
|
||||
|
||||
// FIXME: This should be action one day.
|
||||
APP.remoteControl.receiver.grant(this.props.participantId);
|
||||
dispatch(hideDialog());
|
||||
dispatch(grant(participantId));
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -149,15 +154,24 @@ class RemoteControlAuthorizationDialog extends Component<Props> {
|
||||
* (instance of) RemoteControlAuthorizationDialog.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _displayName: string
|
||||
* _displayName: string,
|
||||
* _isScreenSharing: boolean,
|
||||
* _sourceId: string,
|
||||
* _sourceType: string
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state, ownProps) {
|
||||
const { _displayName, participantId } = ownProps;
|
||||
const participant = getParticipantById(state, participantId);
|
||||
const tracks = state['features/base/tracks'];
|
||||
const track = getLocalVideoTrack(tracks);
|
||||
const _isScreenSharing = track?.videoType === 'desktop';
|
||||
const { sourceType } = track?.jitsiTrack || {};
|
||||
|
||||
return {
|
||||
_displayName: participant ? participant.name : _displayName
|
||||
_displayName: participant ? participant.name : _displayName,
|
||||
_isScreenSharing,
|
||||
_sourceType: sourceType
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,41 @@
|
||||
/**
|
||||
* The type of remote control messages.
|
||||
*/
|
||||
export const REMOTE_CONTROL_MESSAGE_NAME = 'remote-control';
|
||||
|
||||
/**
|
||||
* The value for the "var" attribute of feature tag in disco-info packets.
|
||||
*/
|
||||
export const DISCO_REMOTE_CONTROL_FEATURE
|
||||
= 'http://jitsi.org/meet/remotecontrol';
|
||||
export const DISCO_REMOTE_CONTROL_FEATURE = 'http://jitsi.org/meet/remotecontrol';
|
||||
|
||||
/**
|
||||
* The remote control event.
|
||||
* @typedef {object} RemoteControlEvent
|
||||
* @property {EVENTS | REQUESTS} type - the type of the message
|
||||
* @property {number} x - avaibale for type === mousemove only. The new x
|
||||
* coordinate of the mouse
|
||||
* @property {number} y - For mousemove type - the new y
|
||||
* coordinate of the mouse and for mousescroll - represents the vertical
|
||||
* scrolling diff value
|
||||
* @property {number} button - 1(left), 2(middle) or 3 (right). Supported by
|
||||
* mousedown, mouseup and mousedblclick types.
|
||||
* @property {KEYS} key - Represents the key related to the event. Supported by
|
||||
* keydown and keyup types.
|
||||
* @property {KEYS[]} modifiers - Represents the modifier related to the event.
|
||||
* Supported by keydown and keyup types.
|
||||
* @property {PERMISSIONS_ACTIONS} action - Supported by type === permissions.
|
||||
* Represents the action related to the permissions event.
|
||||
*
|
||||
* Optional properties. Supported for permissions event for action === request:
|
||||
* @property {string} userId - The user id of the participant that has sent the
|
||||
* request.
|
||||
* @property {string} userJID - The full JID in the MUC of the user that has
|
||||
* sent the request.
|
||||
* @property {string} displayName - the displayName of the participant that has
|
||||
* sent the request.
|
||||
* @property {boolean} screenSharing - true if the SS is started for the local
|
||||
* participant and false if not.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Types of remote-control events.
|
||||
@@ -44,36 +77,3 @@ export const PERMISSIONS_ACTIONS = {
|
||||
error: 'error'
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of remote control messages.
|
||||
*/
|
||||
export const REMOTE_CONTROL_MESSAGE_NAME = 'remote-control';
|
||||
|
||||
/**
|
||||
* The remote control event.
|
||||
* @typedef {object} RemoteControlEvent
|
||||
* @property {EVENTS | REQUESTS} type - the type of the message
|
||||
* @property {number} x - avaibale for type === mousemove only. The new x
|
||||
* coordinate of the mouse
|
||||
* @property {number} y - For mousemove type - the new y
|
||||
* coordinate of the mouse and for mousescroll - represents the vertical
|
||||
* scrolling diff value
|
||||
* @property {number} button - 1(left), 2(middle) or 3 (right). Supported by
|
||||
* mousedown, mouseup and mousedblclick types.
|
||||
* @property {KEYS} key - Represents the key related to the event. Supported by
|
||||
* keydown and keyup types.
|
||||
* @property {KEYS[]} modifiers - Represents the modifier related to the event.
|
||||
* Supported by keydown and keyup types.
|
||||
* @property {PERMISSIONS_ACTIONS} action - Supported by type === permissions.
|
||||
* Represents the action related to the permissions event.
|
||||
*
|
||||
* Optional properties. Supported for permissions event for action === request:
|
||||
* @property {string} userId - The user id of the participant that has sent the
|
||||
* request.
|
||||
* @property {string} userJID - The full JID in the MUC of the user that has
|
||||
* sent the request.
|
||||
* @property {string} displayName - the displayName of the participant that has
|
||||
* sent the request.
|
||||
* @property {boolean} screenSharing - true if the SS is started for the local
|
||||
* participant and false if not.
|
||||
*/
|
||||
128
react/features/remote-control/functions.js
Normal file
128
react/features/remote-control/functions.js
Normal file
@@ -0,0 +1,128 @@
|
||||
// @flow
|
||||
|
||||
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
|
||||
import JitsiMeetJS from '../base/lib-jitsi-meet';
|
||||
|
||||
import { enableReceiver, stopReceiver } from './actions';
|
||||
import { REMOTE_CONTROL_MESSAGE_NAME, EVENTS } from './constants';
|
||||
import { keyboardEventToKey } from './keycodes';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Checks if the remote contrrol is enabled.
|
||||
*
|
||||
* @param {*} state - The redux state.
|
||||
* @returns {boolean} - True if the remote control is enabled and false otherwise.
|
||||
*/
|
||||
export function isRemoteControlEnabled(state: Object) {
|
||||
return !state['features/base/config'].disableRemoteControl && JitsiMeetJS.isDesktopSharingEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends remote control message to other participant trough data channel.
|
||||
*
|
||||
* @param {JitsiConference} conference - The JitsiConference object.
|
||||
* @param {string} to - The participant who will receive the event.
|
||||
* @param {RemoteControlEvent} event - The remote control event.
|
||||
* @returns {boolean} - True if the message was sent successfully and false otherwise.
|
||||
*/
|
||||
export function sendRemoteControlEndpointMessage(
|
||||
conference: Object,
|
||||
to: ?string,
|
||||
event: Object) {
|
||||
if (!to) {
|
||||
logger.warn('Remote control: Skip sending remote control event. Params:', to);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
conference.sendEndpointMessage(to, {
|
||||
name: REMOTE_CONTROL_MESSAGE_NAME,
|
||||
...event
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('Failed to send EndpointMessage via the datachannels', error);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles remote control events from the external app. Currently only
|
||||
* events with type EVENTS.supported and EVENTS.stop are
|
||||
* supported.
|
||||
*
|
||||
* @param {RemoteControlEvent} event - The remote control event.
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function onRemoteControlAPIEvent(event: Object, { getState, dispatch }: Object) {
|
||||
switch (event.type) {
|
||||
case EVENTS.supported:
|
||||
logger.log('Remote Control supported.');
|
||||
if (isRemoteControlEnabled(getState())) {
|
||||
dispatch(enableReceiver());
|
||||
} else {
|
||||
logger.log('Remote Control disabled.');
|
||||
}
|
||||
break;
|
||||
case EVENTS.stop: {
|
||||
dispatch(stopReceiver());
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the area used for capturing mouse and key events.
|
||||
*
|
||||
* @returns {JQuery} - A JQuery selector.
|
||||
*/
|
||||
export function getRemoteConrolEventCaptureArea() {
|
||||
return VideoLayout.getLargeVideoWrapper();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extract the keyboard key from the keyboard event.
|
||||
*
|
||||
* @param {KeyboardEvent} event - The event.
|
||||
* @returns {KEYS} The key that is pressed or undefined.
|
||||
*/
|
||||
export function getKey(event: Object) {
|
||||
return keyboardEventToKey(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the modifiers from the keyboard event.
|
||||
*
|
||||
* @param {KeyboardEvent} event - The event.
|
||||
* @returns {Array} With possible values: "shift", "control", "alt", "command".
|
||||
*/
|
||||
export function getModifiers(event: Object) {
|
||||
const modifiers = [];
|
||||
|
||||
if (event.shiftKey) {
|
||||
modifiers.push('shift');
|
||||
}
|
||||
|
||||
if (event.ctrlKey) {
|
||||
modifiers.push('control');
|
||||
}
|
||||
|
||||
|
||||
if (event.altKey) {
|
||||
modifiers.push('alt');
|
||||
}
|
||||
|
||||
if (event.metaKey) {
|
||||
modifiers.push('command');
|
||||
}
|
||||
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
@@ -158,8 +158,9 @@ for (let i = 0; i < 26; i++) {
|
||||
|
||||
/**
|
||||
* Returns key associated with the keyCode from the passed event.
|
||||
* @param {KeyboardEvent} event the event
|
||||
* @returns {KEYS} the key on the keyboard.
|
||||
*
|
||||
* @param {KeyboardEvent} event - The event.
|
||||
* @returns {KEYS} - The key on the keyboard.
|
||||
*/
|
||||
export function keyboardEventToKey(event) {
|
||||
return keyCodeToKey[event.which];
|
||||
5
react/features/remote-control/logger.js
Normal file
5
react/features/remote-control/logger.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// @flow
|
||||
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/remote-control');
|
||||
92
react/features/remote-control/middleware.js
Normal file
92
react/features/remote-control/middleware.js
Normal file
@@ -0,0 +1,92 @@
|
||||
// @flow
|
||||
import { PostMessageTransportBackend, Transport } from '@jitsi/js-utils/transport';
|
||||
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
|
||||
import { CONFERENCE_JOINED } from '../base/conference';
|
||||
import { PARTICIPANT_LEFT } from '../base/participants';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
|
||||
import {
|
||||
clearRequest, setReceiverTransport, setRemoteControlActive, stopController, stopReceiver
|
||||
} from './actions';
|
||||
import { REMOTE_CONTROL_MESSAGE_NAME } from './constants';
|
||||
import { onRemoteControlAPIEvent } from './functions';
|
||||
import './subscriber';
|
||||
|
||||
/**
|
||||
* The redux middleware for the remote control feature.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => async action => {
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT: {
|
||||
const { dispatch } = store;
|
||||
|
||||
dispatch(setReceiverTransport(new Transport({
|
||||
backend: new PostMessageTransportBackend({
|
||||
postisOptions: { scope: 'jitsi-remote-control' }
|
||||
})
|
||||
})));
|
||||
|
||||
break;
|
||||
}
|
||||
case APP_WILL_UNMOUNT: {
|
||||
const { getState, dispatch } = store;
|
||||
const { transport } = getState()['features/remote-control'].receiver;
|
||||
|
||||
if (transport) {
|
||||
transport.dispose();
|
||||
dispatch(setReceiverTransport());
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case CONFERENCE_JOINED: {
|
||||
const result = next(action);
|
||||
const { getState } = store;
|
||||
const { transport } = getState()['features/remote-control'].receiver;
|
||||
|
||||
if (transport) {
|
||||
// We expect here that even if we receive the supported event earlier
|
||||
// it will be cached and we'll receive it.
|
||||
transport.on('event', event => {
|
||||
if (event.name === REMOTE_CONTROL_MESSAGE_NAME) {
|
||||
onRemoteControlAPIEvent(event, store);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
case PARTICIPANT_LEFT: {
|
||||
const { getState, dispatch } = store;
|
||||
const state = getState();
|
||||
const { id } = action.participant;
|
||||
const { receiver, controller } = state['features/remote-control'];
|
||||
const { requestedParticipant, controlled } = controller;
|
||||
|
||||
if (id === controlled) {
|
||||
dispatch(stopController());
|
||||
}
|
||||
|
||||
if (id === requestedParticipant) {
|
||||
dispatch(clearRequest());
|
||||
dispatch(setRemoteControlActive(false));
|
||||
}
|
||||
|
||||
if (receiver?.controller === id) {
|
||||
dispatch(stopReceiver(false, true));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
68
react/features/remote-control/reducer.js
Normal file
68
react/features/remote-control/reducer.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import { ReducerRegistry, set } from '../base/redux';
|
||||
|
||||
import {
|
||||
CAPTURE_EVENTS,
|
||||
REMOTE_CONTROL_ACTIVE,
|
||||
SET_CONTROLLED_PARTICIPANT,
|
||||
SET_CONTROLLER,
|
||||
SET_RECEIVER_ENABLED,
|
||||
SET_RECEIVER_TRANSPORT,
|
||||
SET_REQUESTED_PARTICIPANT
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* The default state.
|
||||
*/
|
||||
const DEFAULT_STATE = {
|
||||
active: false,
|
||||
controller: {
|
||||
isCapturingEvents: false
|
||||
},
|
||||
receiver: {
|
||||
enabled: false
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Listen for actions that mutate the remote control state.
|
||||
*/
|
||||
ReducerRegistry.register(
|
||||
'features/remote-control', (state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case CAPTURE_EVENTS:
|
||||
return {
|
||||
...state,
|
||||
controller: set(state.controller, 'isCapturingEvents', action.isCapturingEvents)
|
||||
};
|
||||
case REMOTE_CONTROL_ACTIVE:
|
||||
return set(state, 'active', action.active);
|
||||
case SET_RECEIVER_TRANSPORT:
|
||||
return {
|
||||
...state,
|
||||
receiver: set(state.receiver, 'transport', action.transport)
|
||||
};
|
||||
case SET_RECEIVER_ENABLED:
|
||||
return {
|
||||
...state,
|
||||
receiver: set(state.receiver, 'enabled', action.enabled)
|
||||
};
|
||||
case SET_REQUESTED_PARTICIPANT:
|
||||
return {
|
||||
...state,
|
||||
controller: set(state.controller, 'requestedParticipant', action.requestedParticipant)
|
||||
};
|
||||
case SET_CONTROLLED_PARTICIPANT:
|
||||
return {
|
||||
...state,
|
||||
controller: set(state.controller, 'controlled', action.controlled)
|
||||
};
|
||||
case SET_CONTROLLER:
|
||||
return {
|
||||
...state,
|
||||
receiver: set(state.receiver, 'controller', action.controller)
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
);
|
||||
33
react/features/remote-control/subscriber.js
Normal file
33
react/features/remote-control/subscriber.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// @flow
|
||||
|
||||
import { StateListenerRegistry } from '../base/redux';
|
||||
|
||||
import { resume, pause } from './actions';
|
||||
|
||||
/**
|
||||
* Listens for large video participant ID changes.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => {
|
||||
const { participantId } = state['features/large-video'];
|
||||
const { controller } = state['features/remote-control'];
|
||||
const { controlled } = controller;
|
||||
|
||||
if (!controlled) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return controlled === participantId;
|
||||
},
|
||||
/* listener */ (isControlledParticipantOnStage, { dispatch }) => {
|
||||
if (isControlledParticipantOnStage === true) {
|
||||
dispatch(resume());
|
||||
} else if (isControlledParticipantOnStage === false) {
|
||||
dispatch(pause());
|
||||
}
|
||||
|
||||
// else {
|
||||
// isControlledParticipantOnStage === undefined. Ignore!
|
||||
// }
|
||||
}
|
||||
);
|
||||
@@ -4,12 +4,15 @@ import React, { Component } from 'react';
|
||||
|
||||
import { Icon, IconMenuThumb } from '../../../base/icons';
|
||||
import { MEDIA_TYPE } from '../../../base/media';
|
||||
import { getLocalParticipant, PARTICIPANT_ROLE } from '../../../base/participants';
|
||||
import { getLocalParticipant, getParticipantById, PARTICIPANT_ROLE } from '../../../base/participants';
|
||||
import { Popover } from '../../../base/popover';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { isRemoteTrackMuted } from '../../../base/tracks';
|
||||
import { requestRemoteControl, stopController } from '../../../remote-control';
|
||||
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
|
||||
|
||||
import MuteEveryoneElseButton from './MuteEveryoneElseButton';
|
||||
import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton';
|
||||
|
||||
import {
|
||||
GrantModeratorButton,
|
||||
@@ -50,6 +53,24 @@ type Props = {
|
||||
*/
|
||||
_isModerator: boolean,
|
||||
|
||||
/**
|
||||
* The position relative to the trigger the remote menu should display
|
||||
* from. Valid values are those supported by AtlasKit
|
||||
* {@code InlineDialog}.
|
||||
*/
|
||||
_menuPosition: string,
|
||||
|
||||
/**
|
||||
* The current state of the participant's remote control session.
|
||||
*/
|
||||
_remoteControlState: number,
|
||||
|
||||
|
||||
/**
|
||||
* The redux dispatch function.
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* A value between 0 and 1 indicating the volume of the participant's
|
||||
* audio element.
|
||||
@@ -61,34 +82,16 @@ type Props = {
|
||||
*/
|
||||
onMenuDisplay: Function,
|
||||
|
||||
/**
|
||||
* Callback to invoke choosing to start a remote control session with
|
||||
* the participant.
|
||||
*/
|
||||
onRemoteControlToggle: Function,
|
||||
|
||||
/**
|
||||
* Callback to invoke when changing the level of the participant's
|
||||
* audio element.
|
||||
*/
|
||||
onVolumeChange: Function,
|
||||
|
||||
/**
|
||||
* The position relative to the trigger the remote menu should display
|
||||
* from. Valid values are those supported by AtlasKit
|
||||
* {@code InlineDialog}.
|
||||
*/
|
||||
menuPosition: string,
|
||||
|
||||
/**
|
||||
* The ID for the participant on which the remote video menu will act.
|
||||
*/
|
||||
participantID: string,
|
||||
|
||||
/**
|
||||
* The current state of the participant's remote control session.
|
||||
*/
|
||||
remoteControlState: number
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -138,7 +141,7 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
||||
<Popover
|
||||
content = { content }
|
||||
onPopoverOpen = { this._onShowRemoteMenu }
|
||||
position = { this.props.menuPosition }>
|
||||
position = { this.props._menuPosition }>
|
||||
<span
|
||||
className = 'popover-trigger remote-video-menu-trigger'>
|
||||
<Icon
|
||||
@@ -175,10 +178,10 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
||||
_disableRemoteMute,
|
||||
_isAudioMuted,
|
||||
_isModerator,
|
||||
dispatch,
|
||||
initialVolumeValue,
|
||||
onRemoteControlToggle,
|
||||
onVolumeChange,
|
||||
remoteControlState,
|
||||
_remoteControlState,
|
||||
participantID
|
||||
} = this.props;
|
||||
|
||||
@@ -214,13 +217,21 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
if (remoteControlState) {
|
||||
if (_remoteControlState) {
|
||||
let onRemoteControlToggle = null;
|
||||
|
||||
if (_remoteControlState === REMOTE_CONTROL_MENU_STATES.STARTED) {
|
||||
onRemoteControlToggle = () => dispatch(stopController(true));
|
||||
} else if (_remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED) {
|
||||
onRemoteControlToggle = () => dispatch(requestRemoteControl(participantID));
|
||||
}
|
||||
|
||||
buttons.push(
|
||||
<RemoteControlButton
|
||||
key = 'remote-control'
|
||||
onClick = { onRemoteControlToggle }
|
||||
participantID = { participantID }
|
||||
remoteControlState = { remoteControlState } />
|
||||
remoteControlState = { _remoteControlState } />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -258,7 +269,12 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
||||
* @param {Object} ownProps - The own props of the component.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _isModerator: boolean
|
||||
* _isAudioMuted: boolean,
|
||||
* _isModerator: boolean,
|
||||
* _disableKick: boolean,
|
||||
* _disableRemoteMute: boolean,
|
||||
* _menuPosition: string,
|
||||
* _remoteControlState: number
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state, ownProps) {
|
||||
@@ -267,12 +283,46 @@ function _mapStateToProps(state, ownProps) {
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
const { remoteVideoMenu = {}, disableRemoteMute } = state['features/base/config'];
|
||||
const { disableKick } = remoteVideoMenu;
|
||||
let _remoteControlState = null;
|
||||
const participant = getParticipantById(state, participantID);
|
||||
const _isRemoteControlSessionActive = participant?.remoteControlSessionStatus ?? false;
|
||||
const _supportsRemoteControl = participant?.supportsRemoteControl ?? false;
|
||||
const { active, controller } = state['features/remote-control'];
|
||||
const { requestedParticipant, controlled } = controller;
|
||||
const activeParticipant = requestedParticipant || controlled;
|
||||
|
||||
if (_supportsRemoteControl
|
||||
&& ((!active && !_isRemoteControlSessionActive) || activeParticipant === participantID)) {
|
||||
if (requestedParticipant === participantID) {
|
||||
_remoteControlState = REMOTE_CONTROL_MENU_STATES.REQUESTING;
|
||||
} else if (controlled) {
|
||||
_remoteControlState = REMOTE_CONTROL_MENU_STATES.STARTED;
|
||||
} else {
|
||||
_remoteControlState = REMOTE_CONTROL_MENU_STATES.NOT_STARTED;
|
||||
}
|
||||
}
|
||||
|
||||
const currentLayout = getCurrentLayout(state);
|
||||
let _menuPosition;
|
||||
|
||||
switch (currentLayout) {
|
||||
case LAYOUTS.TILE_VIEW:
|
||||
_menuPosition = 'left top';
|
||||
break;
|
||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
|
||||
_menuPosition = 'left bottom';
|
||||
break;
|
||||
default:
|
||||
_menuPosition = 'top center';
|
||||
}
|
||||
|
||||
return {
|
||||
_isAudioMuted: isRemoteTrackMuted(tracks, MEDIA_TYPE.AUDIO, participantID) || false,
|
||||
_isModerator: Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR),
|
||||
_disableKick: Boolean(disableKick),
|
||||
_disableRemoteMute: Boolean(disableRemoteMute)
|
||||
_disableRemoteMute: Boolean(disableRemoteMute),
|
||||
_remoteControlState,
|
||||
_menuPosition
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -227,7 +227,7 @@ end
|
||||
-- everything.
|
||||
function is_feature_allowed(session, feature)
|
||||
if (session.jitsi_meet_context_features == nil
|
||||
or session.jitsi_meet_context_features[feature] == "true" or session.jitsi_meet_context_features[feature] == true) then
|
||||
or session.jitsi_meet_context_features[feature] == "true") then
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* Events fired from the remote control module through the EventEmitter.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Notifies about remote control active session status changes.
|
||||
*/
|
||||
export const ACTIVE_CHANGED = 'active-changed';
|
||||
@@ -7,7 +7,7 @@
|
||||
<body>
|
||||
<div class="error_page">
|
||||
<h2>404 Not Found</h2>
|
||||
<p class="error_page__message">You can create a new conversation <a class="link" onclick="window.location = window.location.protocol + '//' + window.location.hostname">here</a></p>
|
||||
<p class="error_page__message">You can create new conversation <a class="link" href="/">here</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user