mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-01-08 07:40:18 +00:00
Compare commits
146 Commits
saghul-pat
...
4835
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07f16a7a51 | ||
|
|
0e7bde2ff0 | ||
|
|
e9d00acad8 | ||
|
|
911aaed052 | ||
|
|
5fd9dc74e4 | ||
|
|
eb68467e15 | ||
|
|
6a5d6afc94 | ||
|
|
2a9b6a7d28 | ||
|
|
67beafc9af | ||
|
|
6175a5cad5 | ||
|
|
678f3e232b | ||
|
|
f3c1b8ac08 | ||
|
|
f225ce886f | ||
|
|
6a4417c6cc | ||
|
|
6b66c8dd20 | ||
|
|
d3680bbebd | ||
|
|
7933d4b4d6 | ||
|
|
e7297714c6 | ||
|
|
8da154b185 | ||
|
|
3c94a5ccfd | ||
|
|
78d4af6bf2 | ||
|
|
33fc3833f9 | ||
|
|
b179542c39 | ||
|
|
49c38a73aa | ||
|
|
fc27300132 | ||
|
|
57ecdff9eb | ||
|
|
effa878fa4 | ||
|
|
9d4e49a5af | ||
|
|
6b4d25c0d3 | ||
|
|
2f5ab2757f | ||
|
|
bde26c4fbb | ||
|
|
68c2c9be40 | ||
|
|
5b21051c6b | ||
|
|
8806269af0 | ||
|
|
3a8bd852b2 | ||
|
|
f50872285d | ||
|
|
e937e99284 | ||
|
|
3972e076f0 | ||
|
|
81cf79e643 | ||
|
|
a22d054b10 | ||
|
|
7fce181080 | ||
|
|
92735478d1 | ||
|
|
7dabfc21b4 | ||
|
|
1395f84550 | ||
|
|
d080460f9b | ||
|
|
61567f47c0 | ||
|
|
4f3058eae2 | ||
|
|
3a073d9af4 | ||
|
|
aef0287605 | ||
|
|
bb19567efa | ||
|
|
79ab973694 | ||
|
|
7046785ca3 | ||
|
|
b817bd19d5 | ||
|
|
817d54b0b9 | ||
|
|
3f0bb6818c | ||
|
|
4fa47c8070 | ||
|
|
0dcb8a025b | ||
|
|
8defaa9aec | ||
|
|
d214079148 | ||
|
|
096ee3cb53 | ||
|
|
fd606896b8 | ||
|
|
226581a81a | ||
|
|
e1c5b1e626 | ||
|
|
831c5ba59d | ||
|
|
bad1bc91cf | ||
|
|
30d0aabaca | ||
|
|
22b6d32174 | ||
|
|
31ace267ce | ||
|
|
194d357005 | ||
|
|
c2ad06c5e6 | ||
|
|
e40b02ab3c | ||
|
|
2587eefefc | ||
|
|
b87c433e99 | ||
|
|
751644db16 | ||
|
|
c508572cc5 | ||
|
|
b86c271a80 | ||
|
|
5efbe5f0ec | ||
|
|
2784c43a1b | ||
|
|
f5a34183e9 | ||
|
|
29f5d87d77 | ||
|
|
ab6790bdaa | ||
|
|
2e308d67d8 | ||
|
|
91ba835f78 | ||
|
|
2643029ac8 | ||
|
|
e40e078a29 | ||
|
|
6df5a4cf31 | ||
|
|
c7c7d7a155 | ||
|
|
8f06866646 | ||
|
|
b559cb8ec6 | ||
|
|
30a2e84da1 | ||
|
|
3122983000 | ||
|
|
407021e258 | ||
|
|
1a62a7b1cc | ||
|
|
0ee03f1538 | ||
|
|
572beb8382 | ||
|
|
d0d32b8a19 | ||
|
|
82ff988c18 | ||
|
|
8fa5d09612 | ||
|
|
508f1e0da9 | ||
|
|
dcda89012e | ||
|
|
d93a402cc2 | ||
|
|
b7b260f4c9 | ||
|
|
4db3f04c0c | ||
|
|
126a2bd0d7 | ||
|
|
29bbcf8590 | ||
|
|
5c46b03251 | ||
|
|
eeb5abbbe8 | ||
|
|
49583b611c | ||
|
|
9e29dd063f | ||
|
|
a2e2d31dfd | ||
|
|
62c06441b1 | ||
|
|
f718a3e050 | ||
|
|
899968d3a9 | ||
|
|
696f509f18 | ||
|
|
430591bd1e | ||
|
|
8ee324b37f | ||
|
|
399d6b6a4b | ||
|
|
ffad21cb59 | ||
|
|
ce6debac45 | ||
|
|
5d8bf0c1e7 | ||
|
|
7bbd06c9f4 | ||
|
|
79a67049a9 | ||
|
|
b1a3c5cd7b | ||
|
|
9573a615b1 | ||
|
|
5d09102e48 | ||
|
|
cc0ecc1fdd | ||
|
|
cecf324023 | ||
|
|
943d5dca35 | ||
|
|
dd1f8339b1 | ||
|
|
159f59b665 | ||
|
|
23bb824731 | ||
|
|
42d926eef3 | ||
|
|
87a110b9c3 | ||
|
|
a7db7ecaff | ||
|
|
79bb98dab3 | ||
|
|
d22792c9e3 | ||
|
|
41e6af3464 | ||
|
|
f50fd7b7bd | ||
|
|
43761fc398 | ||
|
|
4c39d83ff1 | ||
|
|
e525c2b2ec | ||
|
|
f69a31d9c6 | ||
|
|
67930edae2 | ||
|
|
c11a94f7d7 | ||
|
|
bfd093b0ba | ||
|
|
861935c9d7 |
@@ -5,8 +5,9 @@ build/*
|
||||
# modify as little as possible.
|
||||
flow-typed/*
|
||||
libs/*
|
||||
|
||||
react/features/stream-effects/blur/vendor/*
|
||||
resources/*
|
||||
react/features/stream-effects/virtual-background/vendor/*
|
||||
load-test/*
|
||||
|
||||
# ESLint will by default ignore its own configuration file. However, there does
|
||||
# not seem to be a reason why we will want to risk being inconsistent with our
|
||||
|
||||
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -12,5 +12,6 @@ jobs:
|
||||
with:
|
||||
node-version: '12.x'
|
||||
- run: npm install
|
||||
- run: git status -s --untracked-files=no
|
||||
- run: npm run lint
|
||||
- run: make
|
||||
|
||||
8
Makefile
8
Makefile
@@ -5,8 +5,8 @@ LIBJITSIMEET_DIR = node_modules/lib-jitsi-meet/
|
||||
LIBFLAC_DIR = node_modules/libflacjs/dist/min/
|
||||
OLM_DIR = node_modules/olm
|
||||
RNNOISE_WASM_DIR = node_modules/rnnoise-wasm/dist/
|
||||
TFLITE_WASM = react/features/stream-effects/blur/vendor/tflite
|
||||
MEET_MODELS_DIR = react/features/stream-effects/blur/vendor/models/
|
||||
TFLITE_WASM = react/features/stream-effects/virtual-background/vendor/tflite
|
||||
MEET_MODELS_DIR = react/features/stream-effects/virtual-background/vendor/models/
|
||||
NODE_SASS = ./node_modules/.bin/sass
|
||||
NPM = npm
|
||||
OUTPUT_DIR = .
|
||||
@@ -51,10 +51,6 @@ deploy-appbundle:
|
||||
$(OUTPUT_DIR)/analytics-ga.js \
|
||||
$(BUILD_DIR)/analytics-ga.min.js \
|
||||
$(BUILD_DIR)/analytics-ga.min.map \
|
||||
$(BUILD_DIR)/video-blur-effect.min.js \
|
||||
$(BUILD_DIR)/video-blur-effect.min.map \
|
||||
$(BUILD_DIR)/rnnoise-processor.min.js \
|
||||
$(BUILD_DIR)/rnnoise-processor.min.map \
|
||||
$(BUILD_DIR)/close3.min.js \
|
||||
$(BUILD_DIR)/close3.min.map \
|
||||
$(DEPLOY_DIR)
|
||||
|
||||
@@ -122,7 +122,7 @@ gradle.projectsEvaluated {
|
||||
android.applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
output.getProcessManifestProvider().get().doLast {
|
||||
def outputDir = manifestOutputDirectory.get().asFile
|
||||
def outputDir = multiApkManifestOutputDirectory.get().asFile
|
||||
def manifestPath = new File(outputDir, 'AndroidManifest.xml')
|
||||
def charset = 'UTF-8'
|
||||
def text
|
||||
|
||||
@@ -26,4 +26,4 @@ android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
appVersion=21.0.0
|
||||
sdkVersion=3.1.0
|
||||
sdkVersion=3.2.0
|
||||
|
||||
@@ -70,7 +70,6 @@ dependencies {
|
||||
implementation project(':react-native-default-preference')
|
||||
implementation project(':react-native-immersive')
|
||||
implementation project(':react-native-keep-awake')
|
||||
implementation project(':react-native-linear-gradient')
|
||||
implementation project(':react-native-sound')
|
||||
implementation project(':react-native-svg')
|
||||
implementation project(':react-native-webrtc')
|
||||
|
||||
@@ -32,6 +32,7 @@ import com.facebook.react.modules.core.PermissionListener;
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
|
||||
import java.util.HashMap;
|
||||
import android.app.Activity;
|
||||
|
||||
/**
|
||||
* A base activity for SDK users to embed. It uses {@link JitsiMeetFragment} to do the heavy
|
||||
@@ -58,6 +59,9 @@ public class JitsiMeetActivity extends FragmentActivity
|
||||
Intent intent = new Intent(context, JitsiMeetActivity.class);
|
||||
intent.setAction(ACTION_JITSI_MEET_CONFERENCE);
|
||||
intent.putExtra(JITSI_MEET_CONFERENCE_OPTIONS, options);
|
||||
if (!(context instanceof Activity)) {
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
}
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
|
||||
@@ -132,6 +132,7 @@ public class JitsiMeetOngoingConferenceService extends Service
|
||||
public void onCurrentConferenceChanged(String conferenceUrl) {
|
||||
if (conferenceUrl == null) {
|
||||
stopSelf();
|
||||
OngoingNotification.resetStartingtime();
|
||||
JitsiMeetLogger.i(TAG + "Service stopped");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ class OngoingNotification {
|
||||
private static final String CHANNEL_NAME = "Ongoing Conference Notifications";
|
||||
|
||||
static final int NOTIFICATION_ID = new Random().nextInt(99999) + 10000;
|
||||
private static long startingTime = 0;
|
||||
|
||||
static void createOngoingConferenceNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
@@ -85,6 +86,10 @@ class OngoingNotification {
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID);
|
||||
|
||||
if (startingTime == 0) {
|
||||
startingTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
builder
|
||||
.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||
.setContentTitle(context.getString(R.string.ongoing_notification_title))
|
||||
@@ -92,6 +97,8 @@ class OngoingNotification {
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true)
|
||||
.setWhen(startingTime)
|
||||
.setUsesChronometer(true)
|
||||
.setAutoCancel(false)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setOnlyAlertOnce(true)
|
||||
@@ -110,6 +117,10 @@ class OngoingNotification {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
static void resetStartingtime() {
|
||||
startingTime = 0;
|
||||
}
|
||||
|
||||
private static NotificationCompat.Action createAction(Context context, JitsiMeetOngoingConferenceService.Action action, @StringRes int titleId) {
|
||||
Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
|
||||
intent.setAction(action.getName());
|
||||
|
||||
@@ -178,7 +178,6 @@ class ReactInstanceManagerHolder {
|
||||
|
||||
List<ReactPackage> packages
|
||||
= new ArrayList<>(Arrays.asList(
|
||||
new com.BV.LinearGradient.LinearGradientPackage(),
|
||||
new com.calendarevents.CalendarEventsPackage(),
|
||||
new com.corbt.keepawake.KCKeepAwakePackage(),
|
||||
new com.facebook.react.shell.MainReactPackage(),
|
||||
|
||||
@@ -19,8 +19,6 @@ include ':react-native-immersive'
|
||||
project(':react-native-immersive').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-immersive/android')
|
||||
include ':react-native-keep-awake'
|
||||
project(':react-native-keep-awake').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-keep-awake/android')
|
||||
include ':react-native-linear-gradient'
|
||||
project(':react-native-linear-gradient').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-linear-gradient/android')
|
||||
include ':react-native-sound'
|
||||
project(':react-native-sound').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sound/android')
|
||||
include ':react-native-splash-screen'
|
||||
|
||||
1
app.js
1
app.js
@@ -1,7 +1,6 @@
|
||||
/* application specific logic */
|
||||
|
||||
import 'jquery';
|
||||
import 'jquery-contextmenu';
|
||||
import 'jQuery-Impromptu';
|
||||
|
||||
import 'olm';
|
||||
|
||||
181
conference.js
181
conference.js
@@ -99,6 +99,7 @@ import {
|
||||
destroyLocalTracks,
|
||||
getLocalJitsiAudioTrack,
|
||||
getLocalJitsiVideoTrack,
|
||||
getLocalTracks,
|
||||
isLocalCameraTrackMuted,
|
||||
isLocalTrackMuted,
|
||||
isUserInteractionRequiredForUnmute,
|
||||
@@ -115,7 +116,7 @@ import {
|
||||
submitFeedback
|
||||
} from './react/features/feedback';
|
||||
import { showNotification } from './react/features/notifications';
|
||||
import { mediaPermissionPromptVisibilityChanged } from './react/features/overlay';
|
||||
import { mediaPermissionPromptVisibilityChanged, toggleSlowGUMOverlay } from './react/features/overlay';
|
||||
import { suspendDetected } from './react/features/power-monitor';
|
||||
import {
|
||||
initPrejoin,
|
||||
@@ -125,7 +126,7 @@ import {
|
||||
} 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 { setSharedVideoStatus } from './react/features/shared-video/actions';
|
||||
import { AudioMixerEffect } from './react/features/stream-effects/audio-mixer/AudioMixerEffect';
|
||||
import { createPresenterEffect } from './react/features/stream-effects/presenter';
|
||||
import { endpointMessageReceived } from './react/features/subtitles';
|
||||
@@ -473,18 +474,13 @@ export default {
|
||||
*/
|
||||
createInitialLocalTracks(options = {}) {
|
||||
const errors = {};
|
||||
|
||||
// Always get a handle on the audio input device so that we have statistics (such as "No audio input" or
|
||||
// "Are you trying to speak?" ) even if the user joins the conference muted.
|
||||
const initialDevices = config.disableInitialGUM ? [] : [ 'audio' ];
|
||||
const requestedAudio = !config.disableInitialGUM;
|
||||
let requestedVideo = false;
|
||||
|
||||
// Always get a handle on the audio input device so that we have statistics even if the user joins the
|
||||
// conference muted. Previous implementation would only acquire the handle when the user first unmuted,
|
||||
// which would results in statistics ( such as "No audio input" or "Are you trying to speak?") being available
|
||||
// only after that point.
|
||||
if (options.startWithAudioMuted) {
|
||||
this.muteAudio(true, true);
|
||||
}
|
||||
|
||||
if (!config.disableInitialGUM
|
||||
&& !options.startWithVideoMuted
|
||||
&& !options.startAudioOnly
|
||||
@@ -502,10 +498,15 @@ export default {
|
||||
);
|
||||
}
|
||||
|
||||
JitsiMeetJS.mediaDevices.addEventListener(
|
||||
JitsiMediaDevicesEvents.SLOW_GET_USER_MEDIA,
|
||||
() => APP.store.dispatch(toggleSlowGUMOverlay(true))
|
||||
);
|
||||
|
||||
let tryCreateLocalTracks;
|
||||
|
||||
// On Electron there is no permission prompt for granting permissions. That's why we don't need to
|
||||
// spend much time displaying the overlay screen. If GUM is not resolved withing 15 seconds it will
|
||||
// spend much time displaying the overlay screen. If GUM is not resolved within 15 seconds it will
|
||||
// probably never resolve.
|
||||
const timeout = browser.isElectron() ? 15000 : 60000;
|
||||
|
||||
@@ -519,8 +520,10 @@ export default {
|
||||
|
||||
return createLocalTracksF({
|
||||
devices: [ 'audio' ],
|
||||
timeout
|
||||
}, true)
|
||||
timeout,
|
||||
firePermissionPromptIsShownEvent: true,
|
||||
fireSlowPromiseEvent: true
|
||||
})
|
||||
.then(([ audioStream ]) =>
|
||||
[ desktopStream, audioStream ])
|
||||
.catch(error => {
|
||||
@@ -536,8 +539,10 @@ export default {
|
||||
return requestedAudio
|
||||
? createLocalTracksF({
|
||||
devices: [ 'audio' ],
|
||||
timeout
|
||||
}, true)
|
||||
timeout,
|
||||
firePermissionPromptIsShownEvent: true,
|
||||
fireSlowPromiseEvent: true
|
||||
})
|
||||
: [];
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -551,8 +556,10 @@ export default {
|
||||
} else {
|
||||
tryCreateLocalTracks = createLocalTracksF({
|
||||
devices: initialDevices,
|
||||
timeout
|
||||
}, true)
|
||||
timeout,
|
||||
firePermissionPromptIsShownEvent: true,
|
||||
fireSlowPromiseEvent: true
|
||||
})
|
||||
.catch(err => {
|
||||
if (requestedAudio && requestedVideo) {
|
||||
|
||||
@@ -561,7 +568,7 @@ export default {
|
||||
|
||||
if (err.name === JitsiTrackErrors.TIMEOUT && !browser.isElectron()) {
|
||||
// In this case we expect that the permission prompt is still visible. There is no point of
|
||||
// executing GUM with different source. Also at the time of writting the following
|
||||
// executing GUM with different source. Also at the time of writing the following
|
||||
// inconsistency have been noticed in some browsers - if the permissions prompt is visible
|
||||
// and another GUM is executed the prompt does not change its content but if the user
|
||||
// clicks allow the user action isassociated with the latest GUM call.
|
||||
@@ -574,8 +581,10 @@ export default {
|
||||
return (
|
||||
createLocalTracksF({
|
||||
devices: [ 'audio' ],
|
||||
timeout
|
||||
}, true));
|
||||
timeout,
|
||||
firePermissionPromptIsShownEvent: true,
|
||||
fireSlowPromiseEvent: true
|
||||
}));
|
||||
} else if (requestedAudio && !requestedVideo) {
|
||||
errors.audioOnlyError = err;
|
||||
|
||||
@@ -598,8 +607,9 @@ export default {
|
||||
return requestedVideo
|
||||
? createLocalTracksF({
|
||||
devices: [ 'video' ],
|
||||
timeout
|
||||
}, true)
|
||||
firePermissionPromptIsShownEvent: true,
|
||||
fireSlowPromiseEvent: true
|
||||
})
|
||||
: [];
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -615,10 +625,11 @@ export default {
|
||||
|
||||
// Hide the permissions prompt/overlay as soon as the tracks are
|
||||
// created. Don't wait for the connection to be made, since in some
|
||||
// cases, when auth is rquired, for instance, that won't happen until
|
||||
// cases, when auth is required, for instance, that won't happen until
|
||||
// the user inputs their credentials, but the dialog would be
|
||||
// overshadowed by the overlay.
|
||||
tryCreateLocalTracks.then(tracks => {
|
||||
APP.store.dispatch(toggleSlowGUMOverlay(false));
|
||||
APP.store.dispatch(mediaPermissionPromptVisibilityChanged(false));
|
||||
|
||||
return tracks;
|
||||
@@ -809,12 +820,16 @@ export default {
|
||||
return this._setLocalAudioVideoStreams(tracks);
|
||||
}
|
||||
|
||||
const [ tracks, con ] = await this.createInitialLocalTracksAndConnect(
|
||||
roomName, initialOptions);
|
||||
const [ tracks, con ] = await this.createInitialLocalTracksAndConnect(roomName, initialOptions);
|
||||
let localTracks = tracks;
|
||||
|
||||
this._initDeviceList(true);
|
||||
|
||||
return this.startConference(con, tracks);
|
||||
if (initialOptions.startWithAudioMuted) {
|
||||
localTracks = localTracks.filter(track => track.getType() !== MEDIA_TYPE.AUDIO);
|
||||
}
|
||||
|
||||
return this.startConference(con, localTracks);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -882,7 +897,7 @@ export default {
|
||||
showUI && APP.store.dispatch(notifyMicError(error));
|
||||
};
|
||||
|
||||
createLocalTracksF({ devices: [ 'audio' ] }, false)
|
||||
createLocalTracksF({ devices: [ 'audio' ] })
|
||||
.then(([ audioTrack ]) => audioTrack)
|
||||
.catch(error => {
|
||||
maybeShowErrorDialog(error);
|
||||
@@ -996,7 +1011,7 @@ export default {
|
||||
//
|
||||
// FIXME when local track creation is moved to react/redux
|
||||
// it should take care of the use case described above
|
||||
createLocalTracksF({ devices: [ 'video' ] }, false)
|
||||
createLocalTracksF({ devices: [ 'video' ] })
|
||||
.then(([ videoTrack ]) => videoTrack)
|
||||
.catch(error => {
|
||||
// FIXME should send some feedback to the API on error ?
|
||||
@@ -1005,7 +1020,11 @@ export default {
|
||||
// Rollback the video muted status by using null track
|
||||
return null;
|
||||
})
|
||||
.then(videoTrack => this.useVideoStream(videoTrack));
|
||||
.then(videoTrack => {
|
||||
logger.debug(`muteVideo: calling useVideoStream for track: ${videoTrack}`);
|
||||
|
||||
return this.useVideoStream(videoTrack);
|
||||
});
|
||||
} else {
|
||||
// FIXME show error dialog if it fails (should be handled by react)
|
||||
muteLocalVideo(mute);
|
||||
@@ -1305,7 +1324,11 @@ export default {
|
||||
this._getConferenceOptions());
|
||||
|
||||
APP.store.dispatch(conferenceWillJoin(room));
|
||||
this._setLocalAudioVideoStreams(localTracks);
|
||||
|
||||
// Filter out the tracks that are muted.
|
||||
const tracks = localTracks.filter(track => !track.isMuted());
|
||||
|
||||
this._setLocalAudioVideoStreams(tracks);
|
||||
this._room = room; // FIXME do not use this
|
||||
|
||||
sendLocalParticipant(APP.store, room);
|
||||
@@ -1324,8 +1347,11 @@ export default {
|
||||
if (track.isAudioTrack()) {
|
||||
return this.useAudioStream(track);
|
||||
} else if (track.isVideoTrack()) {
|
||||
logger.debug(`_setLocalAudioVideoStreams is calling useVideoStream with track: ${track}`);
|
||||
|
||||
return this.useVideoStream(track);
|
||||
}
|
||||
|
||||
logger.error(
|
||||
'Ignored not an audio nor a video track: ', track);
|
||||
|
||||
@@ -1345,6 +1371,8 @@ export default {
|
||||
* @returns {Promise}
|
||||
*/
|
||||
useVideoStream(newTrack) {
|
||||
logger.debug(`useVideoStream: ${newTrack}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
_replaceLocalVideoTrackQueue.enqueue(onFinish => {
|
||||
const state = APP.store.getState();
|
||||
@@ -1354,24 +1382,30 @@ export default {
|
||||
if (isPrejoinPageVisible(state)) {
|
||||
const oldTrack = getLocalJitsiVideoTrack(state);
|
||||
|
||||
logger.debug(`useVideoStream on the prejoin screen: Replacing ${oldTrack} with ${newTrack}`);
|
||||
|
||||
return APP.store.dispatch(replaceLocalTrack(oldTrack, newTrack))
|
||||
.then(resolve)
|
||||
.catch(reject)
|
||||
.catch(error => {
|
||||
logger.error(`useVideoStream failed on the prejoin screen: ${error}`);
|
||||
reject(error);
|
||||
})
|
||||
.then(onFinish);
|
||||
}
|
||||
|
||||
logger.debug(`useVideoStream: Replacing ${this.localVideo} with ${newTrack}`);
|
||||
APP.store.dispatch(
|
||||
replaceLocalTrack(this.localVideo, newTrack, room))
|
||||
replaceLocalTrack(this.localVideo, newTrack, room))
|
||||
.then(() => {
|
||||
this.localVideo = newTrack;
|
||||
this._setSharingScreen(newTrack);
|
||||
if (newTrack) {
|
||||
APP.UI.addLocalVideoStream(newTrack);
|
||||
}
|
||||
this.setVideoMuteStatus(this.isLocalVideoMuted());
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject)
|
||||
.catch(error => {
|
||||
logger.error(`useVideoStream failed: ${error}`);
|
||||
reject(error);
|
||||
})
|
||||
.then(onFinish);
|
||||
});
|
||||
});
|
||||
@@ -1518,7 +1552,11 @@ export default {
|
||||
|
||||
if (didHaveVideo) {
|
||||
promise = promise.then(() => createLocalTracksF({ devices: [ 'video' ] }))
|
||||
.then(([ stream ]) => this.useVideoStream(stream))
|
||||
.then(([ stream ]) => {
|
||||
logger.debug(`_turnScreenSharingOff using ${stream} for useVideoStream`);
|
||||
|
||||
return this.useVideoStream(stream);
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('failed to switch back to local video', error);
|
||||
|
||||
@@ -1529,7 +1567,11 @@ export default {
|
||||
);
|
||||
});
|
||||
} else {
|
||||
promise = promise.then(() => this.useVideoStream(null));
|
||||
promise = promise.then(() => {
|
||||
logger.debug('_turnScreenSharingOff using null for useVideoStream');
|
||||
|
||||
return this.useVideoStream(null);
|
||||
});
|
||||
}
|
||||
|
||||
return promise.then(
|
||||
@@ -1540,6 +1582,8 @@ export default {
|
||||
},
|
||||
error => {
|
||||
this.videoSwitchInProgress = false;
|
||||
logger.error(`_turnScreenSharingOff failed: ${error}`);
|
||||
|
||||
throw error;
|
||||
});
|
||||
},
|
||||
@@ -1560,6 +1604,7 @@ export default {
|
||||
* @return {Promise.<T>}
|
||||
*/
|
||||
async toggleScreenSharing(toggle = !this._untoggleScreenSharing, options = {}) {
|
||||
logger.debug(`toggleScreenSharing: ${toggle}`);
|
||||
if (this.videoSwitchInProgress) {
|
||||
return Promise.reject('Switch in progress.');
|
||||
}
|
||||
@@ -1626,6 +1671,8 @@ export default {
|
||||
desktopVideoStream.on(
|
||||
JitsiTrackEvents.LOCAL_TRACK_STOPPED,
|
||||
() => {
|
||||
logger.debug(`Local screensharing track stopped. ${this.isSharingScreen}`);
|
||||
|
||||
// If the stream was stopped during screen sharing
|
||||
// session then we should switch back to video.
|
||||
this.isSharingScreen
|
||||
@@ -1729,7 +1776,7 @@ export default {
|
||||
};
|
||||
}
|
||||
|
||||
// Apply the contraints on the desktop track.
|
||||
// Apply the constraints on the desktop track.
|
||||
try {
|
||||
await this.localVideo.track.applyConstraints(desktopResizeConstraints);
|
||||
} catch (err) {
|
||||
@@ -1790,6 +1837,7 @@ export default {
|
||||
const desktopVideoStream = streams.find(stream => stream.getType() === MEDIA_TYPE.VIDEO);
|
||||
|
||||
if (desktopVideoStream) {
|
||||
logger.debug(`_switchToScreenSharing is using ${desktopVideoStream} for useVideoStream`);
|
||||
await this.useVideoStream(desktopVideoStream);
|
||||
}
|
||||
|
||||
@@ -1919,7 +1967,7 @@ export default {
|
||||
}
|
||||
|
||||
APP.store.dispatch(updateRemoteParticipantFeatures(user));
|
||||
logger.log(`USER ${id} connnected:`, user);
|
||||
logger.log(`USER ${id} connected:`, user);
|
||||
APP.UI.addUser(user);
|
||||
});
|
||||
|
||||
@@ -1993,7 +2041,11 @@ export default {
|
||||
|
||||
room.on(JitsiConferenceEvents.TRACK_MUTE_CHANGED, (track, participantThatMutedUs) => {
|
||||
if (participantThatMutedUs) {
|
||||
APP.store.dispatch(participantMutedUs(participantThatMutedUs));
|
||||
APP.store.dispatch(participantMutedUs(participantThatMutedUs, track));
|
||||
if (this.isSharingScreen && track.isVideoTrack()) {
|
||||
logger.debug('TRACK_MUTE_CHANGED while screen sharing');
|
||||
this._turnScreenSharingOff(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2145,8 +2197,26 @@ export default {
|
||||
}
|
||||
);
|
||||
room.on(JitsiConferenceEvents.STARTED_MUTED, () => {
|
||||
(room.isStartAudioMuted() || room.isStartVideoMuted())
|
||||
&& APP.UI.notifyInitiallyMuted();
|
||||
const audioMuted = room.isStartAudioMuted();
|
||||
const videoMuted = room.isStartVideoMuted();
|
||||
const localTracks = getLocalTracks(APP.store.getState()['features/base/tracks']);
|
||||
const promises = [];
|
||||
|
||||
APP.store.dispatch(setAudioMuted(audioMuted));
|
||||
APP.store.dispatch(setVideoMuted(videoMuted));
|
||||
|
||||
// Remove the tracks from the peerconnection.
|
||||
for (const track of localTracks) {
|
||||
if (audioMuted && track.jitsiTrack?.getType() === MEDIA_TYPE.AUDIO) {
|
||||
promises.push(this.useAudioStream(null));
|
||||
}
|
||||
if (videoMuted && track.jitsiTrack?.getType() === MEDIA_TYPE.VIDEO) {
|
||||
promises.push(this.useVideoStream(null));
|
||||
}
|
||||
}
|
||||
|
||||
Promise.allSettled(promises)
|
||||
.then(() => APP.UI.notifyInitiallyMuted());
|
||||
});
|
||||
|
||||
room.on(
|
||||
@@ -2197,7 +2267,7 @@ export default {
|
||||
.then(effect => this.localVideo.setEffect(effect))
|
||||
.then(() => {
|
||||
this.setVideoMuteStatus(false);
|
||||
logger.log('switched local video device');
|
||||
logger.log('Switched local video device while screen sharing and the video is unmuted');
|
||||
this._updateVideoDeviceId();
|
||||
})
|
||||
.catch(err => APP.store.dispatch(notifyCameraError(err)));
|
||||
@@ -2206,7 +2276,7 @@ export default {
|
||||
// id for video, dispose the existing presenter track and create a new effect
|
||||
// that can be applied on un-mute.
|
||||
} else if (this.isSharingScreen && videoWasMuted) {
|
||||
logger.log('switched local video device');
|
||||
logger.log('Switched local video device: while screen sharing and the video is muted');
|
||||
const { height } = this.localVideo.track.getSettings();
|
||||
|
||||
this._updateVideoDeviceId();
|
||||
@@ -2233,12 +2303,20 @@ export default {
|
||||
|
||||
return stream;
|
||||
})
|
||||
.then(stream => this.useVideoStream(stream))
|
||||
.then(stream => {
|
||||
logger.log('Switching the local video device.');
|
||||
|
||||
return this.useVideoStream(stream);
|
||||
})
|
||||
.then(() => {
|
||||
logger.log('switched local video device');
|
||||
logger.log('Switched local video device.');
|
||||
this._updateVideoDeviceId();
|
||||
})
|
||||
.catch(err => APP.store.dispatch(notifyCameraError(err)));
|
||||
.catch(error => {
|
||||
logger.error(`Switching the local video device failed: ${error}`);
|
||||
|
||||
return APP.store.dispatch(notifyCameraError(error));
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -2327,7 +2405,11 @@ export default {
|
||||
// There is no guarantee another event will trigger the update
|
||||
// immediately and in all situations, for example because a remote
|
||||
// participant is having connection trouble so no status changes.
|
||||
APP.UI.updateAllVideos();
|
||||
const displayedUserId = APP.UI.getLargeVideoID();
|
||||
|
||||
if (displayedUserId) {
|
||||
APP.UI.updateLargeVideo(displayedUserId, true);
|
||||
}
|
||||
});
|
||||
|
||||
APP.UI.addListener(
|
||||
@@ -2590,6 +2672,7 @@ export default {
|
||||
delete newDevices.videoinput;
|
||||
|
||||
// Removing the current video track in order to force the unmute to select the preferred device.
|
||||
logger.debug('_onDeviceListChanged: Removing the current video track.');
|
||||
this.useVideoStream(null);
|
||||
|
||||
}
|
||||
|
||||
44
config.js
44
config.js
@@ -240,6 +240,12 @@ var config = {
|
||||
// 90: 2
|
||||
// },
|
||||
|
||||
// Provides a way to translate the legacy bridge signaling messages, 'LastNChangedEvent',
|
||||
// 'SelectedEndpointsChangedEvent' and 'ReceiverVideoConstraint' into the new 'ReceiverVideoConstraints' message
|
||||
// that invokes the new bandwidth allocation algorithm in the bridge which is described here
|
||||
// - https://github.com/jitsi/jitsi-videobridge/blob/master/doc/allocation.md.
|
||||
// useNewBandwidthAllocationStrategy: false,
|
||||
|
||||
// Specify the settings for video quality optimizations on the client.
|
||||
// videoQuality: {
|
||||
// // Provides a way to prevent a video codec from being negotiated on the JVB connection. The codec specified
|
||||
@@ -406,7 +412,26 @@ var config = {
|
||||
// enableAutomaticUrlCopy: false,
|
||||
|
||||
// Base URL for a Gravatar-compatible service. Defaults to libravatar.
|
||||
// gravatarBaseURL: 'https://seccdn.libravatar.org/avatar/';
|
||||
// gravatarBaseURL: 'https://seccdn.libravatar.org/avatar/',
|
||||
|
||||
// Moved from interfaceConfig(TOOLBAR_BUTTONS).
|
||||
// The name of the toolbar buttons to display in the toolbar, including the
|
||||
// "More actions" menu. If present, the button will display. Exceptions are
|
||||
// "livestreaming" and "recording" which also require being a moderator and
|
||||
// some other values in config.js to be enabled. Also, the "profile" button will
|
||||
// not display for users with a JWT.
|
||||
// Notes:
|
||||
// - it's impossible to choose which buttons go in the "More actions" menu
|
||||
// - it's impossible to control the placement of buttons
|
||||
// - 'desktop' controls the "Share your screen" button
|
||||
// - if `toolbarButtons` is undefined, we fallback to enabling all buttons on the UI
|
||||
// toolbarButtons: [
|
||||
// 'microphone', 'camera', 'closedcaptions', 'desktop', 'embedmeeting', 'fullscreen',
|
||||
// 'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
|
||||
// 'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
|
||||
// 'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
|
||||
// 'tileview', 'select-background', 'download', 'help', 'mute-everyone', 'mute-video-everyone', 'security'
|
||||
// ],
|
||||
|
||||
// Stats
|
||||
//
|
||||
@@ -431,6 +456,10 @@ var config = {
|
||||
// Enables sending participants' emails (if available) to callstats and other analytics
|
||||
// enableEmailInStats: false,
|
||||
|
||||
// Controls the percentage of automatic feedback shown to participants when callstats is enabled.
|
||||
// The default value is 100%. If set to 0, no automatic feedback will be requested
|
||||
// feedbackPercentage: 100,
|
||||
|
||||
// Privacy
|
||||
//
|
||||
|
||||
@@ -534,6 +563,10 @@ var config = {
|
||||
// Decides whether the start/stop recording audio notifications should play on record.
|
||||
// disableRecordAudioNotification: false,
|
||||
|
||||
// Disables the sounds that play when other participants join or leave the
|
||||
// conference (if set to true, these sounds will not be played).
|
||||
// disableJoinLeaveSounds: false,
|
||||
|
||||
// Information for the chrome extension banner
|
||||
// chromeExtensionBanner: {
|
||||
// // The chrome extension to be installed address
|
||||
@@ -593,6 +626,10 @@ var config = {
|
||||
// the menu has option to flip the locally seen video for local presentations
|
||||
// disableLocalVideoFlip: false,
|
||||
|
||||
// A property used to unset the default flip state of the local video.
|
||||
// When it is set to 'true', the local(self) video will not be mirrored anymore.
|
||||
// doNotFlipLocalVideo: false,
|
||||
|
||||
// Mainly privacy related settings
|
||||
|
||||
// Disables all invite functions from the app (share, invite, dial out...etc)
|
||||
@@ -660,6 +697,11 @@ var config = {
|
||||
// Sets the conference subject
|
||||
// subject: 'Conference Subject',
|
||||
|
||||
// This property is related to the use case when jitsi-meet is used via the IFrame API. When the property is true
|
||||
// jitsi-meet will use the local storage of the host page instead of its own. This option is useful if the browser
|
||||
// is not persisting the local storage inside the iframe.
|
||||
// useHostPageLocalStorage: true,
|
||||
|
||||
// List of undocumented settings used in jitsi-meet
|
||||
/**
|
||||
_immediateReloadThreshold
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/**
|
||||
* Mixins that mimic the way Atlaskit fills the screen with modals at low screen widths.
|
||||
*/
|
||||
@mixin full-size-modal-positioner() {
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@mixin full-size-modal-dialog() {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the @atlaskit/flag container up a little bit so it does not cover the
|
||||
* toolbar with the first notification.
|
||||
@@ -54,6 +72,63 @@
|
||||
* Keep overflow menu within screen vertical bounds and make it scrollable.
|
||||
*/
|
||||
.toolbox-button-wth-dialog > div:nth-child(2) {
|
||||
background: $menuBG;
|
||||
max-height: calc(100vh - #{$newToolbarSizeWithPadding} - 46px);
|
||||
margin-bottom: 4px;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.audio-preview > div:nth-child(2),
|
||||
.video-preview > div:nth-child(2) {
|
||||
margin-bottom: 4px;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* The following selectors keep the chat modal full-size anywhere between 100px
|
||||
* and 580px for desktop or 680px for mobile.
|
||||
*/
|
||||
@media (min-width: 100px) and (max-width: 320px) {
|
||||
.smiley-input {
|
||||
display: none;
|
||||
}
|
||||
.shift-right .focus-lock > div > div {
|
||||
@include full-size-modal-positioner();
|
||||
}
|
||||
|
||||
.shift-right .focus-lock [role="dialog"] {
|
||||
@include full-size-modal-dialog();
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 480px) and (max-width: 580px) {
|
||||
.shift-right .focus-lock > div > div {
|
||||
@include full-size-modal-positioner();
|
||||
}
|
||||
|
||||
.shift-right .focus-lock [role="dialog"] {
|
||||
@include full-size-modal-dialog();
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 580px) {
|
||||
// Override Atlaskit inline style for the modal background.
|
||||
// Important is unfortunately needed for that.
|
||||
.shift-right .focus-lock [role="dialog"][style] {
|
||||
background-color: $chatBackgroundColor !important;
|
||||
}
|
||||
|
||||
// Remove Atlaskit padding from the chat dialog.
|
||||
.shift-right .focus-lock [role="dialog"] > div:first-child > div:nth-child(2) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
div.Tooltip {
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
@@ -1,26 +1,37 @@
|
||||
.audio-preview {
|
||||
display: inline-block;
|
||||
|
||||
&-content {
|
||||
background: #2A3A4B;
|
||||
font-size: 15px;
|
||||
background: $menuBG;
|
||||
border-radius: 3px;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
max-height: 456px;
|
||||
overflow: auto;
|
||||
width: 328px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
&-header {
|
||||
color: #fff;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
margin-top: 8px;
|
||||
padding: 8px 16px;
|
||||
|
||||
&-icon {
|
||||
color: #A4B8D1;
|
||||
display: inline-block;
|
||||
|
||||
svg {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&--bordered {
|
||||
border-bottom: 1px solid #4C4D50;
|
||||
}
|
||||
|
||||
&-text {
|
||||
font-weight: bold;
|
||||
margin-left: 8px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,19 +40,18 @@
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
padding: 12px 0;
|
||||
padding: 8px 0;
|
||||
margin-left: 48px;
|
||||
|
||||
&--selected {
|
||||
background: #1C2025;
|
||||
background: #131519;
|
||||
cursor: initial;
|
||||
margin-left: 0;
|
||||
padding-left: 21px;
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
&-text {
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
display: inline-block;
|
||||
line-height: 24px;
|
||||
text-overflow: ellipsis;
|
||||
@@ -56,12 +66,13 @@
|
||||
|
||||
&:hover {
|
||||
.audio-preview-entry {
|
||||
background: #3F4E5E;
|
||||
background: #36383C;
|
||||
margin-left: 0;
|
||||
padding-left: 48px;
|
||||
|
||||
&--selected {
|
||||
padding-left: 21px;
|
||||
padding-left: 18px;
|
||||
background: $newToolbarBackgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +85,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.audio-preview-entry-text {
|
||||
max-width: 256px;
|
||||
}
|
||||
@@ -84,18 +99,19 @@
|
||||
|
||||
&:hover {
|
||||
.audio-preview-entry {
|
||||
background: #3F4E5E;
|
||||
background: #36383C;
|
||||
margin-left: 0;
|
||||
padding-left: 48px;
|
||||
|
||||
&--selected {
|
||||
padding-left: 21px;
|
||||
background: $newToolbarBackgroundColor;
|
||||
padding-left: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.audio-preview-entry-text {
|
||||
max-width: 196px;
|
||||
max-width: 178px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +126,7 @@
|
||||
|
||||
&--check {
|
||||
background: #31B76A;
|
||||
margin-right: 13px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
&--exclamation {
|
||||
@@ -121,6 +137,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
&-hr {
|
||||
border-top: 1px solid #4C4D50;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&-test-button {
|
||||
display: none;
|
||||
background: #FFF;
|
||||
@@ -129,23 +150,16 @@
|
||||
color: #1C2025;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
line-height: 24px;
|
||||
padding: 4px 16px;
|
||||
padding: 2px 16px;
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 8px;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
&-meter-mic {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 18px;
|
||||
}
|
||||
|
||||
// Override @atlaskit/InlineDialog container which is made with styled components
|
||||
& > div:nth-child(2) {
|
||||
outline: none;
|
||||
padding: 0;
|
||||
top: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,10 @@ body {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.disabled .jitsi-icon svg {
|
||||
fill: #929292;
|
||||
}
|
||||
|
||||
.jitsi-icon.gray svg {
|
||||
fill: #5E6D7A;
|
||||
cursor: pointer;
|
||||
|
||||
174
css/_chat.scss
174
css/_chat.scss
@@ -1,5 +1,5 @@
|
||||
#sideToolbarContainer {
|
||||
background-color: $newToolbarBackgroundColor;
|
||||
background-color: $chatBackgroundColor;
|
||||
box-sizing: border-box;
|
||||
color: #FFF;
|
||||
display: flex;
|
||||
@@ -32,6 +32,13 @@
|
||||
width: $sidebarWidth;
|
||||
word-wrap: break-word;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
& > :first-child {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
}
|
||||
@@ -98,13 +105,12 @@
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
background-color: $chatHeaderBackgroundColor;
|
||||
height: 70px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-end;
|
||||
padding: 16px;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
@@ -116,22 +122,85 @@
|
||||
.jitsi-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.jitsi-icon > svg {
|
||||
fill: #A4B8D1;
|
||||
.chat-input-container {
|
||||
padding: 0 16px 16px;
|
||||
|
||||
&.populated {
|
||||
#chat-input {
|
||||
.send-button {
|
||||
background: #1B67EC;
|
||||
cursor: pointer;
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
&:hover {
|
||||
background: #3D82FB;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #0852D4;
|
||||
}
|
||||
|
||||
path {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#chat-input {
|
||||
border-top: 1px solid $chatInputSeparatorColor;
|
||||
border: 1px solid $chatInputSeparatorColor;
|
||||
display: flex;
|
||||
padding: 5px 10px;
|
||||
padding: 4px;
|
||||
border-radius: 3px;
|
||||
|
||||
&:focus-within {
|
||||
border: 1px solid #619CF4;
|
||||
}
|
||||
|
||||
* {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.send-button-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.send-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border-radius: 3px;
|
||||
|
||||
path {
|
||||
fill: $chatInputSeparatorColor;
|
||||
}
|
||||
}
|
||||
|
||||
.smiley-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#chat-input .smiley-button {
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
&:hover {
|
||||
background-color: #484A4F;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.remoteuser {
|
||||
color: #B8C7E0;
|
||||
}
|
||||
@@ -145,7 +214,7 @@
|
||||
border-radius:0;
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
resize: none;
|
||||
@@ -161,10 +230,55 @@
|
||||
#nickname {
|
||||
text-align: center;
|
||||
color: #9d9d9d;
|
||||
font-size: 18px;
|
||||
margin-top: 30px;
|
||||
left: 5px;
|
||||
right: 5px;
|
||||
font-size: 16px;
|
||||
margin: auto 0;
|
||||
padding: 0 16px;
|
||||
|
||||
input {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
label {
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.enter-chat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 16px;
|
||||
height: 40px;
|
||||
background: #1B67EC;
|
||||
border-radius: 3px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
|
||||
&.disabled {
|
||||
color: #757575;
|
||||
background: #11336E;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-browser {
|
||||
#nickname {
|
||||
input {
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.enter-chat {
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
#usermsg {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.chatmessage .usermessage {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.sideToolbarContainer {
|
||||
@@ -174,8 +288,8 @@
|
||||
}
|
||||
|
||||
.display-name {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
@@ -203,6 +317,7 @@
|
||||
|
||||
.usermessage {
|
||||
white-space: pre-wrap;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&.error {
|
||||
@@ -225,12 +340,16 @@
|
||||
}
|
||||
|
||||
.messagecontent {
|
||||
margin: 5px 10px;
|
||||
margin: 8px;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
.smiley {
|
||||
font-size: 14pt;
|
||||
}
|
||||
@@ -266,7 +385,9 @@
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
width: $sidebarWidth;
|
||||
width: calc(#{$sidebarWidth} - 32px);
|
||||
margin-bottom: 5px;
|
||||
margin-left: -5px;
|
||||
|
||||
/**
|
||||
* CSS transitions do not apply for auto dimensions. So to produce the css
|
||||
@@ -281,9 +402,8 @@
|
||||
}
|
||||
|
||||
#smileysContainer {
|
||||
background-color: $newToolbarBackgroundColor;
|
||||
border-bottom: 1px solid;
|
||||
border-top: 1px solid;
|
||||
background-color: $chatBackgroundColor;
|
||||
border-top: 1px solid $chatInputSeparatorColor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,9 +509,9 @@
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin: 16px 16px 24px;
|
||||
margin: 16px;
|
||||
width: calc(100% - 32px);
|
||||
box-sizing: border-box;
|
||||
color: #fff;
|
||||
@@ -402,10 +522,6 @@
|
||||
.jitsi-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.jitsi-icon > svg {
|
||||
fill: #A4B8D1;
|
||||
}
|
||||
}
|
||||
|
||||
#chatconversation {
|
||||
@@ -413,6 +529,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.touchmove-hack {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make header close button more easily tappable on mobile.
|
||||
*/
|
||||
@@ -421,6 +543,6 @@
|
||||
place-items: center;
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
background: #2a3a4b;
|
||||
background: #36383C;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
}
|
||||
|
||||
.drawer-menu {
|
||||
padding: 0 16px;
|
||||
max-height: 50vh;
|
||||
background: #242528;
|
||||
border-radius: 16px 16px 0 0;
|
||||
@@ -24,12 +23,6 @@
|
||||
height: 44px;
|
||||
cursor: pointer;
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
&:hover {
|
||||
background-color: $overflowMenuItemHoverBG;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: none;
|
||||
}
|
||||
@@ -67,13 +60,6 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
&:hover {
|
||||
background-color: $overflowMenuItemHoverBG;
|
||||
color: $overflowMenuItemHoverColor;
|
||||
}
|
||||
}
|
||||
|
||||
&.unclickable {
|
||||
cursor: default;
|
||||
}
|
||||
@@ -88,42 +74,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.beta-tag {
|
||||
background: $overflowMenuItemColor;
|
||||
border-radius: 2px;
|
||||
color: $overflowMenuBG;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
margin-left: 8px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.overflow-menu-item-icon {
|
||||
margin-right: 16px;
|
||||
|
||||
i {
|
||||
display: inline;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
i:hover {
|
||||
background-color: initial;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 24px;
|
||||
max-height: 24px;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: #B8C7E0 !important;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-text {
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -23,44 +23,10 @@
|
||||
flex-direction: row;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
bottom: 10px;
|
||||
transform: translateX(-50%);
|
||||
|
||||
.toolbox-button {
|
||||
&:first-child {
|
||||
.toolbox-icon {
|
||||
border-top-left-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
svg {
|
||||
fill: $hangupColor;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
.toolbox-icon {
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filmstrip-toolbox {
|
||||
flex-direction: column;
|
||||
|
||||
.toolbox-button {
|
||||
&:nth-child(1) {
|
||||
svg {
|
||||
fill: $hangupColor;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbox-icon {
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
**/
|
||||
|
||||
.popupmenu {
|
||||
min-width: 75px;
|
||||
background-color: $menuBG;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
text-align: left;
|
||||
padding: 0px;
|
||||
width: 180px;
|
||||
white-space: nowrap;
|
||||
|
||||
&__item {
|
||||
@@ -39,6 +40,7 @@
|
||||
|
||||
&__text {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@@ -110,6 +112,6 @@ ul.popupmenu {
|
||||
margin: -16px -24px;
|
||||
}
|
||||
|
||||
span.remotevideomenu:hover ul.popupmenu, ul.popupmenu:hover {
|
||||
span.localvideomenu:hover ul.popupmenu, span.remotevideomenu:hover ul.popupmenu, ul.popupmenu:hover {
|
||||
display:block !important;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
text {
|
||||
fill: black;
|
||||
font-size: 26px;
|
||||
font-weight: 400;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,14 +197,6 @@
|
||||
&> div {
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.settings-button-small-icon {
|
||||
right: -8px;
|
||||
|
||||
&--hovered {
|
||||
right: -10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,73 +1,3 @@
|
||||
@mixin small-button-size() {
|
||||
.new-toolbox {
|
||||
.toolbox-content {
|
||||
.button-group-center, .button-group-left, .button-group-right {
|
||||
.toolbox-button {
|
||||
.toolbox-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
.toolbox-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin very-small-button-size() {
|
||||
.new-toolbox {
|
||||
.toolbox-content {
|
||||
.button-group-center, .button-group-left, .button-group-right {
|
||||
.settings-button-small-icon {
|
||||
display: none;
|
||||
}
|
||||
.toolbox-button {
|
||||
.toolbox-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
.toolbox-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin full-size-modal-positioner() {
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@mixin full-size-modal-dialog() {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $verySmallScreen) {
|
||||
.welcome {
|
||||
display: block;
|
||||
@@ -137,21 +67,9 @@
|
||||
}
|
||||
|
||||
.desktop-browser {
|
||||
@media only screen and (max-width: $smallScreen) {
|
||||
@include small-button-size();
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $verySmallScreen) {
|
||||
@include very-small-button-size();
|
||||
}
|
||||
|
||||
&.shift-right {
|
||||
@media only screen and (max-width: $smallScreen + $sidebarWidth) {
|
||||
@include small-button-size()
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $verySmallScreen + $sidebarWidth) {
|
||||
@include very-small-button-size();
|
||||
|
||||
#videoResolutionLabel {
|
||||
display: none;
|
||||
@@ -165,25 +83,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 480px) and (max-width: 580px) {
|
||||
.shift-right .focus-lock > div > div {
|
||||
@include full-size-modal-positioner();
|
||||
}
|
||||
|
||||
.shift-right .focus-lock [role="dialog"] {
|
||||
@include full-size-modal-dialog();
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 580px) and (max-width: 680px) {
|
||||
.mobile-browser {
|
||||
&.shift-right .focus-lock > div > div {
|
||||
@include full-size-modal-positioner();
|
||||
}
|
||||
|
||||
&.shift-right .focus-lock [role="dialog"] {
|
||||
@include full-size-modal-dialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,84 +1,62 @@
|
||||
.settings-button {
|
||||
&-container {
|
||||
position: relative;
|
||||
.settings-button-container {
|
||||
position: relative;
|
||||
|
||||
.toolbox-icon {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #d1dbe8;
|
||||
justify-content: center;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
.toolbox-icon {
|
||||
align-items: center;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
&.disabled, .disabled & {
|
||||
cursor: initial;
|
||||
color: #929292;
|
||||
background-color: #36383c;
|
||||
|
||||
&:hover {
|
||||
background-color: #daebfa;
|
||||
border: 1px solid #daebfa;
|
||||
}
|
||||
|
||||
&.toggled {
|
||||
background: #2a3a4b;
|
||||
border: 1px solid #5e6d7a;
|
||||
|
||||
svg {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #5e6d7a;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled, .disabled & {
|
||||
cursor: initial;
|
||||
color: #fff;
|
||||
background-color: #a4b8d1;
|
||||
|
||||
&:hover {
|
||||
background-color: #a4b8d1;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: #5e6d7a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-small-icon {
|
||||
background: #FFF;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
border-radius: 50%;
|
||||
bottom: 0;
|
||||
box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.25);
|
||||
cursor: pointer;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
right: 4px;
|
||||
width: 16px;
|
||||
|
||||
&> svg {
|
||||
fill: #5e6d7a;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
background-color: #a4b8d1;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&--hovered {
|
||||
bottom: -1px;
|
||||
height: 20px;
|
||||
right: 2px;
|
||||
width: 20px;
|
||||
|
||||
&> svg {
|
||||
margin-top: 6px;
|
||||
background-color: #36383c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-button-small-icon {
|
||||
background: #36383C;
|
||||
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25), 0px 0px 0px 1px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
position: absolute;
|
||||
right: -4px;
|
||||
top: -3px;
|
||||
|
||||
&:hover {
|
||||
background: #F2F3F4;
|
||||
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25), 0px 0px 0px 1px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&> svg {
|
||||
fill: #000;
|
||||
}
|
||||
|
||||
&.settings-button-small-icon--disabled {
|
||||
background: #36383C;
|
||||
|
||||
&> svg {
|
||||
fill: #929292;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&> svg {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
background-color: #36383c;
|
||||
cursor: default;
|
||||
|
||||
&> svg {
|
||||
fill: #929292;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,9 +33,6 @@
|
||||
|
||||
&.visible {
|
||||
bottom: 0;
|
||||
.toolbox-background {
|
||||
bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
&.no-buttons {
|
||||
@@ -48,253 +45,233 @@
|
||||
width: calc(100% - #{$sidebarWidth});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toolbox-background {
|
||||
background-image: linear-gradient(to top, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0));
|
||||
transition: bottom .3s ease-in;
|
||||
height: 160px;
|
||||
width: 100%;
|
||||
bottom: -160px;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
z-index: $toolbarBackgroundZ;
|
||||
.toolbox-content {
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
position: relative;
|
||||
z-index: $toolbarZ;
|
||||
|
||||
.button-group-center,
|
||||
.button-group-left,
|
||||
.button-group-right {
|
||||
display: flex;
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
.toolbox-content {
|
||||
box-sizing: border-box;
|
||||
.button-group-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.button-group-right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.toolbox-button-wth-dialog {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbox-button {
|
||||
color: $toolbarButtonColor;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
line-height: $newToolbarSize;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.toolbar-button-with-badge {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
||||
.badge-round {
|
||||
bottom: -5px;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
min-width: 20px;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: -5px;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbox-content-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.toolbox-content-items {
|
||||
background: $newToolbarBackgroundColor;
|
||||
box-shadow: 0px 2px 8px 4px rgba(0, 0, 0, 0.25), 0px 0px 0px 1px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 6px;
|
||||
margin: 0 auto;
|
||||
padding: 6px;
|
||||
text-align: center;
|
||||
|
||||
>div {
|
||||
margin-left: 8px;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overflow-menu {
|
||||
font-size: 14px;
|
||||
list-style-type: none;
|
||||
padding: 8px 0;
|
||||
background-color: $menuBG;
|
||||
|
||||
.profile-text {
|
||||
max-width: 150px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.overflow-menu-item {
|
||||
align-items: center;
|
||||
color: $overflowMenuItemColor;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
height: 40px;
|
||||
line-height: 24px;
|
||||
padding: 8px 16px;
|
||||
box-sizing: border-box;
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
&:hover {
|
||||
background: $overflowMenuItemBackground;
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 20px 16px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
z-index: $toolbarZ;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button-group-center,
|
||||
.button-group-left,
|
||||
.button-group-right {
|
||||
display: flex;
|
||||
width: 33%;
|
||||
&.unclickable {
|
||||
cursor: default;
|
||||
}
|
||||
&.disabled {
|
||||
cursor: initial;
|
||||
color: #929292;
|
||||
|
||||
&:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.button-group-center {
|
||||
justify-content: center;
|
||||
|
||||
.toolbox-button {
|
||||
|
||||
.toolbox-icon {
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #d1dbe8;
|
||||
margin: 0px 4px;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
&:hover {
|
||||
background-color: #daebfa;
|
||||
border: 1px solid #daebfa;
|
||||
}
|
||||
}
|
||||
|
||||
&.toggled {
|
||||
background: #2a3a4b;
|
||||
border: 1px solid #5e6d7a;
|
||||
|
||||
svg {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
&:hover {
|
||||
background-color: #5e6d7a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled, .disabled & {
|
||||
cursor: initial;
|
||||
color: #fff;
|
||||
background-color: #a4b8d1;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: #5e6d7a;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
.toolbox-icon {
|
||||
background-color: $hangupColor;
|
||||
border: 1px solid $hangupColor;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
&:hover {
|
||||
background-color: $hangupColor;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
svg {
|
||||
fill: #929292;
|
||||
}
|
||||
|
||||
.button-group-right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
&.unclickable:hover {
|
||||
background: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overflow-menu {
|
||||
font-size: 1.2em;
|
||||
list-style-type: none;
|
||||
background-color: $overflowMenuBG;
|
||||
/**
|
||||
* Undo atlaskit padding by reducing margins.
|
||||
*/
|
||||
margin: -16px -24px;
|
||||
padding: 4px 0;
|
||||
.beta-tag {
|
||||
background: #36383C;
|
||||
border-radius: 3px;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
margin-left: 8px;
|
||||
padding: 0 4px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.overflow-menu-item {
|
||||
align-items: center;
|
||||
color: $overflowMenuItemColor;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
height: 40px;
|
||||
padding: 5px 12px;
|
||||
box-sizing: border-box;
|
||||
.overflow-menu-item-icon {
|
||||
margin-right: 16px;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
i {
|
||||
display: inline;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
&:hover {
|
||||
background-color: $overflowMenuItemHoverBG;
|
||||
color: $overflowMenuItemHoverColor;
|
||||
}
|
||||
}
|
||||
|
||||
&.unclickable {
|
||||
cursor: default;
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
&.unclickable:hover {
|
||||
background: inherit;
|
||||
}
|
||||
}
|
||||
&.disabled {
|
||||
cursor: initial;
|
||||
color: #3b475c;
|
||||
}
|
||||
}
|
||||
|
||||
.beta-tag {
|
||||
background: $overflowMenuItemColor;
|
||||
border-radius: 2px;
|
||||
color: $overflowMenuBG;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
margin-left: 8px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.overflow-menu-item-icon {
|
||||
margin-right: 16px;
|
||||
|
||||
i {
|
||||
display: inline;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
i:hover {
|
||||
background-color: initial;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 24px;
|
||||
max-height: 24px;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: #B8C7E0 !important;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-text {
|
||||
max-width: 150px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
i:hover {
|
||||
background-color: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbox-button {
|
||||
color: $toolbarButtonColor;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
line-height: $newToolbarSize;
|
||||
margin: 0 8px;
|
||||
text-align: center;
|
||||
img {
|
||||
max-width: 24px;
|
||||
max-height: 24px;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: #fff;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.overflow-menu-hr {
|
||||
border-top: 1px solid #4C4D50;
|
||||
border-bottom: 0;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.toolbox-icon {
|
||||
display: flex;
|
||||
border-radius: 3px;
|
||||
flex-direction: column;
|
||||
font-size: 24px;
|
||||
height: $newToolbarSize;
|
||||
justify-content: center;
|
||||
width: $newToolbarSize;
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
&:hover {
|
||||
background: $newToolbarButtonHoverColor;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-button-with-badge {
|
||||
position: relative;
|
||||
@media (max-width: 320px) {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
.badge-round {
|
||||
bottom: -5px;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
min-width: 20px;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: -5px;
|
||||
}
|
||||
&.toggled {
|
||||
background: $newToolbarButtonToggleColor;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: initial !important;
|
||||
background-color: #36383c !important;
|
||||
|
||||
svg {
|
||||
fill: #929292 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toolbox-button-wth-dialog {
|
||||
display: inline-block;
|
||||
.hangup-button {
|
||||
background-color: $hangupColor;
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
&:hover {
|
||||
background-color: $hangupHoverColor;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbox-icon {
|
||||
display: flex;
|
||||
border-radius: 5px;
|
||||
flex-direction: column;
|
||||
font-size: 24px;
|
||||
height: $newToolbarSize;
|
||||
justify-content: center;
|
||||
width: $newToolbarSize;
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
&:hover {
|
||||
background: $newToolbarButtonHoverColor;
|
||||
}
|
||||
}
|
||||
|
||||
&.toggled {
|
||||
background: $newToolbarButtonHoverColor;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: initial !important;
|
||||
background-color: #a4b8d1 !important;
|
||||
|
||||
svg {
|
||||
fill: #fff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
svg {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,3 +289,47 @@
|
||||
|
||||
@include transition(all .3s ease-out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio and video buttons do not have toggled state.
|
||||
*/
|
||||
.audio-preview,
|
||||
.video-preview {
|
||||
.toolbox-icon.toggled {
|
||||
background: none;
|
||||
|
||||
&:hover {
|
||||
background: $newToolbarButtonHoverColor;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* On small mobile devices make the toolbar full width and pad the invite prompt.
|
||||
*/
|
||||
.toolbox-content-mobile {
|
||||
@media (max-width: 500px) {
|
||||
margin-bottom: 0;
|
||||
|
||||
.toolbox-content-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.toolbox-content-items {
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
padding: 6px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.invite-more-container {
|
||||
margin: 0 16px 8px;
|
||||
}
|
||||
|
||||
.invite-more-container.elevated {
|
||||
margin-bottom: 52px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
* Style variables
|
||||
*/
|
||||
$baseFontFamily: -apple-system, BlinkMacSystemFont, 'open_sanslight', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
$hangupColor: #bf2117;
|
||||
$hangupColor:#DD3849;
|
||||
$hangupHoverColor: #F25363;
|
||||
$hangupFontSize: 2em;
|
||||
|
||||
/**
|
||||
@@ -37,20 +38,20 @@ $presence-idle: rgb(172, 172, 172);
|
||||
/**
|
||||
* Toolbar
|
||||
*/
|
||||
$newToolbarBackgroundColor: rgba(22, 38, 55, 0.8);
|
||||
$newToolbarButtonHoverColor: rgba(255, 255, 255, 0.15);
|
||||
$newToolbarButtonToggleColor: rgba(255, 255, 255, 0.2);
|
||||
$newToolbarBackgroundColor: #131519;
|
||||
$newToolbarButtonHoverColor: rgba(255, 255, 255, 0.2);
|
||||
$newToolbarButtonToggleColor: rgba(255, 255, 255, 0.15);
|
||||
$AOTToolbarButtonHoverColor: rgba(14, 20, 35, 0.6);
|
||||
$AOTToolbarButtonToggleColor: rgba(14, 20, 35, 1);
|
||||
$menuBG:#242528;
|
||||
$newToolbarFontSize: 24px;
|
||||
$newToolbarHangupFontSize: 32px;
|
||||
$newToolbarSize: 40px;
|
||||
$newToolbarSize: 48px;
|
||||
$newToolbarSizeWithPadding: calc(#{$newToolbarSize} + 24px);
|
||||
$toolbarTitleFontSize: 19px;
|
||||
$overflowMenuBG: initial;
|
||||
$overflowMenuItemHoverBG: #313D52;
|
||||
$overflowMenuItemHoverColor: #B8C7E0;
|
||||
$overflowMenuItemColor: #B8C7E0;
|
||||
$overflowMenuItemColor: #fff;
|
||||
$overflowMenuItemBackground: #36383C;
|
||||
|
||||
|
||||
/**
|
||||
* Video layout
|
||||
@@ -90,12 +91,12 @@ $modalTextColor: #333;
|
||||
* Chat
|
||||
*/
|
||||
$chatActionsSeparatorColor: rgb(173, 105, 112);
|
||||
$chatHeaderBackgroundColor: rgba(42, 58, 75, 0.9);
|
||||
$chatBackgroundColor: #131519;
|
||||
$chatInputSeparatorColor: #A4B8D1;
|
||||
$chatLocalMessageBackgroundColor: rgb(4, 98, 178);
|
||||
$chatLocalMessageBackgroundColor: #484A4F;
|
||||
$chatPrivateMessageBackgroundColor: rgb(153, 69, 77);
|
||||
$chatRemoteMessageBackgroundColor: rgb(86, 101, 114);
|
||||
$sidebarWidth: 375px;
|
||||
$chatRemoteMessageBackgroundColor: #242528;
|
||||
$sidebarWidth: 315px;
|
||||
|
||||
/**
|
||||
* Misc.
|
||||
@@ -122,8 +123,8 @@ $zindex10: 10;
|
||||
$reloadZ: 20;
|
||||
$poweredByZ: 100;
|
||||
$ringingZ: 300;
|
||||
$sideToolbarContainerZ: 300;
|
||||
$toolbarZ: 350;
|
||||
$sideToolbarContainerZ: 200;
|
||||
$toolbarZ: 250;
|
||||
$drawerZ: 351;
|
||||
$tooltipsZ: 401;
|
||||
$dropdownMaskZ: 900;
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
.video-preview {
|
||||
background: none;
|
||||
max-height: 290px;
|
||||
display: inline-block;
|
||||
max-height: 344px;
|
||||
|
||||
&-container {
|
||||
background: $menuBG;
|
||||
border-radius: 3px;
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
&-entry {
|
||||
cursor: pointer;
|
||||
height: 135px;
|
||||
margin-bottom: 16px;
|
||||
height: 168px;
|
||||
margin-bottom: 8px;
|
||||
position: relative;
|
||||
width: 240px;
|
||||
width: 284px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
@@ -20,13 +23,15 @@
|
||||
|
||||
&--selected {
|
||||
border: 3px solid #31B76A;
|
||||
border-radius: 3px;
|
||||
cursor: default;
|
||||
height: 129px;
|
||||
width: 234px;
|
||||
height: 162px;
|
||||
width: 278px;
|
||||
}
|
||||
}
|
||||
|
||||
&-video {
|
||||
border-radius: 3px;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
@@ -50,21 +55,28 @@
|
||||
}
|
||||
|
||||
&-label {
|
||||
bottom: 8px;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
overflow: hidden;
|
||||
padding: 8px;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
width: 220px;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
// Override @atlaskit/InlineDialog container which is made with styled components
|
||||
& > div:nth-child(2) {
|
||||
outline: none;
|
||||
padding: 0;
|
||||
&-container {
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
&-text {
|
||||
background-color: #131519;
|
||||
border-radius: 3px;
|
||||
padding: 2px 8px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
margin: 0 auto;
|
||||
max-width: calc(100% - 16px);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: fit-content;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,7 +400,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
.local-video-menu-trigger,
|
||||
.remote-video-menu-trigger,
|
||||
.localvideomenu,
|
||||
.remotevideomenu
|
||||
{
|
||||
display: inline-block;
|
||||
@@ -418,6 +420,7 @@
|
||||
cursor: hand;
|
||||
}
|
||||
}
|
||||
.local-video-menu-trigger,
|
||||
.remote-video-menu-trigger {
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
||||
@@ -208,6 +208,11 @@ body.welcome-page {
|
||||
cursor: pointer;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.toolbox-icon {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.welcome-watermark {
|
||||
@@ -228,6 +233,10 @@ body.welcome-page {
|
||||
}
|
||||
}
|
||||
|
||||
&.without-footer {
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.welcome-cards-container {
|
||||
color:#131519;
|
||||
padding-top: 40px;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
0 0 3px $videoThumbnailSelected;
|
||||
}
|
||||
|
||||
.remotevideomenu > .icon-menu {
|
||||
.remotevideomenu > .icon-menu, .localvideomenu > .icon-menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
box-shadow: inset 0 0 3px $videoThumbnailHovered,
|
||||
0 0 3px $videoThumbnailHovered;
|
||||
|
||||
.remotevideomenu > .icon-menu {
|
||||
.remotevideomenu > .icon-menu, .localvideomenu > .icon-menu {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,3 +121,15 @@
|
||||
align-self: baseline;
|
||||
}
|
||||
}
|
||||
|
||||
.shift-right #filmstripRemoteVideosContainer {
|
||||
/**
|
||||
* Max-width corresponding to the ASPECT_RATIO_BREAKPOINT from features/filmstrip/constants,
|
||||
* from which we subtract the chat size.
|
||||
*/
|
||||
@media only screen and (max-width: calc(500px + #{$sidebarWidth})) {
|
||||
video {
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
* specifically the various status icons.
|
||||
*/
|
||||
.remotevideomenu,
|
||||
.localvideomenu,
|
||||
.videocontainer__toptoolbar {
|
||||
z-index: auto;
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
* and tooltips from getting a new location context due to translate3d.
|
||||
*/
|
||||
.connection-indicator,
|
||||
.local-video-menu-trigger,
|
||||
.remote-video-menu-trigger,
|
||||
.indicator-icon-container {
|
||||
transform: translate3d(0, 0, 0);
|
||||
@@ -68,7 +69,9 @@
|
||||
* Move the remote video menu trigger to the bottom left of the video
|
||||
* thumbnail.
|
||||
*/
|
||||
.localvideomenu,
|
||||
.remotevideomenu,
|
||||
.local-video-menu-trigger,
|
||||
.remote-video-menu-trigger {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
@@ -76,6 +79,7 @@
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.local-video-menu-trigger,
|
||||
.remote-video-menu-trigger {
|
||||
margin-bottom: 7px;
|
||||
margin-left: $remoteVideoMenuIconMargin;
|
||||
|
||||
@@ -43,6 +43,7 @@ $flagsImagePath: "../images/";
|
||||
@import 'modals/settings/settings';
|
||||
@import 'modals/speaker_stats/speaker_stats';
|
||||
@import 'modals/video-quality/video-quality';
|
||||
@import 'modals/virtual-background/virtual-background';
|
||||
@import 'modals/local-recording/local-recording';
|
||||
@import 'videolayout_default';
|
||||
@import 'notice';
|
||||
|
||||
@@ -1,31 +1,42 @@
|
||||
.invite-more {
|
||||
&-container {
|
||||
margin-bottom: 8px;
|
||||
transition: margin-bottom 0.3s;
|
||||
|
||||
&.elevated {
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
font-weight: 600;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
z-index: $zindex2;
|
||||
background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
|
||||
}
|
||||
|
||||
&-header {
|
||||
font-size: 19px;
|
||||
line-height: 28px;
|
||||
margin: 24px 0 16px 0;
|
||||
max-width: 100%;
|
||||
margin-bottom: 16px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&-button {
|
||||
display: flex;
|
||||
margin: auto;
|
||||
max-width: 100%;
|
||||
height: 40px;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 16px;
|
||||
width: fit-content;
|
||||
width: -moz-fit-content;
|
||||
height: 24px;
|
||||
background: #0376DA;
|
||||
border-radius: 3px;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
cursor: pointer;
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
@@ -36,8 +47,9 @@
|
||||
|
||||
&-text {
|
||||
margin-left: 8px;
|
||||
font-size: 15px;
|
||||
line-height: 24px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
&-dialog {
|
||||
@@ -195,3 +207,14 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-browser {
|
||||
.invite-more-content {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.invite-more-button {
|
||||
height: 48px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
44
css/modals/virtual-background/_virtual-background.scss
Normal file
44
css/modals/virtual-background/_virtual-background.scss
Normal file
@@ -0,0 +1,44 @@
|
||||
.virtual-background-dialog{
|
||||
display: inline-flex;
|
||||
cursor: pointer;
|
||||
.thumbnail{
|
||||
object-fit: cover;
|
||||
padding: 5px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
.thumbnail-selected{
|
||||
object-fit: cover;
|
||||
padding: 5px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border: 2px solid #a4b8d1;
|
||||
}
|
||||
.blur-selected{
|
||||
border: 2px solid #a4b8d1;
|
||||
}
|
||||
.virtual-background-none{
|
||||
font-weight: bold;
|
||||
padding: 5px;
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #a4b8d1;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
line-height: 35px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.none-selected{
|
||||
font-weight: bold;
|
||||
padding: 5px;
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
border-radius: 10px;
|
||||
border: 2px solid #a4b8d1;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
line-height: 35px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
@@ -33,4 +33,12 @@
|
||||
bottom: 24px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__spinner-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
4
debian/jitsi-meet-prosody.postinst
vendored
4
debian/jitsi-meet-prosody.postinst
vendored
@@ -35,7 +35,7 @@ case "$1" in
|
||||
db_input critical jitsi-videobridge/jvb-hostname || true
|
||||
db_go
|
||||
fi
|
||||
JVB_HOSTNAME="$RET"
|
||||
JVB_HOSTNAME=$(echo "$RET" | xargs echo -n)
|
||||
|
||||
db_get jitsi-videobridge/jvbsecret
|
||||
if [ -z "$RET" ] ; then
|
||||
@@ -64,7 +64,7 @@ case "$1" in
|
||||
|
||||
# detect dpkg-reconfigure, just delete old links
|
||||
db_get jitsi-meet-prosody/jvb-hostname
|
||||
JVB_HOSTNAME_OLD=$RET
|
||||
JVB_HOSTNAME_OLD=$(echo "$RET" | xargs echo -n)
|
||||
if [ -n "$RET" ] && [ ! "$JVB_HOSTNAME_OLD" = "$JVB_HOSTNAME" ] ; then
|
||||
rm -f /etc/prosody/conf.d/$JVB_HOSTNAME_OLD.cfg.lua
|
||||
rm -f /etc/prosody/certs/$JVB_HOSTNAME_OLD.key
|
||||
|
||||
2
debian/jitsi-meet-prosody.postrm
vendored
2
debian/jitsi-meet-prosody.postrm
vendored
@@ -31,7 +31,7 @@ case "$1" in
|
||||
|
||||
purge)
|
||||
db_get jitsi-meet-prosody/jvb-hostname
|
||||
JVB_HOSTNAME=$RET
|
||||
JVB_HOSTNAME=$(echo "$RET" | xargs echo -n)
|
||||
if [ -n "$RET" ]; then
|
||||
rm -f /etc/prosody/conf.avail/$JVB_HOSTNAME.cfg.lua
|
||||
rm -f /etc/prosody/conf.d/$JVB_HOSTNAME.cfg.lua
|
||||
|
||||
2
debian/jitsi-meet-tokens.postinst
vendored
2
debian/jitsi-meet-tokens.postinst
vendored
@@ -25,7 +25,7 @@ case "$1" in
|
||||
. /usr/share/debconf/confmodule
|
||||
|
||||
db_get jitsi-meet-prosody/jvb-hostname
|
||||
JVB_HOSTNAME="$RET"
|
||||
JVB_HOSTNAME=$(echo "$RET" | xargs echo -n)
|
||||
|
||||
db_get jitsi-meet-tokens/appid
|
||||
if [ "$RET" = "false" ] ; then
|
||||
|
||||
4
debian/jitsi-meet-turnserver.postinst
vendored
4
debian/jitsi-meet-turnserver.postinst
vendored
@@ -30,7 +30,7 @@ case "$1" in
|
||||
db_input critical jitsi-videobridge/jvb-hostname || true
|
||||
db_go
|
||||
fi
|
||||
JVB_HOSTNAME="$RET"
|
||||
JVB_HOSTNAME=$(echo "$RET" | xargs echo -n)
|
||||
|
||||
TURN_CONFIG="/etc/turnserver.conf"
|
||||
NGINX_CONFIG="/etc/nginx/sites-available/$JVB_HOSTNAME.conf"
|
||||
@@ -44,7 +44,7 @@ case "$1" in
|
||||
|
||||
# detect dpkg-reconfigure, just delete old links
|
||||
db_get jitsi-meet-turnserver/jvb-hostname
|
||||
JVB_HOSTNAME_OLD=$RET
|
||||
JVB_HOSTNAME_OLD=$(echo "$RET" | xargs echo -n)
|
||||
if [ -n "$RET" ] && [ ! "$JVB_HOSTNAME_OLD" = "$JVB_HOSTNAME" ] ; then
|
||||
if [[ -f $TURN_CONFIG ]] && grep -q "jitsi-meet coturn config" "$TURN_CONFIG" ; then
|
||||
rm -f $TURN_CONFIG
|
||||
|
||||
4
debian/jitsi-meet-web-config.postinst
vendored
4
debian/jitsi-meet-web-config.postinst
vendored
@@ -32,12 +32,12 @@ case "$1" in
|
||||
db_go
|
||||
db_get jitsi-videobridge/jvb-hostname
|
||||
fi
|
||||
JVB_HOSTNAME="$RET"
|
||||
JVB_HOSTNAME=$(echo "$RET" | xargs echo -n)
|
||||
|
||||
# detect dpkg-reconfigure
|
||||
RECONFIGURING="false"
|
||||
db_get jitsi-meet/jvb-hostname
|
||||
JVB_HOSTNAME_OLD=$RET
|
||||
JVB_HOSTNAME_OLD=$(echo "$RET" | xargs echo -n)
|
||||
if [ -n "$RET" ] && [ ! "$JVB_HOSTNAME_OLD" = "$JVB_HOSTNAME" ] ; then
|
||||
RECONFIGURING="true"
|
||||
rm -f /etc/jitsi/meet/$JVB_HOSTNAME_OLD-config.js
|
||||
|
||||
2
debian/jitsi-meet-web-config.postrm
vendored
2
debian/jitsi-meet-web-config.postrm
vendored
@@ -33,7 +33,7 @@ case "$1" in
|
||||
;;
|
||||
purge)
|
||||
db_get jitsi-meet/jvb-hostname
|
||||
JVB_HOSTNAME=$RET
|
||||
JVB_HOSTNAME=$(echo "$RET" | xargs echo -n)
|
||||
if [ -n "$RET" ]; then
|
||||
rm -f /etc/jitsi/meet/$JVB_HOSTNAME-config.js
|
||||
rm -f /etc/nginx/sites-available/$JVB_HOSTNAME.conf
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
<VirtualHost *:80>
|
||||
ServerName jitsi-meet.example.com
|
||||
Redirect permanent / https://jitsi-meet.example.com/
|
||||
RewriteEngine On
|
||||
RewriteCond %{HTTPS} off
|
||||
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
|
||||
</VirtualHost>
|
||||
|
||||
<VirtualHost *:443>
|
||||
@@ -42,11 +39,12 @@
|
||||
</Location>
|
||||
|
||||
ProxyPreserveHost on
|
||||
ProxyPass /http-bind http://localhost:5280/http-bind/
|
||||
ProxyPassReverse /http-bind http://localhost:5280/http-bind/
|
||||
ProxyPass /http-bind http://localhost:5280/http-bind
|
||||
ProxyPassReverse /http-bind http://localhost:5280/http-bind
|
||||
ProxyPass /xmpp-websocket ws://localhost:5280/xmpp-websocket
|
||||
ProxyPassReverse /xmpp-websocket ws://localhost:5280/xmpp-websocket
|
||||
ProxyPassMatch ^/colibri-ws/default-id ws://localhost:9090
|
||||
ProxyPass /colibri-ws/default-id ws://localhost:9090/colibri-ws/default-id
|
||||
ProxyPassReverse /colibri-ws/default-id ws://localhost:9090/colibri-ws/default-id
|
||||
|
||||
RewriteEngine on
|
||||
RewriteRule ^/([a-zA-Z0-9]+)$ /index.html
|
||||
|
||||
BIN
images/virtual-background/background-1.jpg
Normal file
BIN
images/virtual-background/background-1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 437 KiB |
BIN
images/virtual-background/background-2.jpg
Normal file
BIN
images/virtual-background/background-2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 284 KiB |
BIN
images/virtual-background/background-3.jpg
Normal file
BIN
images/virtual-background/background-3.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 216 KiB |
BIN
images/virtual-background/background-4.jpg
Normal file
BIN
images/virtual-background/background-4.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 341 KiB |
@@ -168,6 +168,13 @@ var interfaceConfig = {
|
||||
REMOTE_THUMBNAIL_RATIO: 1, // 1:1
|
||||
|
||||
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile', 'calendar' ],
|
||||
|
||||
/**
|
||||
* Specify which sharing features should be displayed. If the value is not set
|
||||
* all sharing features will be shown. You can set [] to disable all.
|
||||
*/
|
||||
// SHARING_FEATURES: ['email', 'url', 'dial-in', 'embed'],
|
||||
|
||||
SHOW_BRAND_WATERMARK: false,
|
||||
|
||||
/**
|
||||
@@ -191,23 +198,16 @@ var interfaceConfig = {
|
||||
TOOLBAR_ALWAYS_VISIBLE: false,
|
||||
|
||||
/**
|
||||
* The name of the toolbar buttons to display in the toolbar, including the
|
||||
* "More actions" menu. If present, the button will display. Exceptions are
|
||||
* "livestreaming" and "recording" which also require being a moderator and
|
||||
* some values in config.js to be enabled. Also, the "profile" button will
|
||||
* not display for users with a JWT.
|
||||
* Notes:
|
||||
* - it's impossible to choose which buttons go in the "More actions" menu
|
||||
* - it's impossible to control the placement of buttons
|
||||
* - 'desktop' controls the "Share your screen" button
|
||||
* DEPRECATED!
|
||||
* This config was moved to config.js as `toolbarButtons`.
|
||||
*/
|
||||
TOOLBAR_BUTTONS: [
|
||||
'microphone', 'camera', 'closedcaptions', 'desktop', 'embedmeeting', 'fullscreen',
|
||||
'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
|
||||
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
|
||||
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
|
||||
'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone', 'security'
|
||||
],
|
||||
// TOOLBAR_BUTTONS: [
|
||||
// 'microphone', 'camera', 'closedcaptions', 'desktop', 'embedmeeting', 'fullscreen',
|
||||
// 'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
|
||||
// 'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
|
||||
// 'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
|
||||
// 'tileview', 'select-background', 'download', 'help', 'mute-everyone', 'mute-video-everyone', 'security'
|
||||
// ],
|
||||
|
||||
TOOLBAR_TIMEOUT: 4000,
|
||||
|
||||
|
||||
@@ -61,7 +61,6 @@ target 'JitsiMeetSDK' do
|
||||
pod 'react-native-splash-screen', :path => '../node_modules/react-native-splash-screen'
|
||||
pod 'react-native-webview', :path => '../node_modules/react-native-webview'
|
||||
pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'
|
||||
pod 'BVLinearGradient', :path => '../node_modules/react-native-linear-gradient'
|
||||
pod 'RNCAsyncStorage', :path => '../node_modules/@react-native-async-storage/async-storage'
|
||||
pod 'RNDeviceInfo', :path => '../node_modules/react-native-device-info'
|
||||
pod 'RNGoogleSignin', :path => '../node_modules/@react-native-community/google-signin'
|
||||
|
||||
@@ -5,8 +5,6 @@ PODS:
|
||||
- AppAuth/Core (1.2.0)
|
||||
- AppAuth/ExternalUserAgent (1.2.0)
|
||||
- boost-for-react-native (1.63.0)
|
||||
- BVLinearGradient (2.5.6):
|
||||
- React
|
||||
- CocoaLumberjack (3.5.3):
|
||||
- CocoaLumberjack/Core (= 3.5.3)
|
||||
- CocoaLumberjack/Core (3.5.3)
|
||||
@@ -292,7 +290,7 @@ PODS:
|
||||
- React
|
||||
- react-native-splash-screen (3.2.0):
|
||||
- React
|
||||
- react-native-webrtc (1.87.3):
|
||||
- react-native-webrtc (1.89.1):
|
||||
- React-Core
|
||||
- react-native-webview (11.0.2):
|
||||
- React-Core
|
||||
@@ -371,7 +369,6 @@ PODS:
|
||||
- Yoga (1.14.0)
|
||||
|
||||
DEPENDENCIES:
|
||||
- BVLinearGradient (from `../node_modules/react-native-linear-gradient`)
|
||||
- CocoaLumberjack (~> 3.5.3)
|
||||
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
|
||||
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector/`)
|
||||
@@ -442,8 +439,6 @@ SPEC REPOS:
|
||||
- PromisesObjC
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
BVLinearGradient:
|
||||
:path: "../node_modules/react-native-linear-gradient"
|
||||
DoubleConversion:
|
||||
:podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
|
||||
FBLazyVector:
|
||||
@@ -526,7 +521,6 @@ EXTERNAL SOURCES:
|
||||
SPEC CHECKSUMS:
|
||||
AppAuth: bce82c76043657c99d91e7882e8a9e1a93650cd4
|
||||
boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c
|
||||
BVLinearGradient: e3aad03778a456d77928f594a649e96995f1c872
|
||||
CocoaLumberjack: 2f44e60eb91c176d471fdba43b9e3eae6a721947
|
||||
DoubleConversion: 5805e889d232975c086db112ece9ed034df7a0b2
|
||||
FBLazyVector: ca7f56c8ff6cd8590f7a673d7903b06019805581
|
||||
@@ -563,7 +557,7 @@ SPEC CHECKSUMS:
|
||||
react-native-keep-awake: eba3137546b10003361b37c761f6c429b59814ae
|
||||
react-native-netinfo: 8d8db463bcc5db66a8ac5c48a7d86beb3b92f61a
|
||||
react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865
|
||||
react-native-webrtc: dc1208bdca2c4d091f7b57859e69332bff6f1986
|
||||
react-native-webrtc: ccb0c21eb4fb04326648fbdb4a5d49977e2cf274
|
||||
react-native-webview: b2542d6fd424bcc3e3b2ec5f854f0abb4ec86c87
|
||||
React-RCTActionSheet: bcbc311dc3b47bc8efb2737ff0940239a45789a9
|
||||
React-RCTAnimation: 65f61080ce632f6dea23d52e354ffac9948396c6
|
||||
@@ -584,6 +578,6 @@ SPEC CHECKSUMS:
|
||||
RNWatch: a5320c959c75e72845c07985f3e935e58998f1d3
|
||||
Yoga: 96b469c5e81ff51b917b92e8c3390642d4ded30c
|
||||
|
||||
PODFILE CHECKSUM: 5be5132e41831a98362eeed760558227a4df89ae
|
||||
PODFILE CHECKSUM: d059cebf82da14a53940a16c24c3330752d4b0c8
|
||||
|
||||
COCOAPODS: 1.10.0
|
||||
COCOAPODS: 1.10.1
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
<string>applinks:beta.meet.jit.si</string>
|
||||
<string>applinks:meet.jit.si</string>
|
||||
</array>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.org.jitsi.meet.appgroup</string>
|
||||
</array>
|
||||
<key>com.apple.developer.siri</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
||||
@@ -23,6 +23,12 @@
|
||||
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; };
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
||||
4E51B75E25E4115F0038575A /* DarwinNotificationCenter.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E51B75D25E4115F0038575A /* DarwinNotificationCenter.m */; };
|
||||
4EC49BB725BEDAC100E76218 /* ReplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4EC49B8625BED71300E76218 /* ReplayKit.framework */; };
|
||||
4EC49BBB25BEDAC100E76218 /* SampleHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EC49BBA25BEDAC100E76218 /* SampleHandler.m */; };
|
||||
4EC49BBF25BEDAC100E76218 /* JitsiMeetBroadcast Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 4EC49BB625BEDAC100E76218 /* JitsiMeetBroadcast Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
4EC49BCB25BEDB6400E76218 /* SocketConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EC49BCA25BEDB6400E76218 /* SocketConnection.m */; };
|
||||
4EC49BD125BF19CF00E76218 /* SampleUploader.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EC49BD025BF19CF00E76218 /* SampleUploader.m */; };
|
||||
55BEDABDA92D47D399A70A5E /* libPods-JitsiMeet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D878B07B3FBD6E305EAA6B27 /* libPods-JitsiMeet.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, ); }; };
|
||||
@@ -48,6 +54,13 @@
|
||||
remoteGlobalIDString = 0BEA5C241F7B8F73000D0AB4;
|
||||
remoteInfo = JitsiMeetCompanion;
|
||||
};
|
||||
4EC49BBD25BEDAC100E76218 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 4EC49BB525BEDAC100E76218;
|
||||
remoteInfo = "JitsiMeetBroadcast Extension";
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
@@ -85,6 +98,17 @@
|
||||
name = "Embed Watch Content";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
4EC49B9025BED71300E76218 /* Embed App Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
4EC49BBF25BEDAC100E76218 /* JitsiMeetBroadcast Extension.appex in Embed App Extensions */,
|
||||
);
|
||||
name = "Embed App Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@@ -115,6 +139,18 @@
|
||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
4E51B75C25E4115F0038575A /* DarwinNotificationCenter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DarwinNotificationCenter.h; sourceTree = "<group>"; };
|
||||
4E51B75D25E4115F0038575A /* DarwinNotificationCenter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DarwinNotificationCenter.m; sourceTree = "<group>"; };
|
||||
4EC49B8625BED71300E76218 /* ReplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReplayKit.framework; path = System/Library/Frameworks/ReplayKit.framework; sourceTree = SDKROOT; };
|
||||
4EC49BB625BEDAC100E76218 /* JitsiMeetBroadcast Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "JitsiMeetBroadcast Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
4EC49BB925BEDAC100E76218 /* SampleHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SampleHandler.h; sourceTree = "<group>"; };
|
||||
4EC49BBA25BEDAC100E76218 /* SampleHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SampleHandler.m; sourceTree = "<group>"; };
|
||||
4EC49BBC25BEDAC100E76218 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
4EC49BC925BEDB6400E76218 /* SocketConnection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SocketConnection.h; sourceTree = "<group>"; };
|
||||
4EC49BCA25BEDB6400E76218 /* SocketConnection.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SocketConnection.m; sourceTree = "<group>"; };
|
||||
4EC49BCF25BF19CF00E76218 /* SampleUploader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SampleUploader.h; sourceTree = "<group>"; };
|
||||
4EC49BD025BF19CF00E76218 /* SampleUploader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SampleUploader.m; sourceTree = "<group>"; };
|
||||
4EC49BDB25BF280A00E76218 /* extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = extension.entitlements; sourceTree = "<group>"; };
|
||||
609CB2080B75F75A89923F3D /* Pods-JitsiMeet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
B3B083EB1D4955FF0069CEE7 /* app.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = app.entitlements; sourceTree = "<group>"; };
|
||||
D878B07B3FBD6E305EAA6B27 /* libPods-JitsiMeet.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-JitsiMeet.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -153,6 +189,14 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
4EC49BB325BEDAC100E76218 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
4EC49BB725BEDAC100E76218 /* ReplayKit.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
@@ -165,6 +209,7 @@
|
||||
DEFDBBDB25656E3B00344B23 /* WebRTC.xcframework */,
|
||||
0BD6B4361EF82A6B00D1F4CD /* WebRTC.framework */,
|
||||
D878B07B3FBD6E305EAA6B27 /* libPods-JitsiMeet.a */,
|
||||
4EC49B8625BED71300E76218 /* ReplayKit.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@@ -216,6 +261,24 @@
|
||||
path = src;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4EC49BB825BEDAC100E76218 /* JitsiMeetBroadcast Extension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4EC49BDB25BF280A00E76218 /* extension.entitlements */,
|
||||
4EC49BB925BEDAC100E76218 /* SampleHandler.h */,
|
||||
4EC49BBA25BEDAC100E76218 /* SampleHandler.m */,
|
||||
4EC49BC925BEDB6400E76218 /* SocketConnection.h */,
|
||||
4EC49BCA25BEDB6400E76218 /* SocketConnection.m */,
|
||||
4EC49BCF25BF19CF00E76218 /* SampleUploader.h */,
|
||||
4EC49BD025BF19CF00E76218 /* SampleUploader.m */,
|
||||
4EC49BBC25BEDAC100E76218 /* Info.plist */,
|
||||
4E51B75C25E4115F0038575A /* DarwinNotificationCenter.h */,
|
||||
4E51B75D25E4115F0038575A /* DarwinNotificationCenter.m */,
|
||||
);
|
||||
name = "JitsiMeetBroadcast Extension";
|
||||
path = "broadcast-extension";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5E96ADD5E49F3B3822EF9A52 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -236,6 +299,7 @@
|
||||
13B07FAE1A68108700A75B9A /* src */,
|
||||
5E96ADD5E49F3B3822EF9A52 /* Pods */,
|
||||
0BEA5C261F7B8F73000D0AB4 /* Watch app */,
|
||||
4EC49BB825BEDAC100E76218 /* JitsiMeetBroadcast Extension */,
|
||||
0BEA5C351F7B8F73000D0AB4 /* WatchKit extension */,
|
||||
);
|
||||
indentWidth = 2;
|
||||
@@ -248,6 +312,7 @@
|
||||
13B07F961A680F5B00A75B9A /* jitsi-meet.app */,
|
||||
0BEA5C251F7B8F73000D0AB4 /* JitsiMeetCompanion.app */,
|
||||
0BEA5C311F7B8F73000D0AB4 /* JitsiMeetCompanion Extension.appex */,
|
||||
4EC49BB625BEDAC100E76218 /* JitsiMeetBroadcast Extension.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -305,17 +370,36 @@
|
||||
DE11877A21EE09640078D059 /* Setup Google reverse URL handler */,
|
||||
DE4F6D6E22005C0400DE699E /* Setup Dropbox */,
|
||||
0BEA5C491F7B8F73000D0AB4 /* Embed Watch Content */,
|
||||
4EC49B9025BED71300E76218 /* Embed App Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
0BEA5C401F7B8F73000D0AB4 /* PBXTargetDependency */,
|
||||
4EC49BBE25BEDAC100E76218 /* PBXTargetDependency */,
|
||||
);
|
||||
name = JitsiMeet;
|
||||
productName = "Jitsi Meet";
|
||||
productReference = 13B07F961A680F5B00A75B9A /* jitsi-meet.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
4EC49BB525BEDAC100E76218 /* JitsiMeetBroadcast Extension */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 4EC49BC025BEDAC100E76218 /* Build configuration list for PBXNativeTarget "JitsiMeetBroadcast Extension" */;
|
||||
buildPhases = (
|
||||
4EC49BB225BEDAC100E76218 /* Sources */,
|
||||
4EC49BB325BEDAC100E76218 /* Frameworks */,
|
||||
4EC49BB425BEDAC100E76218 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = "JitsiMeetBroadcast Extension";
|
||||
productName = "JitsiMeetBroadcast Extension";
|
||||
productReference = 4EC49BB625BEDAC100E76218 /* JitsiMeetBroadcast Extension.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
@@ -336,8 +420,6 @@
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
13B07F861A680F5B00A75B9A = {
|
||||
DevelopmentTeam = FC967L3QRG;
|
||||
ProvisioningStyle = Automatic;
|
||||
SystemCapabilities = {
|
||||
com.apple.SafariKeychain = {
|
||||
enabled = 1;
|
||||
@@ -347,6 +429,9 @@
|
||||
};
|
||||
};
|
||||
};
|
||||
4EC49BB525BEDAC100E76218 = {
|
||||
CreatedOnToolsVersion = 12.2;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "app" */;
|
||||
@@ -365,6 +450,7 @@
|
||||
13B07F861A680F5B00A75B9A /* JitsiMeet */,
|
||||
0BEA5C241F7B8F73000D0AB4 /* JitsiMeetCompanion */,
|
||||
0BEA5C301F7B8F73000D0AB4 /* JitsiMeetCompanion Extension */,
|
||||
4EC49BB525BEDAC100E76218 /* JitsiMeetBroadcast Extension */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -397,6 +483,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
4EC49BB425BEDAC100E76218 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
@@ -532,6 +625,17 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
4EC49BB225BEDAC100E76218 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
4EC49BCB25BEDB6400E76218 /* SocketConnection.m in Sources */,
|
||||
4EC49BBB25BEDAC100E76218 /* SampleHandler.m in Sources */,
|
||||
4E51B75E25E4115F0038575A /* DarwinNotificationCenter.m in Sources */,
|
||||
4EC49BD125BF19CF00E76218 /* SampleUploader.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
@@ -545,6 +649,11 @@
|
||||
target = 0BEA5C241F7B8F73000D0AB4 /* JitsiMeetCompanion */;
|
||||
targetProxy = 0BEA5C3F1F7B8F73000D0AB4 /* PBXContainerItemProxy */;
|
||||
};
|
||||
4EC49BBE25BEDAC100E76218 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 4EC49BB525BEDAC100E76218 /* JitsiMeetBroadcast Extension */;
|
||||
targetProxy = 4EC49BBD25BEDAC100E76218 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
@@ -718,7 +827,7 @@
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIconDebug;
|
||||
CODE_SIGN_ENTITLEMENTS = app.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEAD_CODE_STRIPPING = NO;
|
||||
@@ -748,7 +857,7 @@
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIconRelease;
|
||||
CODE_SIGN_ENTITLEMENTS = app.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = FC967L3QRG;
|
||||
@@ -770,6 +879,70 @@
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
4EC49BC125BEDAC100E76218 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = "broadcast-extension/extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = FC967L3QRG;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = "broadcast-extension/Info.plist";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.meet.broadcast.extension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
4EC49BC225BEDAC100E76218 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = "broadcast-extension/extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = FC967L3QRG;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = "broadcast-extension/Info.plist";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.meet.broadcast.extension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
83CBBA201A601CBA00E9B192 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
@@ -914,6 +1087,15 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
4EC49BC025BEDAC100E76218 /* Build configuration list for PBXNativeTarget "JitsiMeetBroadcast Extension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
4EC49BC125BEDAC100E76218 /* Debug */,
|
||||
4EC49BC225BEDAC100E76218 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "app" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
||||
31
ios/app/broadcast-extension/DarwinNotificationCenter.h
Normal file
31
ios/app/broadcast-extension/DarwinNotificationCenter.h
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright @ 2021-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSNotificationName const kBroadcastStartedNotification;
|
||||
extern NSNotificationName const kBroadcastStoppedNotification;
|
||||
|
||||
@interface DarwinNotificationCenter: NSObject
|
||||
|
||||
+ (instancetype)sharedInstance;
|
||||
- (void)postNotificationWithName:(NSNotificationName)name;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
50
ios/app/broadcast-extension/DarwinNotificationCenter.m
Normal file
50
ios/app/broadcast-extension/DarwinNotificationCenter.m
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright @ 2021-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import "DarwinNotificationCenter.h"
|
||||
|
||||
NSNotificationName const kBroadcastStartedNotification = @"iOS_BroadcastStarted";
|
||||
NSNotificationName const kBroadcastStoppedNotification = @"iOS_BroadcastStopped";
|
||||
|
||||
@implementation DarwinNotificationCenter {
|
||||
CFNotificationCenterRef _notificationCenter;
|
||||
}
|
||||
|
||||
+ (instancetype)sharedInstance {
|
||||
static DarwinNotificationCenter *sharedInstance = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sharedInstance = [[self alloc] init];
|
||||
});
|
||||
|
||||
return sharedInstance;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_notificationCenter = CFNotificationCenterGetDarwinNotifyCenter();
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)postNotificationWithName:(NSString*)name {
|
||||
CFNotificationCenterPostNotification(_notificationCenter, (__bridge CFStringRef)name, NULL, NULL, true);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
33
ios/app/broadcast-extension/Info.plist
Normal file
33
ios/app/broadcast-extension/Info.plist
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>JitsiMeet Broadcast Extension</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>21.0.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.broadcast-services-upload</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>SampleHandler</string>
|
||||
<key>RPBroadcastProcessMode</key>
|
||||
<string>RPBroadcastProcessModeSampleBuffer</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
21
ios/app/broadcast-extension/SampleHandler.h
Normal file
21
ios/app/broadcast-extension/SampleHandler.h
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright @ 2021-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import <ReplayKit/ReplayKit.h>
|
||||
|
||||
@interface SampleHandler : RPBroadcastSampleHandler
|
||||
|
||||
@end
|
||||
123
ios/app/broadcast-extension/SampleHandler.m
Normal file
123
ios/app/broadcast-extension/SampleHandler.m
Normal file
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright @ 2021-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import "SampleHandler.h"
|
||||
#import "SocketConnection.h"
|
||||
#import "SampleUploader.h"
|
||||
#import "DarwinNotificationCenter.h"
|
||||
|
||||
@interface SampleHandler ()
|
||||
|
||||
@property (nonatomic, retain) SocketConnection *clientConnection;
|
||||
@property (nonatomic, retain) SampleUploader *uploader;
|
||||
|
||||
@end
|
||||
|
||||
@implementation SampleHandler
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.clientConnection = [[SocketConnection alloc] initWithFilePath:self.socketFilePath];
|
||||
[self setupConnection];
|
||||
|
||||
self.uploader = [[SampleUploader alloc] initWithConnection:self.clientConnection];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo {
|
||||
// User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional.
|
||||
NSLog(@"broadcast started");
|
||||
|
||||
[[DarwinNotificationCenter sharedInstance] postNotificationWithName:kBroadcastStartedNotification];
|
||||
[self openConnection];
|
||||
}
|
||||
|
||||
- (void)broadcastPaused {
|
||||
// User has requested to pause the broadcast. Samples will stop being delivered.
|
||||
}
|
||||
|
||||
- (void)broadcastResumed {
|
||||
// User has requested to resume the broadcast. Samples delivery will resume.
|
||||
}
|
||||
|
||||
- (void)broadcastFinished {
|
||||
// User has requested to finish the broadcast.
|
||||
[[DarwinNotificationCenter sharedInstance] postNotificationWithName:kBroadcastStoppedNotification];
|
||||
[self.clientConnection close];
|
||||
}
|
||||
|
||||
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
|
||||
static NSUInteger frameCount = 0;
|
||||
switch (sampleBufferType) {
|
||||
case RPSampleBufferTypeVideo:
|
||||
// adjust frame rate by using every third frame
|
||||
if (++frameCount%3 == 0 && self.uploader.isReady) {
|
||||
[self.uploader sendSample:sampleBuffer];
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Private Methods
|
||||
|
||||
- (NSString *)socketFilePath {
|
||||
// the appGroupIdentifier must match the value provided in the app's info.plist for the RTCAppGroupIdentifier key
|
||||
NSString *appGroupIdentifier = @"group.org.jitsi.meet.appgroup";
|
||||
NSURL *sharedContainer = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:appGroupIdentifier];
|
||||
NSString *socketFilePath = [[sharedContainer URLByAppendingPathComponent:@"rtc_SSFD"] path];
|
||||
|
||||
return socketFilePath;
|
||||
}
|
||||
|
||||
- (void)setupConnection {
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
self.clientConnection.didClose = ^(NSError *error) {
|
||||
NSLog(@"client connection did close: %@", error);
|
||||
if (error) {
|
||||
[weakSelf finishBroadcastWithError:error];
|
||||
}
|
||||
else {
|
||||
NSInteger JMScreenSharingStopped = 10001;
|
||||
NSError *customError = [NSError errorWithDomain:RPRecordingErrorDomain
|
||||
code:JMScreenSharingStopped
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Screen sharing stopped"}];
|
||||
[weakSelf finishBroadcastWithError:customError];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
- (void)openConnection {
|
||||
dispatch_queue_t queue = dispatch_queue_create("org.jitsi.meet.broadcast.connectTimer", 0);
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
|
||||
dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), 0.1 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC);
|
||||
|
||||
dispatch_source_set_event_handler(timer, ^{
|
||||
BOOL success = [self.clientConnection open];
|
||||
if (success) {
|
||||
dispatch_source_cancel(timer);
|
||||
}
|
||||
});
|
||||
|
||||
dispatch_resume(timer);
|
||||
}
|
||||
|
||||
@end
|
||||
33
ios/app/broadcast-extension/SampleUploader.h
Normal file
33
ios/app/broadcast-extension/SampleUploader.h
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright @ 2021-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <ReplayKit/ReplayKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class SocketConnection;
|
||||
|
||||
@interface SampleUploader : NSObject
|
||||
|
||||
@property (nonatomic, assign, readonly) BOOL isReady;
|
||||
|
||||
- (instancetype)initWithConnection:(SocketConnection *)connection;
|
||||
- (void)sendSample:(CMSampleBufferRef)sampleBuffer;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
155
ios/app/broadcast-extension/SampleUploader.m
Normal file
155
ios/app/broadcast-extension/SampleUploader.m
Normal file
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright @ 2021-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import <MessageUI/MessageUI.h>
|
||||
#import <ReplayKit/ReplayKit.h>
|
||||
|
||||
#import "SampleUploader.h"
|
||||
#import "SocketConnection.h"
|
||||
|
||||
static const NSInteger kBufferMaxLenght = 10 * 1024;
|
||||
|
||||
@interface SampleUploader ()
|
||||
|
||||
@property (nonatomic, assign) BOOL isReady;
|
||||
|
||||
@property (nonatomic, strong) dispatch_queue_t serialQueue;
|
||||
@property (nonatomic, strong) SocketConnection *connection;
|
||||
@property (nonatomic, strong) CIContext *imageContext;
|
||||
|
||||
@property (nonatomic, strong) NSData *dataToSend;
|
||||
@property (nonatomic, assign) NSUInteger byteIndex;
|
||||
|
||||
@end
|
||||
|
||||
@implementation SampleUploader
|
||||
|
||||
- (instancetype)initWithConnection:(SocketConnection *)connection {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.serialQueue = dispatch_queue_create("org.jitsi.meet.broadcast.sampleUploader", DISPATCH_QUEUE_SERIAL);
|
||||
|
||||
self.connection = connection;
|
||||
[self setupConnection];
|
||||
|
||||
self.imageContext = [[CIContext alloc] initWithOptions:nil];
|
||||
self.isReady = false;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)sendSample:(CMSampleBufferRef)sampleBuffer {
|
||||
self.isReady = false;
|
||||
|
||||
self.dataToSend = [self prepareSample:sampleBuffer];
|
||||
self.byteIndex = 0;
|
||||
|
||||
dispatch_async(self.serialQueue, ^{
|
||||
[self sendData];
|
||||
});
|
||||
}
|
||||
|
||||
// MARK: Private Methods
|
||||
|
||||
- (void)setupConnection {
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
self.connection.didOpen = ^{
|
||||
weakSelf.isReady = true;
|
||||
};
|
||||
self.connection.streamHasSpaceAvailable = ^{
|
||||
dispatch_async(weakSelf.serialQueue, ^{
|
||||
weakSelf.isReady = ![weakSelf sendData];
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
This function downscales and converts to jpeg the provided sample buffer, then wraps the resulted image data into a CFHTTPMessageRef. Returns the serialized CFHTTPMessageRef.
|
||||
*/
|
||||
- (NSData *)prepareSample:(CMSampleBufferRef)sampleBuffer {
|
||||
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
|
||||
|
||||
CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
|
||||
CGFloat scaleFactor = 2;
|
||||
size_t width = CVPixelBufferGetWidth(imageBuffer)/scaleFactor;
|
||||
size_t height = CVPixelBufferGetHeight(imageBuffer)/scaleFactor;
|
||||
|
||||
CGAffineTransform scaleTransform = CGAffineTransformMakeScale(1/scaleFactor, 1/scaleFactor);
|
||||
NSData *bufferData = [self jpegDataFromPixelBuffer:imageBuffer withScaling:scaleTransform];
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
|
||||
if (bufferData) {
|
||||
CFHTTPMessageRef httpResponse = CFHTTPMessageCreateResponse(kCFAllocatorDefault, 200, NULL, kCFHTTPVersion1_1);
|
||||
CFHTTPMessageSetHeaderFieldValue(httpResponse, (__bridge CFStringRef)@"Content-Length", (__bridge CFStringRef)[NSString stringWithFormat:@"%ld", bufferData.length]);
|
||||
CFHTTPMessageSetHeaderFieldValue(httpResponse, (__bridge CFStringRef)@"Buffer-Width", (__bridge CFStringRef)[NSString stringWithFormat:@"%ld", width]);
|
||||
CFHTTPMessageSetHeaderFieldValue(httpResponse, (__bridge CFStringRef)@"Buffer-Height", (__bridge CFStringRef)[NSString stringWithFormat:@"%ld", height]);
|
||||
|
||||
CFHTTPMessageSetBody(httpResponse, (__bridge CFDataRef)bufferData);
|
||||
|
||||
CFDataRef serializedMessage = CFHTTPMessageCopySerializedMessage(httpResponse);
|
||||
CFRelease(httpResponse);
|
||||
|
||||
return CFBridgingRelease(serializedMessage);
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)sendData {
|
||||
if (!self.dataToSend) {
|
||||
NSLog(@"no data to send");
|
||||
return false;
|
||||
}
|
||||
|
||||
NSUInteger bytesLeft = self.dataToSend.length - self.byteIndex;
|
||||
|
||||
NSInteger length = bytesLeft > kBufferMaxLenght ? kBufferMaxLenght : bytesLeft;
|
||||
uint8_t buffer[length];
|
||||
[self.dataToSend getBytes:&buffer range:NSMakeRange(self.byteIndex, length)];
|
||||
|
||||
length = [self.connection writeBufferToStream:buffer maxLength:length];
|
||||
if (length > 0) {
|
||||
self.byteIndex += length;
|
||||
bytesLeft -= length;
|
||||
|
||||
if (bytesLeft == 0) {
|
||||
NSLog(@"video sample processed successfully");
|
||||
self.dataToSend = nil;
|
||||
self.byteIndex = 0;
|
||||
}
|
||||
}
|
||||
else {
|
||||
NSLog(@"writeBufferToStream failure");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
- (NSData *)jpegDataFromPixelBuffer:(CVPixelBufferRef)pixelBuffer withScaling:(CGAffineTransform)scaleTransform {
|
||||
CIImage *image = [[CIImage alloc] initWithCVPixelBuffer:pixelBuffer];
|
||||
image = [image imageByApplyingTransform:scaleTransform];
|
||||
|
||||
NSDictionary *options = @{(NSString *)kCGImageDestinationLossyCompressionQuality: [NSNumber numberWithFloat:1.0]};
|
||||
NSData *imageData = [self.imageContext JPEGRepresentationOfImage:image
|
||||
colorSpace:image.colorSpace
|
||||
options:options];
|
||||
return imageData;
|
||||
}
|
||||
|
||||
@end
|
||||
34
ios/app/broadcast-extension/SocketConnection.h
Normal file
34
ios/app/broadcast-extension/SocketConnection.h
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright @ 2021-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface SocketConnection : NSObject
|
||||
|
||||
@property (nonatomic, copy, nullable) void (^didOpen)(void);
|
||||
@property (nonatomic, copy, nullable) void (^didClose)(NSError*);
|
||||
@property (nonatomic, copy, nullable) void (^streamHasSpaceAvailable)(void);
|
||||
|
||||
- (instancetype)initWithFilePath:(nonnull NSString *)filePath;
|
||||
- (BOOL)open;
|
||||
- (void)close;
|
||||
- (NSInteger)writeBufferToStream:(const uint8_t*)buffer maxLength:(NSInteger)length;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
189
ios/app/broadcast-extension/SocketConnection.m
Normal file
189
ios/app/broadcast-extension/SocketConnection.m
Normal file
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* Copyright @ 2021-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
#import "SocketConnection.h"
|
||||
|
||||
@interface SocketConnection () <NSStreamDelegate>
|
||||
|
||||
@property (nonatomic, copy) NSString *filePath;
|
||||
|
||||
@property (nonatomic, strong) NSInputStream *inputStream;
|
||||
@property (nonatomic, strong) NSOutputStream *outputStream;
|
||||
|
||||
@property (nonatomic, strong) NSThread *networkThread;
|
||||
|
||||
@end
|
||||
|
||||
@implementation SocketConnection {
|
||||
int _socket;
|
||||
struct sockaddr_un _socketAddr;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFilePath:(NSString *)path {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.filePath = path;
|
||||
|
||||
[self setupSocketWithFilePath:path];
|
||||
[self setupNetworkThread];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)open {
|
||||
NSLog(@"Open socket connection");
|
||||
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:self.filePath]) {
|
||||
NSLog(@"failure: socket file missing");
|
||||
return false;
|
||||
}
|
||||
|
||||
int status = connect(_socket, (struct sockaddr *)&_socketAddr, sizeof(_socketAddr));
|
||||
if (status < 0) {
|
||||
NSLog(@"failure: socket connect (%d)", status);
|
||||
return false;
|
||||
}
|
||||
|
||||
[self.networkThread start];
|
||||
|
||||
CFReadStreamRef readStream;
|
||||
CFWriteStreamRef writeStream;
|
||||
|
||||
CFStreamCreatePairWithSocket(kCFAllocatorDefault, _socket, &readStream, &writeStream);
|
||||
|
||||
self.inputStream = (__bridge_transfer NSInputStream *)readStream;
|
||||
self.inputStream.delegate = self;
|
||||
[self.inputStream setProperty:@"kCFBooleanTrue" forKey:@"kCFStreamPropertyShouldCloseNativeSocket"];
|
||||
|
||||
self.outputStream = (__bridge_transfer NSOutputStream *)writeStream;
|
||||
self.outputStream.delegate = self;
|
||||
[self.outputStream setProperty:@"kCFBooleanTrue" forKey:@"kCFStreamPropertyShouldCloseNativeSocket"];
|
||||
|
||||
[self performSelector:@selector(scheduleStreams) onThread:self.networkThread withObject:nil waitUntilDone:true];
|
||||
|
||||
[self.inputStream open];
|
||||
[self.outputStream open];
|
||||
|
||||
NSLog(@"read stream status: %ld", CFReadStreamGetStatus(readStream));
|
||||
NSLog(@"write stream status: %ld", CFWriteStreamGetStatus(writeStream));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
- (void)close {
|
||||
[self performSelector:@selector(unscheduleStreams) onThread:self.networkThread withObject:nil waitUntilDone:true];
|
||||
|
||||
self.inputStream.delegate = nil;
|
||||
self.outputStream.delegate = nil;
|
||||
|
||||
[self.inputStream close];
|
||||
[self.outputStream close];
|
||||
|
||||
[self.networkThread cancel];
|
||||
}
|
||||
|
||||
- (NSInteger)writeBufferToStream:(const uint8_t*)buffer maxLength:(NSInteger)length {
|
||||
return [self.outputStream write:buffer maxLength:length];
|
||||
}
|
||||
|
||||
// MARK: Private Methods
|
||||
|
||||
- (BOOL)isOpen {
|
||||
return self.inputStream.streamStatus == NSStreamStatusOpen && self.outputStream.streamStatus == NSStreamStatusOpen;
|
||||
}
|
||||
|
||||
- (void)setupSocketWithFilePath:(NSString*)path {
|
||||
_socket = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
|
||||
memset(&_socketAddr, 0, sizeof(_socketAddr));
|
||||
_socketAddr.sun_family = AF_UNIX;
|
||||
strncpy(_socketAddr.sun_path, path.UTF8String, sizeof(_socketAddr.sun_path) - 1);
|
||||
}
|
||||
|
||||
- (void)setupNetworkThread {
|
||||
self.networkThread = [[NSThread alloc] initWithBlock:^{
|
||||
do {
|
||||
@autoreleasepool {
|
||||
[[NSRunLoop currentRunLoop] run];
|
||||
}
|
||||
} while (![NSThread currentThread].isCancelled);
|
||||
}];
|
||||
self.networkThread.qualityOfService = NSQualityOfServiceUserInitiated;
|
||||
}
|
||||
|
||||
- (void)scheduleStreams {
|
||||
[self.inputStream scheduleInRunLoop:NSRunLoop.currentRunLoop forMode:NSRunLoopCommonModes];
|
||||
[self.outputStream scheduleInRunLoop:NSRunLoop.currentRunLoop forMode:NSRunLoopCommonModes];
|
||||
}
|
||||
|
||||
- (void)unscheduleStreams {
|
||||
[self.inputStream removeFromRunLoop:NSRunLoop.currentRunLoop forMode:NSRunLoopCommonModes];
|
||||
[self.outputStream removeFromRunLoop:NSRunLoop.currentRunLoop forMode:NSRunLoopCommonModes];
|
||||
}
|
||||
|
||||
- (void)notifyDidClose:(NSError *)error {
|
||||
if (self.didClose) {
|
||||
self.didClose(error);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - NSStreamDelegate
|
||||
|
||||
@implementation SocketConnection (NSStreamDelegate)
|
||||
|
||||
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
|
||||
switch (eventCode) {
|
||||
case NSStreamEventOpenCompleted:
|
||||
NSLog(@"client stream open completed");
|
||||
if (aStream == self.outputStream && self.didOpen) {
|
||||
self.didOpen();
|
||||
}
|
||||
break;
|
||||
case NSStreamEventHasBytesAvailable:
|
||||
if (aStream == self.inputStream) {
|
||||
uint8_t buffer;
|
||||
NSInteger numberOfBytesRead = [(NSInputStream *)aStream read:&buffer maxLength:sizeof(buffer)];
|
||||
if (!numberOfBytesRead && aStream.streamStatus == NSStreamStatusAtEnd) {
|
||||
NSLog(@"server socket closed");
|
||||
[self close];
|
||||
[self notifyDidClose:nil];
|
||||
}
|
||||
}
|
||||
break;
|
||||
case NSStreamEventHasSpaceAvailable:
|
||||
if (aStream == self.outputStream && self.streamHasSpaceAvailable) {
|
||||
NSLog(@"client stream has space available");
|
||||
self.streamHasSpaceAvailable();
|
||||
}
|
||||
break;
|
||||
case NSStreamEventErrorOccurred:
|
||||
NSLog(@"client stream error occurred: %@", aStream.streamError);
|
||||
[self close];
|
||||
[self notifyDidClose:aStream.streamError];
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
10
ios/app/broadcast-extension/extension.entitlements
Normal file
10
ios/app/broadcast-extension/extension.entitlements
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.org.jitsi.meet.appgroup</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -51,7 +51,7 @@
|
||||
if ([FIRUtilities appContainsRealServiceInfoPlist]) {
|
||||
NSLog(@"Enabling Firebase");
|
||||
[FIRApp configure];
|
||||
// Crashlytics defaults to disabled wirth the FirebaseCrashlyticsCollectionEnabled Info.plist key.
|
||||
// Crashlytics defaults to disabled with the FirebaseCrashlyticsCollectionEnabled Info.plist key.
|
||||
[[FIRCrashlytics crashlytics] setCrashlyticsCollectionEnabled:![jitsiMeet isCrashReportingDisabled]];
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>FirebaseCrashlyticsCollectionEnabled</key>
|
||||
<string>false</string>
|
||||
<key>FirebaseScreenReportingEnabled</key>
|
||||
<false/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
@@ -66,14 +68,18 @@
|
||||
<string>See your scheduled meetings in the app.</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Participate in meetings with video.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Participate in meetings with voice.</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>Local network is used for establishing Peer-to-Peer connections.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Participate in meetings with voice.</string>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>org.jitsi.JitsiMeet.ios.conference</string>
|
||||
</array>
|
||||
<key>RTCAppGroupIdentifier</key>
|
||||
<string>group.org.jitsi.meet.appgroup</string>
|
||||
<key>RTCScreenSharingExtension</key>
|
||||
<string>org.jitsi.meet.broadcast.extension</string>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
@@ -99,7 +105,5 @@
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>FirebaseCrashlyticsCollectionEnabled</key>
|
||||
<string>false</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -35,7 +35,7 @@ fi
|
||||
|
||||
echo "PR_REPO_SLUG=${PR_REPO_SLUG} PR_BRANCH=${PR_BRANCH}"
|
||||
|
||||
# do the marge and git log
|
||||
# do the merge and git log
|
||||
|
||||
if [ $PR_BRANCH != "master" ]; then
|
||||
echo "Will merge ${PR_REPO_SLUG}/${PR_BRANCH} into master"
|
||||
|
||||
@@ -90,7 +90,7 @@ echo "importing dev-key.p12"
|
||||
security import ${CERT_DIR}/dev-key.p12 -k ios-build.keychain -P $IOS_SIGNING_CERT_PASSWORD -A
|
||||
|
||||
echo "will set-key-partition-list"
|
||||
# Fix for OS X Sierra that hungs in the codesign step
|
||||
# Fix for OS X Sierra that hangs in the codesign step
|
||||
security set-key-partition-list -S apple-tool:,apple: -s -k $ENCRYPTION_PASSWORD ios-build.keychain > /dev/null
|
||||
echo "done set-key-partition-list"
|
||||
|
||||
|
||||
@@ -16,6 +16,20 @@ platform :ios do
|
||||
app_identifier: "com.atlassian.JitsiMeet.ios"
|
||||
)
|
||||
|
||||
# Set the broadcast extension identifier
|
||||
update_app_identifier(
|
||||
xcodeproj: "app/app.xcodeproj",
|
||||
plist_path: "broadcast-extension/Info.plist",
|
||||
app_identifier: "com.atlassian.JitsiMeet.ios.broadcast"
|
||||
)
|
||||
update_info_plist(
|
||||
xcodeproj: "app/app.xcodeproj",
|
||||
plist_path: "src/Info.plist",
|
||||
block: proc do |plist|
|
||||
plist["RTCScreenSharingExtension"] = "com.atlassian.JitsiMeet.ios.broadcast"
|
||||
end
|
||||
)
|
||||
|
||||
# Set the (watch) app identifier
|
||||
update_app_identifier(
|
||||
xcodeproj: "app/app.xcodeproj",
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
0BCA49601EC4B6C600B793EE /* POSIX.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BCA495D1EC4B6C600B793EE /* POSIX.m */; };
|
||||
0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BCA495E1EC4B6C600B793EE /* Proximity.m */; };
|
||||
0BD906EA1EC0C00300C8C18E /* JitsiMeet.h in Headers */ = {isa = PBXBuildFile; fileRef = 0BD906E81EC0C00300C8C18E /* JitsiMeet.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
4E51B76425E5345E0038575A /* ScheenshareEventEmiter.h in Headers */ = {isa = PBXBuildFile; fileRef = 4E51B76225E5345E0038575A /* ScheenshareEventEmiter.h */; };
|
||||
4E51B76525E5345E0038575A /* ScheenshareEventEmiter.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E51B76325E5345E0038575A /* ScheenshareEventEmiter.m */; };
|
||||
6C31EDC820C06D490089C899 /* recordingOn.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 6C31EDC720C06D490089C899 /* recordingOn.mp3 */; };
|
||||
6C31EDCA20C06D530089C899 /* recordingOff.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 6C31EDC920C06D530089C899 /* recordingOff.mp3 */; };
|
||||
6F08DF7D4458EE3CF3F36F6D /* libPods-JitsiMeetSDK.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E4376CA6886DE68FD7A4294B /* libPods-JitsiMeetSDK.a */; };
|
||||
@@ -85,6 +87,8 @@
|
||||
0BD906E51EC0C00300C8C18E /* JitsiMeetSDK.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = JitsiMeetSDK.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
0BD906E81EC0C00300C8C18E /* JitsiMeet.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiMeet.h; sourceTree = "<group>"; };
|
||||
0BD906E91EC0C00300C8C18E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
4E51B76225E5345E0038575A /* ScheenshareEventEmiter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ScheenshareEventEmiter.h; sourceTree = "<group>"; };
|
||||
4E51B76325E5345E0038575A /* ScheenshareEventEmiter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ScheenshareEventEmiter.m; sourceTree = "<group>"; };
|
||||
6C31EDC720C06D490089C899 /* recordingOn.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = recordingOn.mp3; path = ../../sounds/recordingOn.mp3; sourceTree = "<group>"; };
|
||||
6C31EDC920C06D530089C899 /* recordingOff.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = recordingOff.mp3; path = ../../sounds/recordingOff.mp3; sourceTree = "<group>"; };
|
||||
75635B0820751D6D00F29C9F /* joined.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = joined.wav; path = ../../sounds/joined.wav; sourceTree = "<group>"; };
|
||||
@@ -231,6 +235,8 @@
|
||||
C8AFD27D2462C613000293D2 /* InfoPlistUtil.h */,
|
||||
C8AFD27E2462C613000293D2 /* InfoPlistUtil.m */,
|
||||
C81E9AB825AC5AD800B134D9 /* ExternalAPI.h */,
|
||||
4E51B76225E5345E0038575A /* ScheenshareEventEmiter.h */,
|
||||
4E51B76325E5345E0038575A /* ScheenshareEventEmiter.m */,
|
||||
);
|
||||
path = src;
|
||||
sourceTree = "<group>";
|
||||
@@ -298,6 +304,7 @@
|
||||
0B93EF7E1EC9DDCD0030D24D /* RCTBridgeWrapper.h in Headers */,
|
||||
DE81A2DE2317ED5400AE1940 /* JitsiMeetBaseLogHandler.h in Headers */,
|
||||
DEA9F284258A5D9900D4CD74 /* JitsiMeetSDK.h in Headers */,
|
||||
4E51B76425E5345E0038575A /* ScheenshareEventEmiter.h in Headers */,
|
||||
DE65AACC2318028300290BEC /* JitsiMeetBaseLogHandler+Private.h in Headers */,
|
||||
0B412F221EDEF6EA00B1A0A6 /* JitsiMeetViewDelegate.h in Headers */,
|
||||
0BD906EA1EC0C00300C8C18E /* JitsiMeet.h in Headers */,
|
||||
@@ -466,6 +473,7 @@
|
||||
C69EFA0C209A0F660027712B /* JMCallKitEmitter.swift in Sources */,
|
||||
DEFE535621FB2E8300011A3A /* ReactUtils.m in Sources */,
|
||||
C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */,
|
||||
4E51B76525E5345E0038575A /* ScheenshareEventEmiter.m in Sources */,
|
||||
A4A934E9212F3ADB001E9388 /* Dropbox.m in Sources */,
|
||||
C69EFA0D209A0F660027712B /* JMCallKitProxy.swift in Sources */,
|
||||
DE81A2D52316AC4D00AE1940 /* JitsiMeetLogger.m in Sources */,
|
||||
|
||||
@@ -20,11 +20,11 @@
|
||||
|
||||
- (void)sendHangUp;
|
||||
- (void)sendSetAudioMuted:(BOOL)muted;
|
||||
- (void)sendEndpointTextMessage:(NSString*)to :(NSString*)message;
|
||||
- (void)sendEndpointTextMessage:(NSString*)message :(NSString*)to;
|
||||
- (void)toggleScreenShare;
|
||||
- (void)retrieveParticipantsInfo:(void (^)(NSArray*))completion;
|
||||
- (void)openChat:(NSString*)to;
|
||||
- (void)closeChat;
|
||||
- (void)sendChatMessage:(NSString*)to :(NSString*)message;
|
||||
- (void)sendChatMessage:(NSString*)message :(NSString*)to ;
|
||||
|
||||
@end
|
||||
|
||||
@@ -153,7 +153,7 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name
|
||||
[self sendEventWithName:setAudioMutedAction body:data];
|
||||
}
|
||||
|
||||
- (void)sendEndpointTextMessage:(NSString*)to :(NSString*)message {
|
||||
- (void)sendEndpointTextMessage:(NSString*)message :(NSString*)to {
|
||||
NSMutableDictionary *data = [[NSMutableDictionary alloc] init];
|
||||
data[@"to"] = to;
|
||||
data[@"message"] = message;
|
||||
@@ -185,7 +185,7 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name
|
||||
[self sendEventWithName:closeChatAction body:nil];
|
||||
}
|
||||
|
||||
- (void)sendChatMessage:(NSString*)to :(NSString*)message {
|
||||
- (void)sendChatMessage:(NSString*)message :(NSString*)to {
|
||||
NSMutableDictionary *data = [[NSMutableDictionary alloc] init];
|
||||
data[@"to"] = to;
|
||||
data[@"message"] = message;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>3.1.0</string>
|
||||
<string>3.2.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#import "RCTBridgeWrapper.h"
|
||||
#import "ReactUtils.h"
|
||||
#import "RNSplashScreen.h"
|
||||
#import "ScheenshareEventEmiter.h"
|
||||
|
||||
#import <RNGoogleSignin/RNGoogleSignin.h>
|
||||
#import <WebRTC/RTCLogging.h>
|
||||
@@ -31,6 +32,7 @@
|
||||
@implementation JitsiMeet {
|
||||
RCTBridgeWrapper *_bridgeWrapper;
|
||||
NSDictionary *_launchOptions;
|
||||
ScheenshareEventEmiter *_screenshareEventEmiter;
|
||||
}
|
||||
|
||||
#pragma mak - This class is a singleton
|
||||
@@ -50,6 +52,9 @@
|
||||
if (self = [super init]) {
|
||||
// Initialize the on and only bridge for interfacing with React Native.
|
||||
_bridgeWrapper = [[RCTBridgeWrapper alloc] init];
|
||||
|
||||
// Initialize the listener for handling start/stop screensharing notifications.
|
||||
_screenshareEventEmiter = [[ScheenshareEventEmiter alloc] init];
|
||||
|
||||
// Register a fatal error handler for React.
|
||||
registerReactFatalErrorHandler();
|
||||
|
||||
@@ -38,11 +38,11 @@
|
||||
- (void)leave;
|
||||
- (void)hangUp;
|
||||
- (void)setAudioMuted:(BOOL)muted;
|
||||
- (void)sendEndpointTextMessage:(NSString*)to :(NSString*)message;
|
||||
- (void)sendEndpointTextMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to;
|
||||
- (void)toggleScreenShare;
|
||||
- (void)retrieveParticipantsInfo:(void (^)(NSArray*))completionHandler;
|
||||
- (void)openChat:(NSString*)to;
|
||||
- (void)retrieveParticipantsInfo:(void (^ _Nonnull)(NSArray * _Nullable))completionHandler;
|
||||
- (void)openChat:(NSString * _Nullable)to;
|
||||
- (void)closeChat;
|
||||
- (void)sendChatMessage:(NSString*)to :(NSString*)message;
|
||||
- (void)sendChatMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to;
|
||||
|
||||
@end
|
||||
|
||||
@@ -125,9 +125,9 @@ static void initializeViewsMap() {
|
||||
[externalAPI sendSetAudioMuted:muted];
|
||||
}
|
||||
|
||||
- (void)sendEndpointTextMessage:(NSString*)to :(NSString*)message {
|
||||
- (void)sendEndpointTextMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to {
|
||||
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
|
||||
[externalAPI sendEndpointTextMessage:to :message];
|
||||
[externalAPI sendEndpointTextMessage:message :to];
|
||||
}
|
||||
|
||||
- (void)toggleScreenShare {
|
||||
@@ -135,7 +135,7 @@ static void initializeViewsMap() {
|
||||
[externalAPI toggleScreenShare];
|
||||
}
|
||||
|
||||
- (void)retrieveParticipantsInfo:(void (^)(NSArray*))completionHandler {
|
||||
- (void)retrieveParticipantsInfo:(void (^ _Nonnull)(NSArray * _Nullable))completionHandler {
|
||||
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
|
||||
[externalAPI retrieveParticipantsInfo:completionHandler];
|
||||
}
|
||||
@@ -150,9 +150,9 @@ static void initializeViewsMap() {
|
||||
[externalAPI closeChat];
|
||||
}
|
||||
|
||||
- (void)sendChatMessage:(NSString*)to :(NSString*)message {
|
||||
- (void)sendChatMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to {
|
||||
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
|
||||
[externalAPI sendChatMessage:to :message];
|
||||
[externalAPI sendChatMessage:message :to];
|
||||
}
|
||||
|
||||
#pragma mark Private methods
|
||||
@@ -184,7 +184,7 @@ static void initializeViewsMap() {
|
||||
// conference. However, React and, respectively,
|
||||
// appProperties/initialProperties are declarative expressions i.e. one and
|
||||
// the same URL will not trigger an automatic re-render in the JavaScript
|
||||
// source code. The workaround implemented bellow introduces imperativeness
|
||||
// source code. The workaround implemented below introduces imperativeness
|
||||
// in React Component props by defining a unique value per invocation.
|
||||
props[@"timestamp"] = @(mach_absolute_time());
|
||||
|
||||
|
||||
25
ios/sdk/src/ScheenshareEventEmiter.h
Normal file
25
ios/sdk/src/ScheenshareEventEmiter.h
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright @ 2021-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ScheenshareEventEmiter : NSObject
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
63
ios/sdk/src/ScheenshareEventEmiter.m
Normal file
63
ios/sdk/src/ScheenshareEventEmiter.m
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright @ 2021-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import "ScheenshareEventEmiter.h"
|
||||
#import "JitsiMeet+Private.h"
|
||||
#import "ExternalAPI.h"
|
||||
|
||||
NSNotificationName const kBroadcastStartedNotification = @"iOS_BroadcastStarted";
|
||||
NSNotificationName const kBroadcastStoppedNotification = @"iOS_BroadcastStopped";
|
||||
|
||||
@implementation ScheenshareEventEmiter {
|
||||
CFNotificationCenterRef _notificationCenter;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_notificationCenter = CFNotificationCenterGetDarwinNotifyCenter();
|
||||
[self setupObserver];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self clearObserver];
|
||||
}
|
||||
|
||||
// MARK: Private Methods
|
||||
|
||||
- (void)setupObserver {
|
||||
CFNotificationCenterAddObserver(_notificationCenter, (__bridge const void *)(self), broadcastToggleNotificationCallback, (__bridge CFStringRef)kBroadcastStartedNotification, NULL, CFNotificationSuspensionBehaviorDeliverImmediately);
|
||||
CFNotificationCenterAddObserver(_notificationCenter, (__bridge const void *)(self), broadcastToggleNotificationCallback, (__bridge CFStringRef)kBroadcastStoppedNotification, NULL, CFNotificationSuspensionBehaviorDeliverImmediately);
|
||||
}
|
||||
|
||||
- (void)clearObserver {
|
||||
CFNotificationCenterRemoveObserver(_notificationCenter, (__bridge const void *)(self), (__bridge CFStringRef)kBroadcastStartedNotification, NULL);
|
||||
CFNotificationCenterRemoveObserver(_notificationCenter, (__bridge const void *)(self), (__bridge CFStringRef)kBroadcastStoppedNotification, NULL);
|
||||
}
|
||||
|
||||
void broadcastToggleNotificationCallback(CFNotificationCenterRef center,
|
||||
void *observer,
|
||||
CFStringRef name,
|
||||
const void *object,
|
||||
CFDictionaryRef userInfo) {
|
||||
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
|
||||
[externalAPI toggleScreenShare];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -76,7 +76,7 @@ internal final class JMCallKitEmitter: NSObject, CXProviderDelegate {
|
||||
|
||||
// Avoid mute actions ping-pong: if the mute action was caused by
|
||||
// the JS side (we requested a transaction) don't call the delegate
|
||||
// method. If it was called by the provder itself (when the user presses
|
||||
// method. If it was called by the provider itself (when the user presses
|
||||
// the mute button in the CallKit view) then call the delegate method.
|
||||
//
|
||||
// NOTE: don't try to be clever and remove this. Been there, done that.
|
||||
|
||||
@@ -29,7 +29,7 @@ public protocol PiPViewCoordinatorDelegate: class {
|
||||
/// when is presented in Picure in Picture mode.
|
||||
public class PiPViewCoordinator {
|
||||
|
||||
/// Limits the boundries of view position on screen when minimized
|
||||
/// Limits the boundaries of view position on screen when minimized
|
||||
public var dragBoundInsets: UIEdgeInsets = UIEdgeInsets(top: 25,
|
||||
left: 5,
|
||||
bottom: 5,
|
||||
|
||||
@@ -60,7 +60,7 @@ fi
|
||||
|
||||
echo "PR_REPO_SLUG=${PR_REPO_SLUG} PR_BRANCH=${PR_BRANCH}"
|
||||
|
||||
# do the marge and git log
|
||||
# do the merge and git log
|
||||
|
||||
if [ $PR_BRANCH != "master" ]; then
|
||||
echo "Will merge ${PR_REPO_SLUG}/${PR_BRANCH} into master"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
{
|
||||
"addPeople": {
|
||||
"add": "Inviter",
|
||||
"add": "Invitér",
|
||||
"countryNotSupported": "Vi supporterer ikke dette land endnu.",
|
||||
"countryReminder": "Ringer du til uden for USA? Benyt venligst landekode!",
|
||||
"disabled": "Du kan ikke invitere deltagere.",
|
||||
@@ -15,33 +16,33 @@
|
||||
"searchPeople": "Søg efter personer",
|
||||
"searchPeopleAndNumbers": "Søg efter personer eller tilføj deres telefonnummer",
|
||||
"telephone": "Telefon: {{number}}",
|
||||
"title": "Inviter personer til dette møde"
|
||||
"title": "Invitér personer til dette møde"
|
||||
},
|
||||
"audioDevices": {
|
||||
"bluetooth": "Bluetooth",
|
||||
"headphones": "Høretelefoner",
|
||||
"phone": "Telefon",
|
||||
"speaker": "Højtaler",
|
||||
"none": "Der er ikke nogen lyd enheder tilgængelige"
|
||||
"none": "Der er ikke nogen lydenheder tilgængelige"
|
||||
},
|
||||
"audioOnly": {
|
||||
"audioOnly": "Kun lyd"
|
||||
},
|
||||
"calendarSync": {
|
||||
"addMeetingURL": "Tilføj et mødelink",
|
||||
"confirmAddLink": "Ønsker du at tilføj et Jitsi link til denne aftale?",
|
||||
"confirmAddLink": "Ønsker du at tilføje et Jitsi link til denne aftale?",
|
||||
"error": {
|
||||
"appConfiguration": "Kalender integration er ikke sat korrekt op.",
|
||||
"generic": "Der er sket en fejl. Verificer venligst dine kalenderindstilliinger eller prøv på at genopfriske din kalender.",
|
||||
"notSignedIn": "Der er sket en fejl under log ind for at hente kalenderaftalerne. Kontroller venligst dine kalenderindstilliinger og forsøg at logge ind igen."
|
||||
"appConfiguration": "Kalenderintegration er ikke sat korrekt op.",
|
||||
"generic": "Der er sket en fejl. Verificér venligst dine kalenderindstillinger eller prøv at genopfriske din kalender.",
|
||||
"notSignedIn": "Der er sket en fejl under login for at hente kalenderaftalerne. Kontroller venligst dine kalenderindstillinger og forsøg at logge ind igen."
|
||||
},
|
||||
"join": "Deltag",
|
||||
"joinTooltip": "Deltag i mødet",
|
||||
"nextMeeting": "næste møde",
|
||||
"noEvents": "Der er ikke nogen kommande aftaler i kalenderen.",
|
||||
"ongoingMeeting": "igangværende møde",
|
||||
"permissionButton": "Åben indstillinger",
|
||||
"permissionMessage": "Kalender tilladelsen er nødvendig for at kunne se dine aftaler i appen.",
|
||||
"nextMeeting": "Næste møde",
|
||||
"noEvents": "Der er ikke nogen kommende aftaler i kalenderen.",
|
||||
"ongoingMeeting": "Igangværende møde",
|
||||
"permissionButton": "Åbn indstillinger",
|
||||
"permissionMessage": "Kalendertilladelsen er nødvendig for at kunne se dine aftaler i appen.",
|
||||
"refresh": "Genopfrisk aftaler",
|
||||
"today": "I dag"
|
||||
},
|
||||
@@ -57,7 +58,7 @@
|
||||
},
|
||||
"privateNotice": "Privat besked til {{recipient}}",
|
||||
"title": "Chat",
|
||||
"you": "dig"
|
||||
"you": "Dig"
|
||||
},
|
||||
"chromeExtensionBanner": {
|
||||
"installExtensionText": "Installér plugin for Google Kalender og Office 365 integration",
|
||||
@@ -69,8 +70,8 @@
|
||||
},
|
||||
"connection": {
|
||||
"ATTACHED": "Forbundet",
|
||||
"AUTHENTICATING": "Autoriserer",
|
||||
"AUTHFAIL": "Autorisation lykkedes ikke",
|
||||
"AUTHENTICATING": "Godkender",
|
||||
"AUTHFAIL": "Godkendelse lykkedes ikke",
|
||||
"CONNECTED": "Forbundet",
|
||||
"CONNECTING": "Forbinder",
|
||||
"CONNFAIL": "Forbindelse kunne ikke oprettes",
|
||||
@@ -96,7 +97,7 @@
|
||||
"localport": "Lokal port:",
|
||||
"localport_plural": "Lokale porte:",
|
||||
"more": "Vis mere",
|
||||
"packetloss": "Pakke tab:",
|
||||
"packetloss": "Pakketab:",
|
||||
"quality": {
|
||||
"good": "God",
|
||||
"inactive": "Inaktiv",
|
||||
@@ -133,9 +134,9 @@
|
||||
"defaultNickname": "ex. Jane Pink",
|
||||
"deviceError": {
|
||||
"cameraError": "Kunne ikke forbinde til dit kamera",
|
||||
"cameraPermission": "Kamera tilladelse mangler",
|
||||
"cameraPermission": "Kameratilladelse mangler",
|
||||
"microphoneError": "Kunne ikke forbinde til din mikrofon",
|
||||
"microphonePermission": "Mikrofon tilladelse mangler"
|
||||
"microphonePermission": "Mikrofontilladelse mangler"
|
||||
},
|
||||
"deviceSelection": {
|
||||
"noPermission": "Tilladelse ikke givet",
|
||||
@@ -145,21 +146,21 @@
|
||||
},
|
||||
"dialog": {
|
||||
"accessibilityLabel": {
|
||||
"liveStreaming": "Live Stream"
|
||||
"liveStreaming": "Livestream"
|
||||
},
|
||||
"allow": "Tillad",
|
||||
"alreadySharedVideoMsg": "En anden deltager deler allerede en video. Denne konference tillader kun en delt video af gangen.",
|
||||
"alreadySharedVideoTitle": "Det er kun muligt at dele en video af gangen",
|
||||
"applicationWindow": "Applikations vindue",
|
||||
"alreadySharedVideoTitle": "Det er kun muligt at dele én video ad gangen",
|
||||
"applicationWindow": "Applikationsvindue",
|
||||
"Back": "Tilbage",
|
||||
"cameraConstraintFailedError": "Dit kamera lever ikke op til de nødvendige krav..",
|
||||
"cameraNotFoundError": "Kamera kunne ikke findes.",
|
||||
"cameraNotSendingData": "Vi kan ikke tilgå dit kamera. Kontroller venligst om der er en anden applikation der gør brug af dit kamera, eller vælg en andet kamera og genindlæs siden.",
|
||||
"cameraNotSendingData": "Vi kan ikke tilgå dit kamera. Kontrollér venligst om der er en anden applikation der gør brug af dit kamera, eller vælg et andet kamera og genindlæs siden.",
|
||||
"cameraNotSendingDataTitle": "Kan ikke tilgå kamera",
|
||||
"cameraPermissionDeniedError": "Du har ikke givet tilladelse til at bruge dit kamera. Du kan stadig deltage i mødet men de andre deltagere vil ikke kunne se dig. Gør brug af kamera knappen i adressebaren for at give tilladelse.",
|
||||
"cameraUnknownError": "Kan ikke gør brug af dit kamera, årsag ukendt.",
|
||||
"cameraUnsupportedResolutionError": "Dit kamera supporterer ikke den nødvendige opløsning.",
|
||||
"Cancel": "Afbryd",
|
||||
"cameraPermissionDeniedError": "Du har ikke givet tilladelse til at bruge dit kamera. Du kan stadig deltage i mødet, men de andre deltagere vil ikke kunne se dig. Gør brug af kameraknappen i adressebaren for at give tilladelse.",
|
||||
"cameraUnknownError": "Kan ikke gøre brug af dit kamera, årsag ukendt.",
|
||||
"cameraUnsupportedResolutionError": "Dit kamera understøtter ikke den nødvendige opløsning.",
|
||||
"Cancel": "Annullér",
|
||||
"close": "Luk",
|
||||
"conferenceDisconnectMsg": "Kontroller venligst din netværksforbindelse. Forbinder igen om {{seconds}} sekunder…",
|
||||
"conferenceDisconnectTitle": "Din forbindelse er blevet afbrudt.",
|
||||
@@ -172,31 +173,31 @@
|
||||
"connectErrorWithMsg": "Det var ikke muligt at forbinde til mødet: {{msg}}",
|
||||
"connecting": "Forbinder",
|
||||
"contactSupport": "Kontakt support",
|
||||
"copy": "Kopier invitation",
|
||||
"dismiss": "Afbryd",
|
||||
"copy": "Kopiér invitation",
|
||||
"dismiss": "Afvis",
|
||||
"displayNameRequired": "Navn/alias er påkrævet",
|
||||
"done": "Gem",
|
||||
"enterDisplayName": "Indtast venligst dit navn/alias",
|
||||
"error": "Fejl",
|
||||
"externalInstallationMsg": "Du skal installerer vores skærmdelings plugin.",
|
||||
"externalInstallationMsg": "Du skal installere vores skærmdelingsplugin.",
|
||||
"externalInstallationTitle": "Plugin skal bruges",
|
||||
"goToStore": "Gå til webstore",
|
||||
"gracefulShutdown": "Vores service er pt. under vedligeholdese. Forsøg venligst igen senere.",
|
||||
"gracefulShutdown": "Vores service er pt. under vedligeholdelse. Prøv igen senere.",
|
||||
"IamHost": "Jeg er vært",
|
||||
"incorrectRoomLockPassword": "",
|
||||
"incorrectPassword": "Forkert brugernavn eller adgangskode",
|
||||
"inlineInstallationMsg": "Du skal installerer vores skærmdelings plugin.",
|
||||
"inlineInstallExtension": "Installer nu",
|
||||
"inlineInstallationMsg": "Du skal installere vores skærmdelingsplugin.",
|
||||
"inlineInstallExtension": "Installér nu",
|
||||
"internalError": "Der er opstået en fejl: {{error}}",
|
||||
"internalErrorTitle": "Intern fejl",
|
||||
"kickMessage": "Du er blevet afbrudt fra mødet!",
|
||||
"kickParticipantButton": "Afbryd",
|
||||
"kickParticipantDialog": "Er du sikker på at du vil afbryde den deltager?",
|
||||
"kickParticipantTitle": "Afbryd denne deltager?",
|
||||
"kickTitle": "Afbrudt fra møde",
|
||||
"liveStreaming": "Live Streaming",
|
||||
"liveStreamingDisabledForGuestTooltip": "Gæster kan ikke starte en live stream.",
|
||||
"liveStreamingDisabledTooltip": "Live streaming er slået fra.",
|
||||
"kickParticipantButton": "Smid ud",
|
||||
"kickParticipantDialog": "Er du sikker på at du vil smide denne deltager ud?",
|
||||
"kickParticipantTitle": "Smid denne deltager ud?",
|
||||
"kickTitle": "Smidt ud af mødet",
|
||||
"liveStreaming": "Livestreaming",
|
||||
"liveStreamingDisabledForGuestTooltip": "Gæster kan ikke starte en livestream.",
|
||||
"liveStreamingDisabledTooltip": "Livestreaming er slået fra.",
|
||||
"lockMessage": "Kunne ikke sikre mødet.",
|
||||
"lockRoom": "Tilføj adgangskode",
|
||||
"lockTitle": "Sikring fejlet",
|
||||
@@ -208,79 +209,79 @@
|
||||
"micNotFoundError": "Mikrofon ikke fundet.",
|
||||
"micNotSendingData": "Vi kunne ikke tilgå din mikrofon. Vælg en anden mikrofon under indstillinger eller genindlæs applikationen.",
|
||||
"micNotSendingDataTitle": "Kunne ikke tilgå din mikrofon",
|
||||
"micPermissionDeniedError": "Du har ikke givet tilladelse til at bruge din mikrofon. Du kan stadig deltage i mødet men de andre deltagere kan ikke høre dig. Benyt kamera knappen i adressbaren for at give tilladelse.",
|
||||
"micPermissionDeniedError": "Du har ikke givet tilladelse til at bruge din mikrofon. Du kan stadig deltage i mødet men de andre deltagere kan ikke høre dig. Benyt kameraknappen i adressebaren for at give tilladelse.",
|
||||
"micUnknownError": "Kan ikke tilgå mikrofon af ukendt årsag.",
|
||||
"muteEveryoneElseDialog": "Er du sikker på at du vil slå lyden fra for denne deltager? Du kan ikke tænde igen, men de kan selv tænde til enhver tid.",
|
||||
"muteEveryoneElseDialog": "Er du sikker på at du vil slå lyden fra for alle andre end denne deltager? Du kan ikke tænde lyden igen, men de kan selv tænde til enhver tid.",
|
||||
"muteEveryoneElseTitle": "Slå lyd fra for alle undtagen {{whom}}?",
|
||||
"muteEveryoneDialog": "Er du sikker på at du vil slå lyden fra for denne deltager? Du kan ikke tænde igen, men de kan selv tænde til enhver tid.",
|
||||
"muteEveryoneDialog": "Er du sikker på at du vil slå lyden fra for alle? Du kan ikke tænde lyden igen, men de kan selv tænde til enhver tid.",
|
||||
"muteEveryoneTitle": "Slå lyd fra for alle?",
|
||||
"muteEveryoneSelf": "dig selv",
|
||||
"muteEveryoneStartMuted": "Lyden er slpet fra for alle fra nu af",
|
||||
"muteEveryoneSelf": "Dig selv",
|
||||
"muteEveryoneStartMuted": "Lyden er slået fra for alle fra nu af",
|
||||
"muteParticipantBody": "Du kan ikke tænde for deres mikrofon, men de kan selv tænde for deres mikrofon til enhver tid.",
|
||||
"muteParticipantButton": "Slå lyd fra",
|
||||
"muteParticipantDialog": "Er du sikker på at du vil slå lyden fra for denne deltager? Du kan ikke tænde igen, men de kan selv tænde til enhver tid.",
|
||||
"muteParticipantTitle": "Slå lyd fra for denne deltager?",
|
||||
"Ok": "Ok",
|
||||
"muteParticipantTitle": "Slå lyden fra for denne deltager?",
|
||||
"Ok": "OK",
|
||||
"passwordLabel": "Adgangskode",
|
||||
"passwordNotSupported": "Sætte en adgangskode for mødet er ikke understøttet.",
|
||||
"passwordNotSupported": "Det er ikke understøttet at sætte en adgangskode for mødet",
|
||||
"passwordNotSupportedTitle": "Adgangskode er ikke understøttet",
|
||||
"passwordRequired": "Adgangskode påkrævet",
|
||||
"popupError": "Din browser blockerer for pop-upper fra denne hjemmeside. Slå venligst pop-upper til i din browsers indstillinger og forsøg igen.",
|
||||
"popupErrorTitle": "Pop-up blokkeret",
|
||||
"popupError": "Din browser blokerer for pop op-vinduer fra denne hjemmeside. Slå venligst pop op-vinduer til i din browsers indstillinger og forsøg igen.",
|
||||
"popupErrorTitle": "Pop op-vinduer er blokeret",
|
||||
"recording": "Optager",
|
||||
"recordingDisabledForGuestTooltip": "Gæster kan ikke starte en optagelse.",
|
||||
"recordingDisabledTooltip": "Start optagelse er slået fra.",
|
||||
"recordingDisabledForGuestTooltip": "Gæster kan ikke starte en optagelse",
|
||||
"recordingDisabledTooltip": "Optagefunktionalitet er slået fra",
|
||||
"rejoinNow": "Deltag igen",
|
||||
"remoteControlAllowedMessage": "{{user}} accepterede din fjernstyring anmodning!",
|
||||
"remoteControlDeniedMessage": "{{user}} afviste din fjernstyring anmodning!",
|
||||
"remoteControlAllowedMessage": "{{user}} accepterede din anmodning om fjernstyring!",
|
||||
"remoteControlDeniedMessage": "{{user}} afviste din anmodning om fjernstyring!",
|
||||
"remoteControlErrorMessage": "En fejl er opstået ved anmodning om fjernstyring for {{user}}!",
|
||||
"remoteControlRequestMessage": "Vil du tillade at {{user}} fjerstyrer din computer?",
|
||||
"remoteControlShareScreenWarning": "Bemærk at hvis du vælger \"Tillad\" så vil du tillade fjernstyring!",
|
||||
"remoteControlStopMessage": "Fjernstyrings sessionen er afsluttet!",
|
||||
"remoteControlTitle": "Fjerstyring",
|
||||
"remoteControlStopMessage": "Fjernstyringssessionen er afsluttet!",
|
||||
"remoteControlTitle": "Fjernstyring",
|
||||
"Remove": "Fjern",
|
||||
"removePassword": "Fjern adgangskode",
|
||||
"removeSharedVideoMsg": "Er du sikker på at du vil fjerne di delte video?",
|
||||
"removeSharedVideoMsg": "Er du sikker på at du vil fjerne din delte video?",
|
||||
"removeSharedVideoTitle": "Fjern delt video",
|
||||
"reservationError": "Reservation - system fejl",
|
||||
"reservationErrorMsg": "Fejlkode: {{code}}, fejl: {{msg}}",
|
||||
"retry": "Forsøg igen",
|
||||
"screenSharingFailedToInstall": "Skærmdelings-plugin kunne ikke installeres.",
|
||||
"screenSharingFailedToInstallTitle": "Skærmdelings-plugin kunne ikke installeres.",
|
||||
"screenSharingFirefoxPermissionDeniedError": "Noget gik galt under skærmdeling. Kontroller venligst at du har givet tilladelse til skærmdeling. ",
|
||||
"screenSharingFirefoxPermissionDeniedError": "Noget gik galt under skærmdeling. Kontrollér venligst at du har givet tilladelse til skærmdeling. ",
|
||||
"screenSharingFirefoxPermissionDeniedTitle": "Skærmdeling kunne ikke startes!",
|
||||
"screenSharingPermissionDeniedError": "Noget gik galt under skærmdeling. Kontroller venligst at du har givet tilladelse til skærmdeling. Genindlæs siden og forsøg igen.",
|
||||
"sendPrivateMessage": "Du har modtage en privat besked. Ønsker du at svare private eller øsnker du at svare til gruppen?",
|
||||
"screenSharingPermissionDeniedError": "Noget gik galt under skærmdeling. Kontrollér venligst at du har givet tilladelse til skærmdeling. Genindlæs siden og forsøg igen.",
|
||||
"sendPrivateMessage": "Du har modtaget en privat besked. Ønsker du at svare privat eller til gruppen?",
|
||||
"sendPrivateMessageCancel": "Send til gruppen",
|
||||
"sendPrivateMessageOk": "Send privat",
|
||||
"sendPrivateMessageTitle": "Send privat?",
|
||||
"serviceUnavailable": "Service er ikke tilgængelig",
|
||||
"sessTerminated": "Møde afsluttet",
|
||||
"Share": "Del",
|
||||
"shareVideoLinkError": "Angiv venligst et validt Youtube link.",
|
||||
"shareVideoLinkError": "Angiv venligst et gyldigt YouTube link.",
|
||||
"shareVideoTitle": "Del en video",
|
||||
"shareYourScreen": "Del din skærm",
|
||||
"shareYourScreenDisabled": "Skærmdeling er ikke slået til.",
|
||||
"shareYourScreenDisabledForGuest": "Gæster kan ikke dele deres skærm.",
|
||||
"startLiveStreaming": "Start live stream",
|
||||
"startLiveStreaming": "Start livestream",
|
||||
"startRecording": "Start optagelse",
|
||||
"startRemoteControlErrorMessage": "Der er sket en fejl under opstart af fjern kontrol sessionen!",
|
||||
"stopLiveStreaming": "Stop live stream",
|
||||
"startRemoteControlErrorMessage": "Der er sket en fejl under opstart af fjernstyringssessionen!",
|
||||
"stopLiveStreaming": "Stop livestream",
|
||||
"stopRecording": "Stop optagelse",
|
||||
"stopRecordingWarning": "Er du sikker på at du ønsker at stoppe optagelesen?",
|
||||
"stopStreamingWarning": "Er du sikker på at du ønsker at stoppe live streaming?",
|
||||
"streamKey": "Live stream nøgle",
|
||||
"stopRecordingWarning": "Er du sikker på at du ønsker at stoppe optagelsen?",
|
||||
"stopStreamingWarning": "Er du sikker på at du ønsker at stoppe livestreaming?",
|
||||
"streamKey": "Livestream-nøgle",
|
||||
"Submit": "Gem",
|
||||
"thankYou": "Tak for at du har benyttet {{appName}}!",
|
||||
"token": "token",
|
||||
"tokenAuthFailed": "Beklager, du kan ikke deltage i dette møde.",
|
||||
"tokenAuthFailedTitle": "Log ind fejlede",
|
||||
"transcribing": "Transcribering",
|
||||
"unlockRoom": "Fjern møde adgangskode",
|
||||
"userPassword": "bruger adgangskode",
|
||||
"WaitForHostMsg": "Mødet <b>{{room}}</b> er ikke startet endnu. Hvis du er hosten log venligst ind, ellers vent på at hosten kommer.",
|
||||
"WaitForHostMsgWOk": "Mødet <b>{{room}}</b> er ikke startet endnu. Hvis du er hosten tryk venligst på OK for at logge ind, ellers vent på at hosten kommer.",
|
||||
"WaitingForHost": "Venter på host …",
|
||||
"tokenAuthFailedTitle": "Login fejlede",
|
||||
"transcribing": "Transskribering",
|
||||
"unlockRoom": "Fjern mødeadgangskode",
|
||||
"userPassword": "Brugeradgangskode",
|
||||
"WaitForHostMsg": "Mødet <b>{{room}}</b> er ikke startet endnu. Hvis du er værten, log venligst ind. Ellers vent på at værten kommer",
|
||||
"WaitForHostMsgWOk": "Mødet <b>{{room}}</b> er ikke startet endnu. Hvis du er værten, tryk venligst på OK for at logge ind. Ellers vent på at værten kommer.",
|
||||
"WaitingForHost": "Venter på vært …",
|
||||
"Yes": "Ja",
|
||||
"yourEntireScreen": "Hele din skærm"
|
||||
},
|
||||
@@ -295,37 +296,37 @@
|
||||
"bad": "Dårlig",
|
||||
"detailsLabel": "Uddyb.",
|
||||
"good": "God",
|
||||
"rateExperience": "Vurder din oplevelse",
|
||||
"rateExperience": "Vurdér din oplevelse",
|
||||
"veryBad": "Meget dårlig",
|
||||
"veryGood": "Meget god"
|
||||
},
|
||||
"incomingCall": {
|
||||
"answer": "Besvar",
|
||||
"audioCallTitle": "Indkommende opkald",
|
||||
"audioCallTitle": "Indgående opkald",
|
||||
"decline": "Afvis",
|
||||
"productLabel": "fra Appinux skærmbesøg",
|
||||
"videoCallTitle": "Indkommende videoopkald"
|
||||
"productLabel": "Fra Appinux skærmbesøg",
|
||||
"videoCallTitle": "Indgående videoopkald"
|
||||
},
|
||||
"info": {
|
||||
"accessibilityLabel": "Vis info",
|
||||
"addPassword": "Tilføj adgangskode",
|
||||
"cancelPassword": "Annulé adgangskode",
|
||||
"cancelPassword": "Annullér adgangskode",
|
||||
"conferenceURL": "Link:",
|
||||
"country": "Land",
|
||||
"dialANumber": "For at deltage i mødet ring til et af følgende telefonnumre og indtast pinkode.",
|
||||
"dialInConferenceID": "Pinkode:",
|
||||
"dialInNotSupported": "Deltagelse vis telfonen er pt. ikke understøttet.",
|
||||
"dialInNotSupported": "Deltagelse via telefonopkald er pt. ikke understøttet.",
|
||||
"dialInNumber": "Ring ind:",
|
||||
"dialInSummaryError": "Der er opstået en fejl under hentning af ring ind information. Forsøg venligst igen senere.",
|
||||
"dialInSummaryError": "Der er opstået en fejl under indhenting af telefonopkaldsdetaljer. Forsøg venligst igen senere.",
|
||||
"dialInTollFree": "Gratis",
|
||||
"genericError": "Der er opstået en fejl",
|
||||
"inviteLiveStream": "For at se en live stream i dette møde klik på dette link: {{url}}",
|
||||
"inviteLiveStream": "For at se en livestream i dette møde klik på dette link: {{url}}",
|
||||
"invitePhone": "Ring-ind: {{number}},,{{conferenceID}}#\n",
|
||||
"invitePhoneAlternatives": "",
|
||||
"inviteURLFirstPartGeneral": "Der er blevet inviteret til at deltage i et møde.",
|
||||
"inviteURLFirstPartPersonal": "{{name}} inviterer dig til at deltage i et møde.\n",
|
||||
"inviteURLSecondPart": "\nDeltag i mødet:\n{{url}}\n",
|
||||
"liveStreamURL": "Live stream:",
|
||||
"liveStreamURL": "Livestream:",
|
||||
"moreNumbers": "Flere telefonnumre",
|
||||
"noNumbers": "Ring-ind nummer findes ikke.",
|
||||
"noPassword": "Ingen",
|
||||
@@ -333,8 +334,8 @@
|
||||
"numbers": "Ring-ind numre",
|
||||
"password": "Adgangskode:",
|
||||
"title": "Del",
|
||||
"tooltip": "Del link og rind-ind information for dette møde",
|
||||
"label": "Møde information"
|
||||
"tooltip": "Del link og ring-ind information for dette møde",
|
||||
"label": "Mødeinformation"
|
||||
},
|
||||
"inviteDialog": {
|
||||
"alertText": "Kunne ikke invitere nogle af deltagerne.",
|
||||
@@ -348,54 +349,54 @@
|
||||
"msg": "Der er opstået en fejl.",
|
||||
"retry": "Forsøg igen",
|
||||
"support": "Support",
|
||||
"supportMsg": "Hvis denne fejl bliver ved, kontakt"
|
||||
"supportMsg": "Hvis denne fejl bliver ved, kontakt da support"
|
||||
},
|
||||
"keyboardShortcuts": {
|
||||
"focusLocal": "Fokusér på din video",
|
||||
"focusRemote": "Fokusér på en anden persons video",
|
||||
"focusLocal": "Sæt fokus din video",
|
||||
"focusRemote": "Sæt fokus på en anden persons billede",
|
||||
"fullScreen": "Vis eller annullér fuld skærm",
|
||||
"keyboardShortcuts": "Tastaturgenveje",
|
||||
"localRecording": "Vis eller skjul optage kontrolknapper",
|
||||
"mute": "Slå lyd fra eller til for din mikrofon",
|
||||
"localRecording": "Vis eller skjul optagelseskontrolknapper",
|
||||
"mute": "Slå din mikrofon til eller fra",
|
||||
"pushToTalk": "Tryk for at tale",
|
||||
"raiseHand": "Lyft eller sænk din hånd",
|
||||
"showSpeakerStats": "Vis højtaler statistik",
|
||||
"toggleChat": "Åben eller luk chat",
|
||||
"toggleFilmstrip": "Vis eller skjul video små billeder",
|
||||
"toggleScreensharing": "Skift mellem video og skærmdeling",
|
||||
"raiseHand": "Løft eller sænk din hånd",
|
||||
"showSpeakerStats": "Vis højtalerstatistik",
|
||||
"toggleChat": "Åbn eller luk chat",
|
||||
"toggleFilmstrip": "Vis eller skjul små videobilleder",
|
||||
"toggleScreensharing": "Skift mellem video- og skærmdeling",
|
||||
"toggleShortcuts": "Vis eller skjul tastaturgenveje",
|
||||
"videoMute": "Tænd eller sluk dit kamera",
|
||||
"videoQuality": "Indstil opkaldskvalitet"
|
||||
"videoQuality": "Indstil videokvalitet"
|
||||
},
|
||||
"liveStreaming": {
|
||||
"busy": "Der arbejdes på at frigive streamingresourcer, forsøg venligst igen om et par minutter.",
|
||||
"busyTitle": "Alle streamers er optaget",
|
||||
"busy": "Der arbejdes på at frigive streamingressourcer, forsøg venligst igen om et par minutter.",
|
||||
"busyTitle": "Alle streams er optaget",
|
||||
"changeSignIn": "Skift konto.",
|
||||
"choose": "Vælg en live stream",
|
||||
"chooseCTA": "Vælg en stream mulighed. Du er pt. logget ind som {{email}}.",
|
||||
"enterStreamKey": "Indtast din Youtube live stream her.",
|
||||
"error": "Fejl under live streaming. Forsøg venligst igen.",
|
||||
"choose": "Vælg en livestream",
|
||||
"chooseCTA": "Vælg en stream-mulighed. Du er pt. logget ind som {{email}}.",
|
||||
"enterStreamKey": "Indtast din livestream-nøgle til Youtube her.",
|
||||
"error": "Fejl under livestreaming. Forsøg venligst igen.",
|
||||
"errorAPI": "En fejl opstod ved forsøg på at tilgå din YouTube-broadcast. Forsøg venligst at logge ind igen.",
|
||||
"errorLiveStreamNotEnabled": "Live Streaming er ikke slpet til for {{email}}. Slå venligst live streaming til eller log ind på en konto der har live streaming slået til.",
|
||||
"expandedOff": "Live stream er stoppet",
|
||||
"errorLiveStreamNotEnabled": "Livestreaming er ikke slået til for {{email}}. Slå venligst livestreaming til eller log ind på en konto der har livestreaming slået til.",
|
||||
"expandedOff": "Livestream er stoppet",
|
||||
"expandedOn": "Mødet bliver pt. streamet til Youtube.",
|
||||
"expandedPending": "Live streaming starter…",
|
||||
"failedToStart": "Kunne ikke starte live streaming",
|
||||
"getStreamKeyManually": "Vi kunne ikke hente nogle live streams. Forsøg at hente din live stream nøgle fra Youtube.",
|
||||
"invalidStreamKey": "Live stream nøgle er muligvis forkert.",
|
||||
"off": "Live Streaming er stoppet",
|
||||
"offBy": "{{name}} stoppede live streaming",
|
||||
"on": "Live Streaming",
|
||||
"onBy": "{{name}} startede live streaming",
|
||||
"pending": "Starter Live Stream…",
|
||||
"serviceName": "Live Streaming service",
|
||||
"expandedPending": "Livestreaming starter…",
|
||||
"failedToStart": "Kunne ikke starte livestreaming",
|
||||
"getStreamKeyManually": "Vi kunne ikke hente nogle livestreams. Forsøg at hente din livestream-nøgle fra YouTube.",
|
||||
"invalidStreamKey": "Livestream-nøgle er muligvis forkert.",
|
||||
"off": "Livestreaming er stoppet",
|
||||
"offBy": "{{name}} stoppede livestreaming",
|
||||
"on": "Livestreaming",
|
||||
"onBy": "{{name}} startede livestreaming",
|
||||
"pending": "Starter livestream…",
|
||||
"serviceName": "Livestreaming-service",
|
||||
"signedInAs": "Du er pt. logget ind som:",
|
||||
"signIn": "Log ind med Google",
|
||||
"signInCTA": "Log ind eller indtast din live stream nøgle fra YouTube.",
|
||||
"signInCTA": "Log ind eller indtast din livestream-nøgle fra YouTube.",
|
||||
"signOut": "Log ud",
|
||||
"start": "Start en live stream",
|
||||
"start": "Start en livestream",
|
||||
"streamIdHelp": "Hvad er dette?",
|
||||
"unavailableTitle": "Live Streaming er ikke tilgængelig"
|
||||
"unavailableTitle": "Livestreaming er ikke tilgængelig"
|
||||
},
|
||||
"localRecording": {
|
||||
"clientState": {
|
||||
@@ -403,24 +404,24 @@
|
||||
"on": "Slået til",
|
||||
"unknown": "Ukendt"
|
||||
},
|
||||
"dialogTitle": "Optage kontrolknapper",
|
||||
"dialogTitle": "Optagelseskontrolknapper",
|
||||
"duration": "Varighed",
|
||||
"durationNA": "Ikke tilgængelig",
|
||||
"encoding": "Encoding",
|
||||
"label": "Optager",
|
||||
"labelToolTip": "Optager er slået til",
|
||||
"localRecording": "Local Recording",
|
||||
"labelToolTip": "Optagelse er slået til",
|
||||
"localRecording": "Lokal optagelse",
|
||||
"me": "Mig",
|
||||
"messages": {
|
||||
"engaged": "Optager er slået til.",
|
||||
"finished": "Optagelses session {{token}} afsluttet. Send venligst den optagede fil til moderator.",
|
||||
"finishedModerator": "Optagelses session {{token}} afsluttet. Optagelsen er gemt. Bed deltageren om at sende deres optagelser.",
|
||||
"engaged": "Optagelse er slået til.",
|
||||
"finished": "Optagelse {{token}} afsluttet. Send venligst den optagne fil til moderator.",
|
||||
"finishedModerator": "Optagelse {{token}} afsluttet. Optagelsen er gemt. Bed deltageren om at sende deres optagelser.",
|
||||
"notModerator": "Du er ikke moderator. Du kan ikke starte eller stoppe optagelser."
|
||||
},
|
||||
"moderator": "Moderator",
|
||||
"no": "Nej",
|
||||
"participant": "Deltager",
|
||||
"participantStats": "Deltager statistik",
|
||||
"participantStats": "Deltagerstatistik",
|
||||
"sessionToken": "Sessionsnøgle",
|
||||
"start": "Start optagelse",
|
||||
"stop": "Stop optagelse",
|
||||
@@ -433,19 +434,19 @@
|
||||
"connectedOneMember": "{{name}} deltager nu i mødet",
|
||||
"connectedThreePlusMembers": "{{name}} og {{count}} andre deltager nu i mødet",
|
||||
"connectedTwoMembers": "{{first}} og {{second}} deltager nu i mødet",
|
||||
"disconnected": "afbrudt",
|
||||
"focus": "Konference fokus",
|
||||
"disconnected": "Afbrudt",
|
||||
"focus": "Konferencefokus",
|
||||
"focusFail": "{{component}} ikke tilgængelig - forsøg igen om {{ms}} sekunder",
|
||||
"grantedTo": "Moderator rettigheder er givet til {{to}}!",
|
||||
"grantedTo": "Moderatorrettigheder er givet til {{to}}!",
|
||||
"invitedOneMember": "{{name}} er blevet inviteret",
|
||||
"invitedThreePlusMembers": "{{name}} og {{count}} andre er blevet inviteret",
|
||||
"invitedTwoMembers": "{{first}} og {{second}} er blevet inviteret",
|
||||
"kickParticipant": "{{kicked}} blev afbrudt fra mødet af {{kicker}}",
|
||||
"me": "Mig",
|
||||
"moderator": "Moderator rettigheder givet!",
|
||||
"moderator": "Moderatorrettigheder givet",
|
||||
"muted": "Du har startet samtalen med lyden slået fra.",
|
||||
"mutedTitle": "Din lyd er slået fra!",
|
||||
"mutedRemotelyTitle": "Din lyd er slået fra af {{participantDisplayName}}!",
|
||||
"mutedTitle": "Din lyd er slået fra",
|
||||
"mutedRemotelyTitle": "Din lyd er slået fra af {{participantDisplayName}}",
|
||||
"mutedRemotelyDescription": "",
|
||||
"passwordRemovedRemotely": "",
|
||||
"passwordSetRemotely": "",
|
||||
@@ -453,16 +454,16 @@
|
||||
"somebody": "Nogen",
|
||||
"startSilentTitle": "",
|
||||
"startSilentDescription": "",
|
||||
"suboptimalExperienceDescription": "Hmmm... vi er bange for at din oplevelse med {{appName}} ikke vil være optimal. Vi arbejder på at forbedre dette, men indtil da så forsøg venligst at bruge en af følgende <a href='{{recommendedBrowserPageLink}}' target='_blank'>fuldt understøttede internet browsere</a>.",
|
||||
"suboptimalExperienceTitle": "Browser Advarsel",
|
||||
"suboptimalExperienceDescription": "Hmmm... vi er bange for at din oplevelse med {{appName}} ikke vil være optimal. Vi arbejder på at forbedre dette, men indtil da forsøg venligst at bruge en af følgende <a href='{{recommendedBrowserPageLink}}' target='_blank'>fuldt understøttede internet browsere</a>.",
|
||||
"suboptimalExperienceTitle": "Browser-advarsel",
|
||||
"unmute": "",
|
||||
"newDeviceCameraTitle": "Nyt kamerat fundet",
|
||||
"newDeviceAudioTitle": "Ny lyd enhed fundet",
|
||||
"newDeviceCameraTitle": "Nyt kamera fundet",
|
||||
"newDeviceAudioTitle": "Ny lydenhed fundet",
|
||||
"newDeviceAction": "Benyt"
|
||||
},
|
||||
"passwordSetRemotely": "sat af et andet medlem",
|
||||
"passwordSetRemotely": "Sat af et andet medlem",
|
||||
"passwordDigitsOnly": "Op til {{number}} tal",
|
||||
"poweredby": "powered by",
|
||||
"poweredby": "Powered by",
|
||||
"presenceStatus": {
|
||||
"busy": "Optaget",
|
||||
"calling": "Ringer op…",
|
||||
@@ -479,8 +480,8 @@
|
||||
},
|
||||
"profile": {
|
||||
"setDisplayNameLabel": "Vælg navn/alias",
|
||||
"setEmailInput": "Indtast email",
|
||||
"setEmailLabel": "Indtast Gravatar-e-post",
|
||||
"setEmailInput": "Indtast email-adresse",
|
||||
"setEmailLabel": "Indtast Gravatar-email-adresse",
|
||||
"title": "Profil"
|
||||
},
|
||||
"raisedHand": "Ønsker at tale",
|
||||
@@ -488,13 +489,13 @@
|
||||
"authDropboxText": "Upload til Dropbox",
|
||||
"availableSpace": "Tilgængelig plads: {{spaceLeft}} MB (plads til ca. {{duration}} minutters optagelse)",
|
||||
"beta": "BETA",
|
||||
"busy": "Der arbejdes på at frigive optagelsesresourcer. Prøv venligst igen om nogle få minutter.",
|
||||
"busyTitle": "Alle optagere er pt optaget",
|
||||
"busy": "Der arbejdes på at frigive optagelsesressourcer. Prøv venligst igen om nogle få minutter.",
|
||||
"busyTitle": "Alle optagere er pt. optaget",
|
||||
"error": "Fejl under optagelse, forsøg venligst igen om lidt.",
|
||||
"expandedOff": "Optagelse er stoppet½",
|
||||
"expandedOn": "Mødet bliver pt. optaget.",
|
||||
"expandedOff": "Optagelse er stoppet",
|
||||
"expandedOn": "Mødet bliver pt. optaget",
|
||||
"expandedPending": "Optagelse starter…",
|
||||
"failedToStart": "Kunne ikke start optagelse",
|
||||
"failedToStart": "Kunne ikke starte optagelse",
|
||||
"fileSharingdescription": "Del optagelse med deltagere",
|
||||
"live": "LIVE",
|
||||
"loggedIn": "Logget ind som {{userName}}",
|
||||
@@ -504,22 +505,22 @@
|
||||
"onBy": "{{name}} startede optagelsen",
|
||||
"pending": "Gør klar til at optage mødet…",
|
||||
"rec": "REC",
|
||||
"serviceDescription": "Din optagelse bliver gemt af optager servicen",
|
||||
"serviceName": "Optager service",
|
||||
"serviceDescription": "Din optagelse bliver gemt af optagelsesservicen",
|
||||
"serviceName": "Optagelsesservice",
|
||||
"signIn": "Log ind",
|
||||
"signOut": "Log ud",
|
||||
"unavailable": "Optager service {{serviceName}} er ikke tilgængelig. Der arbejdes på at løse problemet, forsøg igen senere.",
|
||||
"unavailable": "Optagelsesservice {{serviceName}} er ikke tilgængelig. Der arbejdes på at løse problemet, forsøg igen senere.",
|
||||
"unavailableTitle": "Optagelse ikke tilgængelig"
|
||||
},
|
||||
"sectionList": {
|
||||
"pullToRefresh": "Træk ned for update"
|
||||
"pullToRefresh": "Træk ned for at opdatere"
|
||||
},
|
||||
"settings": {
|
||||
"calendar": {
|
||||
"about": "{{appName}}s kalenderintegration benyttes til at sikkert tilgå din kalender så den kan læse fremtidige aftaler.",
|
||||
"about": "{{appName}}s kalenderintegration benyttes til sikkert at tilgå din kalender så den kan læse fremtidige aftaler.",
|
||||
"disconnect": "Afbryd",
|
||||
"microsoftSignIn": "Log ind med Microsoft",
|
||||
"signedIn": "Henter kalenderaftaler for {{email}}. Tryk på Afbryd knappen for at afbryde forbindelsen.",
|
||||
"signedIn": "Henter kalenderaftaler for {{email}}. Tryk på Afbryd for at afbryde forbindelsen.",
|
||||
"title": "Kalender"
|
||||
},
|
||||
"devices": "Enheder",
|
||||
@@ -530,28 +531,28 @@
|
||||
"more": "Mere",
|
||||
"name": "Navn",
|
||||
"noDevice": "Ingen",
|
||||
"selectAudioOutput": "Lyd udgang",
|
||||
"selectAudioOutput": "Lydudgang",
|
||||
"selectCamera": "Kamera",
|
||||
"selectMic": "Mikrofon",
|
||||
"startAudioMuted": "Alle starter med lyden slået fra",
|
||||
"startVideoMuted": "Alle starter skjult",
|
||||
"startVideoMuted": "Alle starter med video slået fra",
|
||||
"title": "Indstillinger"
|
||||
},
|
||||
"settingsView": {
|
||||
"advanced": "Avanceret",
|
||||
"alertOk": "OK",
|
||||
"alertTitle": "Advarsel",
|
||||
"alertURLText": "Den indtastede server url er ikke korrekt",
|
||||
"alertURLText": "Den indtastede server-URL er ikke korrekt",
|
||||
"buildInfoSection": "Build Information",
|
||||
"conferenceSection": "Konference",
|
||||
"disableCallIntegration": "Slå indbygget opkalds integration fra",
|
||||
"disableCallIntegration": "Slå indbygget opkaldsintegration fra",
|
||||
"disableP2P": "Slå Peer-To-Peer fra",
|
||||
"displayName": "Navn/alias",
|
||||
"email": "Email",
|
||||
"header": "Indstillinger",
|
||||
"profileSection": "Profil",
|
||||
"serverURL": "Server URL",
|
||||
"showAdvanced": "Show advanced settings",
|
||||
"serverURL": "Server-URL",
|
||||
"showAdvanced": "Vis avancerede indstillinger",
|
||||
"startWithAudioMuted": "Start med lyden slået fra",
|
||||
"startWithVideoMuted": "Start med video slået fra",
|
||||
"version": "Version"
|
||||
@@ -560,18 +561,18 @@
|
||||
"dialInfoText": "\n\n=====\n\nØnsker du at ringe ind fra din telefon?\n\n{{defaultDialInNumber}}Klik på dette link for at se telefonnummeret for dette møde\n{{dialInfoPageUrl}}",
|
||||
"mainText": "Klik på det følgende link for at deltage i mødet:\n{{roomUrl}}"
|
||||
},
|
||||
"speaker": "Højtaler",
|
||||
"speaker": "Deltager",
|
||||
"speakerStats": {
|
||||
"hours": "{{count}}t",
|
||||
"minutes": "{{count}}m",
|
||||
"name": "Navn",
|
||||
"seconds": "{{count}}s",
|
||||
"speakerStats": "Højtaler info",
|
||||
"speakerTime": "Højtaler tid"
|
||||
"speakerStats": "Deltagerstatistik",
|
||||
"speakerTime": "Taletid"
|
||||
},
|
||||
"startupoverlay": {
|
||||
"policyText": " ",
|
||||
"title": "{{app}} har brug for adgang til din mikrofon og kamera."
|
||||
"title": "{{app}} har brug for tilladelse til din mikrofon og dit kamera."
|
||||
},
|
||||
"suspendedoverlay": {
|
||||
"rejoinKeyTitle": "Forbind igen",
|
||||
@@ -582,157 +583,157 @@
|
||||
"accessibilityLabel": {
|
||||
"audioOnly": "Tænd/sluk lyd",
|
||||
"audioRoute": "Vælg lydenhed",
|
||||
"callQuality": "Indstil møde kvalitet",
|
||||
"cc": "Slå undertekst fra/til",
|
||||
"callQuality": "Indstil opkaldskvalitet",
|
||||
"cc": "Slå undertekster fra/til",
|
||||
"chat": "Slå chat fra/til",
|
||||
"document": "Slå delte dokumenter fra/til",
|
||||
"download": "Hent vores apps",
|
||||
"feedback": "Giv tilbagemelding",
|
||||
"feedback": "Giv feedback",
|
||||
"fullScreen": "Slå fuld skærm fra/til",
|
||||
"hangup": "Forlad mødet",
|
||||
"help": "Hjælp",
|
||||
"invite": "Inviter deltagere",
|
||||
"kick": "Afbryd deltager",
|
||||
"localRecording": "Slå optagekontroller fra/til",
|
||||
"invite": "Invitér deltagere",
|
||||
"kick": "Smid deltager ud",
|
||||
"localRecording": "Slå optagelseskontrol fra/til",
|
||||
"lockRoom": "Slå mødeadgangskode fra/til",
|
||||
"moreActions": "Slå \"Flere indstillinger\" menu fra/til",
|
||||
"moreActionsMenu": "Flere indtstillinger",
|
||||
"moreOptions": "Show more options",
|
||||
"moreActionsMenu": "Flere indstillinger",
|
||||
"moreOptions": "Vis flere indstillinger",
|
||||
"mute": "Slå lyd til/fra",
|
||||
"muteEveryone": "Mute everyone",
|
||||
"muteEveryone": "Slå lyd fra for alle",
|
||||
"pip": "Slå Billede-i-billede fra/til",
|
||||
"privateMessage": "Send privat besked",
|
||||
"privateMessage": "Send privatbesked",
|
||||
"profile": "Redigér profil",
|
||||
"raiseHand": "Slå løft hånden fra/til",
|
||||
"recording": "Slå optagelse til/fra",
|
||||
"remoteMute": "Slå lyd fra for deltager",
|
||||
"Settings": "Slå indstillinger fra/til",
|
||||
"sharedvideo": "Slå YouTube video deling fra/til",
|
||||
"sharedvideo": "Slå YouTube-videodeling fra/til",
|
||||
"shareRoom": "Invitér nogen",
|
||||
"shareYourScreen": "Slå skærmdeling fra/til",
|
||||
"shortcuts": "Slå genveje fra/til",
|
||||
"show": "Vis",
|
||||
"speakerStats": "Slå højtaler info fra/til",
|
||||
"tileView": "Slå tileview fra/til",
|
||||
"speakerStats": "Slå højtalerinfo fra/til",
|
||||
"tileView": "Slå gittervisning fra/til",
|
||||
"toggleCamera": "Slå kamera fra/til",
|
||||
"videomute": "Slå video fra/til",
|
||||
"videoblur": "Slå baggrundsløring fra/til"
|
||||
"videoblur": "Slå baggrundssløring fra/til"
|
||||
},
|
||||
"addPeople": "Tilføj deltager til mødet",
|
||||
"audioOnlyOff": "Slå kun-lyd møder fra",
|
||||
"audioOnlyOn": "Slå kun-lyd møder til",
|
||||
"audioRoute": "Vælg lydenhed",
|
||||
"authenticate": "Log ind",
|
||||
"callQuality": "Indstil møde kvalitet",
|
||||
"chat": "Åben / Luk chat",
|
||||
"callQuality": "Indstil opkaldskvalitet",
|
||||
"chat": "Åbn/luk chat",
|
||||
"closeChat": "Luk chat",
|
||||
"documentClose": "Luk delt dokument",
|
||||
"documentOpen": "Åben delt dokument",
|
||||
"download": "Hent vores apps",
|
||||
"enterFullScreen": "Vis fuld skærm",
|
||||
"enterTileView": "Tileview",
|
||||
"enterTileView": "Gittervisning",
|
||||
"exitFullScreen": "Luk fuld skærm",
|
||||
"exitTileView": "Luk Tileview",
|
||||
"feedback": "Giv tilbagemeldign",
|
||||
"exitTileView": "Luk gittervisning",
|
||||
"feedback": "Giv feedback",
|
||||
"hangup": "Forlad",
|
||||
"help": "Hjælp",
|
||||
"invite": "Invitér deltagere",
|
||||
"login": "Log ind",
|
||||
"logout": "Log ud",
|
||||
"lowerYourHand": "Ta hånden ned",
|
||||
"lowerYourHand": "Tag hånden ned",
|
||||
"moreActions": "Flere handlinger",
|
||||
"moreOptions": "Flere indstillinger",
|
||||
"mute": "Slå lyd fra / Slå lyd til",
|
||||
"mute": "Slå lyd fra/til",
|
||||
"muteEveryone": "Slå lyd fra for alle",
|
||||
"noAudioSignalTitle": "Der kommer ingen input fra din mikrofon!",
|
||||
"noAudioSignalTitle": "Der kommer ingen input fra din mikrofon",
|
||||
"noAudioSignalDesc": "Hvis du ikke med vilje har slået den fra under systemindstillinger eller hardware, kan du overveje at skifte enhed.",
|
||||
"noAudioSignalDescSuggestion": "Hvis du ikke med vilje har slået den fra under systemindstillinger eller hardware, skal du overveje at skifte til den foreslåede enhed.",
|
||||
"noAudioSignalDialInDesc": "Du kan også ringe op ved hjælp af:",
|
||||
"noAudioSignalDialInLinkDesc": "Opkaldsnumre",
|
||||
"noisyAudioInputTitle": "Det lyder som om din mikrofon laver støj!",
|
||||
"noisyAudioInputTitle": "Det lyder som om din mikrofon laver støj",
|
||||
"noisyAudioInputDesc": "Det lyder som om din mikrofon laver støj. Overvej venligst at slå lyden fra eller skifte enhed.",
|
||||
"openChat": "Åben chat",
|
||||
"openChat": "Åbn chat",
|
||||
"pip": "Benyt Billede-i-billede",
|
||||
"privateMessage": "Send private message",
|
||||
"privateMessage": "Send privat besked",
|
||||
"profile": "Rediger profil",
|
||||
"raiseHand": "Ræk hånden op / Tag hånden ned",
|
||||
"raiseYourHand": "Ræk hånden op",
|
||||
"Settings": "Indstillinger",
|
||||
"sharedvideo": "Del en Youtube video",
|
||||
"sharedvideo": "Del en YouTube-video",
|
||||
"shareRoom": "Invitér deltagere",
|
||||
"shortcuts": "Vis genveje",
|
||||
"speakerStats": "Højtaler info",
|
||||
"speakerStats": "Deltagerstatistik",
|
||||
"startScreenSharing": "Start skærmdeling",
|
||||
"startSubtitles": "Vis undertekster",
|
||||
"stopScreenSharing": "Stop skærmdeling",
|
||||
"stopSubtitles": "Skjul undertekster",
|
||||
"stopSharedVideo": "Stop YouTube video",
|
||||
"stopSharedVideo": "Stop YouTube-video",
|
||||
"talkWhileMutedPopup": "Forsøger du at sige noget? Din lyd er slået fra.",
|
||||
"tileViewToggle": "Slå tileview fra/til",
|
||||
"tileViewToggle": "Slå gittervisning fra/til",
|
||||
"toggleCamera": "Slå kamera fra/til",
|
||||
"videomute": "Start / Stop kamera",
|
||||
"videomute": "Start/stop kamera",
|
||||
"startvideoblur": "Slå baggrundssløring til",
|
||||
"stopvideoblur": "Slå baggrundssløring fra"
|
||||
},
|
||||
"transcribing": {
|
||||
"ccButtonTooltip": "Start / Stop undertekster",
|
||||
"error": "Transkription mislykkedes. Prøv venligst igen.",
|
||||
"expandLabel": "Transkription er i øjeblikket tændt",
|
||||
"failedToStart": "Transkription kunne ikke starte",
|
||||
"ccButtonTooltip": "Start/stop undertekster",
|
||||
"error": "Transskription mislykkedes. Prøv venligst igen.",
|
||||
"expandLabel": "Transskription er slået til",
|
||||
"failedToStart": "Transskription kunne ikke starte",
|
||||
"labelToolTip": "Mødet transkriberes",
|
||||
"off": "Transkription stoppet",
|
||||
"pending": "Forbereder sig til at transkribere mødet …",
|
||||
"pending": "Forbereder sig til at transskribere mødet…",
|
||||
"start": "Begynd at vise undertekster",
|
||||
"stop": "Stop med at få vist undertekster",
|
||||
"stop": "Stop med at vise undertekster",
|
||||
"tr": "TR"
|
||||
},
|
||||
"userMedia": {
|
||||
"androidGrantPermissions": "Vælg <b><i> Tillad</i> </b>, når din browser beder om tilladelser.",
|
||||
"chromeGrantPermissions": "Vælg <b><i> Tillad</i> </b>, når din browser beder om tilladelser.",
|
||||
"edgeGrantPermissions": "Vælg <b><i>Ja</i> </b>, når din browser beder om tilladelser.",
|
||||
"electronGrantPermissions": "Giv tilladelse til at bruge dit kamera og din mikrofon",
|
||||
"firefoxGrantPermissions": "Vælg <b> <i> Del valgt enhed </i> </b>, når din browser beder om tilladelser.",
|
||||
"iexplorerGrantPermissions": "Vælg <b><i>OK</i> </b>, når din browser beder om tilladelser.",
|
||||
"nwjsGrantPermissions": "Giv tilladelse til at bruge dit kamera og din mikrofon",
|
||||
"operaGrantPermissions": "Vælg <b><i> Tillad</i> </b>, når din browser beder om tilladelser.",
|
||||
"react-nativeGrantPermissions": "Vælg <b><i> Tillad</i> </b>, når din browser beder om tilladelser.",
|
||||
"safariGrantPermissions": "Vælg <b><i>OK</i> </b>, når din browser beder om tilladelser."
|
||||
"androidGrantPermissions": "Vælg <b><i> Tillad</i> </b>, når din browser beder om tilladelser",
|
||||
"chromeGrantPermissions": "Vælg <b><i> Tillad</i> </b>, når din browser beder om tilladelser",
|
||||
"edgeGrantPermissions": "Vælg <b><i>Ja</i> </b>, når din browser beder om tilladelser",
|
||||
"electronGrantPermissions": "Giv tilladelse til at dit kamera og din mikrofon benyttes",
|
||||
"firefoxGrantPermissions": "Vælg <b> <i> Del valgt enhed </i> </b>, når din browser beder om tilladelser",
|
||||
"iexplorerGrantPermissions": "Vælg <b><i>OK</i> </b>, når din browser beder om tilladelser",
|
||||
"nwjsGrantPermissions": "Giv tilladelse til at dit kamera og din mikrofon benyttes",
|
||||
"operaGrantPermissions": "Vælg <b><i> Tillad</i> </b>, når din browser beder om tilladelser",
|
||||
"react-nativeGrantPermissions": "Vælg <b><i> Tillad</i> </b>, når din browser beder om tilladelser",
|
||||
"safariGrantPermissions": "Vælg <b><i>OK</i> </b>, når din browser beder om tilladelser"
|
||||
},
|
||||
"videoSIPGW": {
|
||||
"busy": "Vi arbejder på at frigøre ressourcer. Prøv igen om et par minutter.",
|
||||
"busy": "Vi arbejder på at frigøre ressourcer. Prøv igen om et par minutter",
|
||||
"busyTitle": "Mødeservice er i øjeblikket optaget",
|
||||
"errorAlreadyInvited": "{{displayName}} allerede inviteret",
|
||||
"errorInvite": "Mødet er endnu ikke etableret. Prøv igen senere.",
|
||||
"errorInviteFailed": "Vi arbejder på at løse problemet. Prøv igen senere.",
|
||||
"errorInviteFailedTitle": "Inviterer {{displayName}} mislykkedes",
|
||||
"errorInviteTitle": "Fejl ved indbydelse til møde",
|
||||
"errorInvite": "Mødet er endnu ikke etableret. Prøv igen senere",
|
||||
"errorInviteFailed": "Vi arbejder på at løse problemet. Prøv igen senere",
|
||||
"errorInviteFailedTitle": "Invitationen af {{displayName}} mislykkedes",
|
||||
"errorInviteTitle": "Fejl ved invitation til møde",
|
||||
"pending": "{{displayName}} er blevet inviteret"
|
||||
},
|
||||
"videoStatus": {
|
||||
"audioOnly": "AUD",
|
||||
"audioOnlyExpanded": "Du er i kun-lyd tilstand. Denne tilstand sparrer båndbredde, men du kan ikke se andres video.",
|
||||
"audioOnlyExpanded": "Du er i kun-lyd-tilstand. Denne tilstand sparer båndbredde, men du kan ikke se andres video",
|
||||
"callQuality": "Opkaldskvalitet",
|
||||
"hd": "HD",
|
||||
"hdTooltip": "Viser HD video",
|
||||
"hdTooltip": "Viser høj opløsning",
|
||||
"highDefinition": "Høj opløsning",
|
||||
"labelTooiltipNoVideo": "Ingen video",
|
||||
"labelTooltipAudioOnly": "Tilstand kun-lyd er slået til",
|
||||
"labelTooltipAudioOnly": "Kun-lyd er slået til",
|
||||
"ld": "LD",
|
||||
"ldTooltip": "Viser lav opløsnings video",
|
||||
"ldTooltip": "Viser lav opløsning",
|
||||
"lowDefinition": "Lav opløsning",
|
||||
"onlyAudioAvailable": "Kun-lyd er tilgængelig",
|
||||
"onlyAudioSupported": "Vi understøtter kun-lyd i denne browser.",
|
||||
"sd": "SD",
|
||||
"sdTooltip": "Vider normal opløsnings video",
|
||||
"sdTooltip": "Viser standard opløsning",
|
||||
"standardDefinition": "Standard opløsning"
|
||||
},
|
||||
"videothumbnail": {
|
||||
"domute": "Slå lyd fra",
|
||||
"domuteOthers": "Slå lyd fra for alle andre",
|
||||
"flip": "Flip",
|
||||
"kick": "Afbryd",
|
||||
"kick": "Smid ud",
|
||||
"moderator": "Moderator",
|
||||
"mute": "Medlemmets lyd er slået fra",
|
||||
"mute": "Deltagerens lyd er slået fra",
|
||||
"muted": "Lyd slået fra",
|
||||
"remoteControl": "Fjernbetjening",
|
||||
"show": "",
|
||||
@@ -741,9 +742,9 @@
|
||||
"welcomepage": {
|
||||
"accessibilityLabel": {
|
||||
"join": "Tryk for at deltage",
|
||||
"roomname": "Skriv møde navn"
|
||||
"roomname": "Skriv mødenavn"
|
||||
},
|
||||
"appDescription": "Chat med hele dit team eller inviter alle du kender. {{app}} er en fuldt krypteret, 100% open source videokonference løsning som du kan bruge hele dagen, hver dag, gratis - uden at du behøver en konto. ",
|
||||
"appDescription": "Chat med hele dit team eller invitér alle du kender. {{app}} er en fuldt krypteret, 100% open source videokonference løsning som du kan bruge hele dagen, hver dag, gratis - uden at du behøver en konto. ",
|
||||
"audioVideoSwitch": {
|
||||
"audio": "Lyd",
|
||||
"video": "Video"
|
||||
@@ -755,13 +756,13 @@
|
||||
"go": "Start",
|
||||
"join": "Deltag",
|
||||
"info": "Info",
|
||||
"privacy": "Privatliv",
|
||||
"privacy": "Privacy",
|
||||
"recentList": "Seneste",
|
||||
"recentListDelete": "Slet",
|
||||
"recentListEmpty": "Din seneste liste er i øjeblikket tom. Chat med dit team, og du finder alle dine seneste møder her.",
|
||||
"reducedUIText": "",
|
||||
"roomname": "Indtast mødenavn",
|
||||
"roomnameHint": "Indtast mødenavnet, du vil deltage i.",
|
||||
"roomnameHint": "Indtast mødenavnet på det møde, du vil deltage i.",
|
||||
"sendFeedback": "Giv tilbagemelding",
|
||||
"betingelser": "Betingelser",
|
||||
"title": "Skærmbesøg & videomøder"
|
||||
|
||||
@@ -239,12 +239,19 @@
|
||||
"muteEveryoneElseTitle": "Alle außer {{whom}} stummschalten?",
|
||||
"muteEveryoneDialog": "Wollen Sie wirklich alle stummschalten? Sie können deren Stummschaltung nicht mehr beenden, aber sie können ihre Stummschaltung jederzeit selbst beenden.",
|
||||
"muteEveryoneTitle": "Alle stummschalten?",
|
||||
"muteEveryoneElsesVideoDialog": "Sobald die Kamera deaktiviert ist, können Sie sie nicht wieder aktivieren, die Teilnehmer können dies aber jederzeit wieder ändern.",
|
||||
"muteEveryoneElsesVideoTitle": "Die Kamera von allen außer {{whom}} ausschalten?",
|
||||
"muteEveryonesVideoDialog": "Sind Sie sicher, dass Sie die Kamera von allen Teilnehmern deaktivieren möchten? Sie können sie nicht wieder aktivieren, die Teilnehmer können dies aber jederzeit wieder ändern.",
|
||||
"muteEveryonesVideoTitle": "Die Kamera von allen anderen ausschalten?",
|
||||
"muteEveryoneSelf": "sich selbst",
|
||||
"muteEveryoneStartMuted": "Alle beginnen von jetzt an stummgeschaltet",
|
||||
"muteParticipantBody": "Sie können die Stummschaltung anderer Personen nicht aufheben, aber eine Person kann ihre eigene Stummschaltung jederzeit beenden.",
|
||||
"muteParticipantButton": "Stummschalten",
|
||||
"muteParticipantDialog": "Wollen Sie diese Person wirklich stummschalten? Sie können die Stummschaltung nicht wieder aufheben, die Person kann dies aber jederzeit selbst tun.",
|
||||
"muteParticipantTitle": "Person stummschalten?",
|
||||
"muteParticipantsVideoButton": "Kamera ausschalten",
|
||||
"muteParticipantsVideoTitle": "Die Kamera von dieser Person ausschalten?",
|
||||
"muteParticipantsVideoBody": "Sie können die Kamera nicht wieder aktivieren, die Teilnehmer können dies aber jederzeit wieder ändern.",
|
||||
"Ok": "OK",
|
||||
"passwordLabel": "Dieses Meeting wurde gesichert. Bitte geben Sie das $t(lockRoomPasswordUppercase) ein, um dem Meeting beizutreten.",
|
||||
"passwordNotSupported": "Das Festlegen eines Konferenzpassworts wird nicht unterstützt.",
|
||||
@@ -484,6 +491,8 @@
|
||||
"mutedTitle": "Stummschaltung aktiv!",
|
||||
"mutedRemotelyTitle": "Sie wurden von {{participantDisplayName}} stummgeschaltet!",
|
||||
"mutedRemotelyDescription": "Sie können jederzeit die Stummschaltung aufheben, wenn Sie bereit sind zu sprechen. Wenn Sie fertig sind, können Sie sich wieder stummschalten, um Geräusche vom Meeting fernzuhalten.",
|
||||
"videoMutedRemotelyTitle": "Ihre Kamera wurde von {{participantDisplayName}} ausgeschaltet!",
|
||||
"videoMutedRemotelyDescription": "Sie können sie jederzeit wieder einschalten.",
|
||||
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person entfernt",
|
||||
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person gesetzt",
|
||||
"raisedHand": "{{name}} möchte sprechen.",
|
||||
@@ -682,6 +691,7 @@
|
||||
},
|
||||
"startupoverlay": {
|
||||
"policyText": " ",
|
||||
"genericTitle": "Die Konferenz muss Ihr Mikrofon und Ihre Kamera verwenden.",
|
||||
"title": "{{app}} benötigt Kamera und Mikrofon."
|
||||
},
|
||||
"suspendedoverlay": {
|
||||
@@ -714,12 +724,16 @@
|
||||
"moreOptions": "Menü „Weitere Optionen“",
|
||||
"mute": "„Audio stummschalten“ ein-/ausschalten",
|
||||
"muteEveryone": "Alle stummschalten",
|
||||
"muteEveryoneElse": "Alle anderen stummschalten",
|
||||
"muteEveryonesVideo": "Alle Kameras ausschalten",
|
||||
"muteEveryoneElsesVideo": "Alle anderen Kameras ausschalten",
|
||||
"pip": "Bild-in-Bild-Modus ein-/ausschalten",
|
||||
"privateMessage": "Private Nachricht senden",
|
||||
"profile": "Profil bearbeiten",
|
||||
"raiseHand": "„Melden“ ein-/ausschalten",
|
||||
"recording": "Aufzeichnung ein-/ausschalten",
|
||||
"remoteMute": "Personen stummschalten",
|
||||
"remoteVideoMute": "Kamera von dieser Person ausschalten",
|
||||
"security": "Sicherheitsoptionen",
|
||||
"Settings": "Einstellungen ein-/ausschalten",
|
||||
"sharedvideo": "YouTube-Videofreigabe ein-/ausschalten",
|
||||
@@ -764,6 +778,7 @@
|
||||
"moreOptions": "Weitere Optionen",
|
||||
"mute": "Stummschaltung aktivieren / deaktivieren",
|
||||
"muteEveryone": "Alle stummschalten",
|
||||
"muteEveryonesVideo": "Alle Kameras ausschalten",
|
||||
"noAudioSignalTitle": "Es kommt kein Input von Ihrem Mikrofon!",
|
||||
"noAudioSignalDesc": "Wenn Sie das Gerät nicht absichtlich über die Systemeinstellungen oder die Hardware stumm geschaltet haben, sollten Sie einen Wechsel des Geräts in Erwägung ziehen.",
|
||||
"noAudioSignalDescSuggestion": "Wenn Sie das Gerät nicht absichtlich über die Systemeinstellungen oder die Hardware stummgeschaltet haben, sollten Sie einen Wechsel auf das vorgeschlagene Gerät in Erwägung ziehen.",
|
||||
@@ -849,13 +864,16 @@
|
||||
},
|
||||
"videothumbnail": {
|
||||
"domute": "Stummschalten",
|
||||
"domuteVideo": "Kamera ausschalten",
|
||||
"domuteOthers": "Alle anderen stummschalten",
|
||||
"domuteVideoOfOthers": "Alle anderen Kameras auschalten",
|
||||
"flip": "Spiegeln",
|
||||
"grantModerator": "Moderationsrechte vergeben",
|
||||
"kick": "Hinauswerfen",
|
||||
"moderator": "Moderation",
|
||||
"mute": "Person ist stumm geschaltet",
|
||||
"muted": "Stummgeschaltet",
|
||||
"videoMuted": "Kamera ausgeschaltet",
|
||||
"remoteControl": "Fernsteuerung",
|
||||
"show": "Im Vordergrund anzeigen",
|
||||
"videomute": "Person hat die Kamera angehalten"
|
||||
|
||||
@@ -105,8 +105,8 @@
|
||||
"bitrate": "Débit :",
|
||||
"bridgeCount": "Nombre de serveurs :",
|
||||
"codecs": "Codecs (A/V) :",
|
||||
"e2e_rtt": "E2E RTT :",
|
||||
"connectedTo": "Connecté à :",
|
||||
"e2e_rtt": "E2E RTT :",
|
||||
"framerate": "Images par seconde :",
|
||||
"less": "Cacher les détails",
|
||||
"localaddress": "Adresse locale :",
|
||||
@@ -251,7 +251,7 @@
|
||||
"muteParticipantDialog": "Êtes-vous sûr(e) de vouloir couper le micro de ce participant ? Seul le participant pourra ensuite réactiver son micro à tout moment.",
|
||||
"muteParticipantTitle": "Couper le micro de ce participant ?",
|
||||
"Ok": "Ok",
|
||||
"passwordLabel": "La réunion a été verrouillée par un·e participant·e. Veuillez entrer le $t(lockRoomPassword) pour la rejoindre.",
|
||||
"passwordLabel": "La réunion a été verrouillée par un(e) participant(e). Veuillez entrer le $t(lockRoomPassword) pour la rejoindre.",
|
||||
"passwordNotSupported": "La définition d'un $t(lockRoomPassword) de réunion n'est pas prise en charge.",
|
||||
"passwordNotSupportedTitle": "L'ajout d'un $t(lockRoomPassword) n'est pas supporté",
|
||||
"passwordRequired": "$t(lockRoomPasswordUppercase) requis",
|
||||
@@ -396,16 +396,16 @@
|
||||
"focusRemote": "Épingler la vidéo de quelqu'un d'autre",
|
||||
"fullScreen": "Activer / Désactiver le mode plein écran",
|
||||
"keyboardShortcuts": "Raccourcis clavier",
|
||||
"localRecording": "Afficher ou masquer les commandes de l'enregistrement local",
|
||||
"mute": "Activer ou désactiver le microphone",
|
||||
"pushToTalk": "Appuyer pour parler",
|
||||
"raiseHand": "Lever ou baisser la main",
|
||||
"localRecording": "Afficher / Masquer les commandes de l'enregistrement local",
|
||||
"mute": "Activer / Désactiver le microphone",
|
||||
"pushToTalk": "Maintenir la touche pour parler",
|
||||
"raiseHand": "Lever / Baisser la main",
|
||||
"showSpeakerStats": "Afficher les statistiques de l'interlocuteur",
|
||||
"toggleChat": "Ouvrir ou fermer le panneau de conversation",
|
||||
"toggleFilmstrip": "Afficher ou masquer les vignettes vidéos",
|
||||
"toggleChat": "Ouvrir / Fermer le panneau de conversation",
|
||||
"toggleFilmstrip": "Afficher / Masquer les vignettes vidéos",
|
||||
"toggleScreensharing": "Basculer entre la caméra et le partage d'écran",
|
||||
"toggleShortcuts": "Afficher ou masquer les raccourcis clavier",
|
||||
"videoMute": "Démarrer ou arrêter votre caméra",
|
||||
"toggleShortcuts": "Afficher / Masquer les raccourcis clavier",
|
||||
"videoMute": "Démarrer / Arrêter votre caméra",
|
||||
"videoQuality": "Accorder la qualité des appels"
|
||||
},
|
||||
"liveStreaming": {
|
||||
@@ -423,15 +423,15 @@
|
||||
"expandedOff": "La diffusion en direct a été arrêtée",
|
||||
"expandedOn": "La conférence est en cours de diffusion sur YouTube.",
|
||||
"expandedPending": "La diffusion en direct a commencé ...",
|
||||
"failedToStart": "La diffusion n'as pas réussi à démarrer",
|
||||
"getStreamKeyManually": "Nous n'avons pu récupérer aucun flux en direct. Essayez d'obtenir votre clé de diffusion en direct sur YouTube.",
|
||||
"failedToStart": "La diffusion n'a pas réussi à démarrer",
|
||||
"getStreamKeyManually": "Nous n'avons pas réussi à récupérer un flux de direct. Essayez d'obtenir votre clé de diffusion en direct sur YouTube.",
|
||||
"invalidStreamKey": "La clé de diffusion en direct n'est peut-être pas correcte.",
|
||||
"limitNotificationDescriptionWeb": "En raison de la forte demande, votre diffusion sera limitée à {{limit}} min. Pour un streaming illimité, essayez <a href={{url}} rel='noopener noreferrer' target='_blank'> {{app}} </a>.",
|
||||
"limitNotificationDescriptionWeb": "En raison d'une forte demande, votre diffusion sera limitée à {{limit}} min. Pour un streaming illimité, essayez <a href={{url}} rel='noopener noreferrer' target='_blank'> {{app}} </a>.",
|
||||
"limitNotificationDescriptionNative": "Votre diffusion sera limitée à {{limit}} min. Pour un streaming illimité, essayez {{app}}.",
|
||||
"off": "Le Streaming a été arrêté",
|
||||
"offBy": "{{name}} a arrêté la diffusion en continu",
|
||||
"off": "La diffusion en direct (streaming) a été arrêté",
|
||||
"offBy": "{{name}} a arrêté la diffusion en direct",
|
||||
"on": "En direct",
|
||||
"onBy": "{{name}} a démarré la diffusion en continu",
|
||||
"onBy": "{{name}} a démarré la diffusion en direct",
|
||||
"pending": "Lancement du direct ...",
|
||||
"serviceName": "Service de diffusion en direct",
|
||||
"signedInAs": "Vous êtes connecté en tant que :",
|
||||
@@ -461,12 +461,12 @@
|
||||
"messages": {
|
||||
"engaged": "L'enregistrement local a démarré.",
|
||||
"finished": "L'enregistrement de la session {{token}} s'est terminé. Merci d'envoyer le fichier au modérateur.",
|
||||
"finishedModerator": "L'enregistrement de la session {{token}} s'est terminé. La piste a bien été sauvegardée. Merci de demander aux autres participants de soumettre leurs enregistrements.",
|
||||
"finishedModerator": "L'enregistrement de la session {{token}} s'est terminé. L'enregistrement a bien été sauvegardée. Merci de demander aux autres participants de soumettre leurs enregistrements.",
|
||||
"notModerator": "Vous n'êtes pas le modérateur. Vous ne pouvez pas démarrer ou arrêter un enregistrement local."
|
||||
},
|
||||
"moderator": "Modérateur",
|
||||
"moderator": "Modérateur ",
|
||||
"no": "Non",
|
||||
"participant": "Participant·e",
|
||||
"participant": "Participant(e)",
|
||||
"participantStats": "Statistiques du participant",
|
||||
"sessionToken": "Token de la session",
|
||||
"start": "Démarrer l'enregistrement",
|
||||
@@ -484,7 +484,7 @@
|
||||
"focus": "Focus de conférence",
|
||||
"focusFail": "{{component}} n'est pas disponible - réessayez dans {{ms}} sec",
|
||||
"grantedTo": "Droits modérateur accordés à {{to}} !",
|
||||
"invitedOneMember": "{{name}} a été invité·e",
|
||||
"invitedOneMember": "{{name}} a été invité(e)",
|
||||
"invitedThreePlusMembers": "{{name}} et {{count}} autres ont été invités",
|
||||
"invitedTwoMembers": "{{first}} et {{second}} ont été invités",
|
||||
"kickParticipant": "{{kicked}} a été expulsé par {{kicker}}",
|
||||
@@ -516,7 +516,8 @@
|
||||
"poweredby": "produit par",
|
||||
"prejoin": {
|
||||
"audioAndVideoError": "Erreur audio et video:",
|
||||
"audioOnlyError": "Erreur audio:",
|
||||
"audioDeviceProblem": "Il y a un problème avec votre périphérique audio",
|
||||
"audioOnlyError": "Erreur audio :",
|
||||
"audioTrackError": "N'a pas pu créer la piste audio.",
|
||||
"calling": "Appel",
|
||||
"callMe": "Appelez-moi",
|
||||
@@ -602,7 +603,7 @@
|
||||
"expandedOff": "L'enregistrement a été arrêté",
|
||||
"expandedOn": "Cette conférence est actuellement en cours d'enregistrement.",
|
||||
"expandedPending": "Démarrage de l'enregistrement ...",
|
||||
"failedToStart": "L'enregistrement n'as pas réussi à démarrer",
|
||||
"failedToStart": "L'enregistrement n'a pas réussi à démarrer",
|
||||
"fileSharingdescription": "Partager l'enregistrement avec les participants de la réunion",
|
||||
"limitNotificationDescriptionWeb": "En raison d'une forte demande, votre enregistrement sera limité à {{limit}} min. Pour des enregistrements illimités, essayez <a href={{url}} rel='noopener noreferrer' target='_blank'> {{app}} </a>.",
|
||||
"limitNotificationDescriptionNative": "En raison d'une forte demande, votre enregistrement sera limité à {{limit}} min. Pour des enregistrements illimités, essayez <3> {{app}} </3>.",
|
||||
@@ -702,48 +703,48 @@
|
||||
},
|
||||
"toolbar": {
|
||||
"accessibilityLabel": {
|
||||
"audioOnly": "Activer/désactiver le mode voix uniquement",
|
||||
"audioOnly": "Activer / Désactiver le mode voix uniquement",
|
||||
"audioRoute": "Sélectionner la source audio",
|
||||
"callQuality": "Ajuster la qualité vidéo",
|
||||
"cc": "Activer/désactiver les sous-titres",
|
||||
"chat": "Afficher/masquer la discussion instantanée",
|
||||
"document": "Activer/désactiver le document partagé",
|
||||
"cc": "Activer / Désactiver les sous-titres",
|
||||
"chat": "Afficher / Masquer la discussion instantanée",
|
||||
"document": "Activer / Désactiver le document partagé",
|
||||
"download": "Télécharger nos applications",
|
||||
"embedMeeting": "Intégrer la réunion",
|
||||
"feedback": "Laisser des commentaires",
|
||||
"fullScreen": "Activer/désactiver le plein écran",
|
||||
"fullScreen": "Activer / Désactiver le plein écran",
|
||||
"grantModerator": "Nommer modérateur",
|
||||
"hangup": "Quitter la conversation",
|
||||
"help": "Aide",
|
||||
"invite": "Inviter des participants",
|
||||
"kick": "Expulser le participant",
|
||||
"lobbyButton": "Activer / désactiver le mode lobby",
|
||||
"localRecording": "Activer/désactiver les contrôles d'enregistrement local",
|
||||
"lockRoom": "Activer/Désactiver le mot de passe de la réunion",
|
||||
"moreActions": "Activer/désactiver le menu d'actions supplémentaires",
|
||||
"lobbyButton": "Activer / Désactiver le mode salle d'attente",
|
||||
"localRecording": "Activer / Désactiver les contrôles d'enregistrement local",
|
||||
"lockRoom": "Activer / Désactiver le mot de passe de la réunion",
|
||||
"moreActions": "Activer / Désactiver le menu d'actions supplémentaires",
|
||||
"moreActionsMenu": "Menu d'actions supplémentaires",
|
||||
"moreOptions": "Voir plus d'options",
|
||||
"mute": "Activer/désactiver l'audio",
|
||||
"mute": "Activer / Désactiver l'audio",
|
||||
"muteEveryone": "Rendre muet tout le monde",
|
||||
"pip": "Activer/désactiver le mode Picture in Picture",
|
||||
"pip": "Activer / Désactiver le mode Picture in Picture",
|
||||
"privateMessage": "Envoyer un message privé",
|
||||
"profile": "Éditer votre profil",
|
||||
"raiseHand": "Lever/baisser la main",
|
||||
"recording": "Activer/désactiver l'enregistrement",
|
||||
"raiseHand": "Lever / Baisser la main",
|
||||
"recording": "Activer / Désactiver l'enregistrement",
|
||||
"remoteMute": "Désactiver le micro du participant",
|
||||
"security": "Options de sécurité",
|
||||
"Settings": "Afficher/masquer le menu des paramètres",
|
||||
"sharedvideo": "Démarrer/arrêter le partage de vidéo YouTube",
|
||||
"Settings": "Afficher / Masquer le menu des paramètres",
|
||||
"sharedvideo": "Démarrer / Arrêter le partage de vidéo YouTube",
|
||||
"shareRoom": "Inviter quelqu'un",
|
||||
"shareYourScreen": "Activer/désactiver le partage d'écran",
|
||||
"shortcuts": "Afficher/masquer les raccourcis",
|
||||
"shareYourScreen": "Activer / Désactiver le partage d'écran",
|
||||
"shortcuts": "Afficher / Masquer les raccourcis",
|
||||
"show": "Afficher en premier plan",
|
||||
"speakerStats": "Afficher/cacher les statistiques de parole",
|
||||
"tileView": "Activer/désactiver la vue mosaïque",
|
||||
"speakerStats": "Afficher / Cacher les statistiques de parole",
|
||||
"tileView": "Activer / Désactiver la vue mosaïque",
|
||||
"toggleCamera": "Changer de caméra",
|
||||
"toggleFilmstrip": "Basculer de pellicule",
|
||||
"videomute": "Activer/désactiver la vidéo",
|
||||
"videoblur": "Activer/désactiver le flou de la vidéo"
|
||||
"videomute": "Activer / Désactiver la vidéo",
|
||||
"videoblur": "Activer / Désactiver le flou de la vidéo"
|
||||
},
|
||||
"addPeople": "Ajouter des personnes à votre appel",
|
||||
"audioOnlyOff": "Désactiver le mode bande passante réduite",
|
||||
@@ -766,8 +767,8 @@
|
||||
"hangup": "Quitter",
|
||||
"help": "Aide",
|
||||
"invite": "Inviter des participants",
|
||||
"lobbyButtonDisable": "Désactiver le contrôle des participant·e·s",
|
||||
"lobbyButtonEnable": "Activer le contrôle des participant·e·s",
|
||||
"lobbyButtonDisable": "Désactiver le mode salle d'attente / contrôle des participant(e)s",
|
||||
"lobbyButtonEnable": "Activer le mode salle d'attente / contrôle des participant(e)s",
|
||||
"login": "Connexion",
|
||||
"logout": "Déconnexion",
|
||||
"lowerYourHand": "Baisser la main",
|
||||
@@ -800,22 +801,22 @@
|
||||
"stopSubtitles": "Désactiver les sous-titres",
|
||||
"stopSharedVideo": "Arrêter la vidéo YouTube",
|
||||
"talkWhileMutedPopup": "Vous voulez parler ? Votre micro est coupé.",
|
||||
"tileViewToggle": "Activer/désactiver la vue mosaïque",
|
||||
"tileViewToggle": "Activer / Désactiver la vue mosaïque",
|
||||
"toggleCamera": "Changer de caméra",
|
||||
"videomute": "Démarrer / Arrêter la caméra",
|
||||
"startvideoblur": "Flouter mon arrière plan",
|
||||
"stopvideoblur": "Désactiver le flou d'arrière-plan"
|
||||
},
|
||||
"transcribing": {
|
||||
"ccButtonTooltip": "Activer/Désactiver les sous-titres",
|
||||
"ccButtonTooltip": "Activer / Désactiver les sous-titres",
|
||||
"error": "Échec de la transcription. Veuillez réessayer.",
|
||||
"expandedLabel": "La transcription est actuellement activée",
|
||||
"failedToStart": "Échec de démarrage de la transcription",
|
||||
"labelToolTip": "La transcription de la réunion est en cours",
|
||||
"off": "La transcription désactivée",
|
||||
"pending": "Préparation de la transcription de la réunion ...",
|
||||
"start": "Afficher/masquer les sous-titres",
|
||||
"stop": "Désactiver le sous-titrage",
|
||||
"start": "Activer les sous-titres",
|
||||
"stop": "Désactiver les sous-titres",
|
||||
"tr": "TR"
|
||||
},
|
||||
"userMedia": {
|
||||
@@ -939,10 +940,10 @@
|
||||
"nameField": "Saisissez votre nom",
|
||||
"notificationLobbyAccessDenied": "{{targetParticipantName}} a été refusé par {{originParticipantName}}",
|
||||
"notificationLobbyAccessGranted": "{{targetParticipantName}} a été accepté par {{originParticipantName}}",
|
||||
"notificationLobbyDisabled": "Lobby a été désactivé par {{originParticipantName}}",
|
||||
"notificationLobbyEnabled": "Lobby a été activé par {{originParticipantName}}",
|
||||
"notificationTitle": "Lobby",
|
||||
"passwordField": "Saisissez le mot de passe de la réunion",
|
||||
"notificationLobbyDisabled": "Le mode salle d'attente a été désactivé par {{originParticipantName}}",
|
||||
"notificationLobbyEnabled": "Le mode salle d'attente a été activé par {{originParticipantName}}",
|
||||
"notificationTitle": "Salle d'attente",
|
||||
"passwordField": "Veuillez saisir le mot de passe de la réunion",
|
||||
"passwordJoinButton": "Rejoindre",
|
||||
"reject": "Refuser",
|
||||
"toggleLabel": "Activer la salle d'attente"
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"today": "Today"
|
||||
},
|
||||
"chat": {
|
||||
"enter": "Enter chat room",
|
||||
"error": "Error: your message was not sent. Reason: {{error}}",
|
||||
"fieldPlaceHolder": "Type your message here",
|
||||
"messagebox": "Type a message",
|
||||
@@ -240,12 +241,20 @@
|
||||
"muteEveryoneElseTitle": "Mute everyone except {{whom}}?",
|
||||
"muteEveryoneDialog": "Are you sure you want to mute everyone? You won't be able to unmute them, but they can unmute themselves at any time.",
|
||||
"muteEveryoneTitle": "Mute everyone?",
|
||||
"muteEveryoneElsesVideoDialog": "Once the camera is disabled, you won't be able to turn it back on, but they can turn it back on at any time.",
|
||||
"muteEveryoneElsesVideoTitle": "Disable everyone's camera except {{whom}}?",
|
||||
"muteEveryonesVideoDialog": "Are you sure you want to disable everyone's camera? You won't be able to turn it back on, but they can turn it back on at any time.",
|
||||
"muteEveryonesVideoDialogOk": "Disable",
|
||||
"muteEveryonesVideoTitle": "Disable everyone's camera?",
|
||||
"muteEveryoneSelf": "yourself",
|
||||
"muteEveryoneStartMuted": "Everyone starts muted from now on",
|
||||
"muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.",
|
||||
"muteParticipantButton": "Mute",
|
||||
"muteParticipantDialog": "Are you sure you want to mute this participant? You won't be able to unmute them, but they can unmute themselves at any time.",
|
||||
"muteParticipantTitle": "Mute this participant?",
|
||||
"muteParticipantsVideoButton": "Disable camera",
|
||||
"muteParticipantsVideoTitle": "Disable camera of this participant?",
|
||||
"muteParticipantsVideoBody": "You won't be able to turn the camera back on, but they can turn it back on at any time.",
|
||||
"Ok": "OK",
|
||||
"passwordLabel": "The meeting has been locked by a participant. Please enter the $t(lockRoomPassword) to join.",
|
||||
"passwordNotSupported": "Setting a meeting $t(lockRoomPassword) is not supported.",
|
||||
@@ -305,6 +314,7 @@
|
||||
"unlockRoom": "Remove meeting $t(lockRoomPassword)",
|
||||
"user": "user",
|
||||
"userPassword": "user password",
|
||||
"videoLink": "Video link",
|
||||
"WaitForHostMsg": "The conference <b>{{room}}</b> has not yet started. If you are the host then please authenticate. Otherwise, please wait for the host to arrive.",
|
||||
"WaitForHostMsgWOk": "The conference <b>{{room}}</b> has not yet started. If you are the host then please press Ok to authenticate. Otherwise, please wait for the host to arrive.",
|
||||
"WaitingForHost": "Waiting for the host ...",
|
||||
@@ -323,6 +333,11 @@
|
||||
"embedMeeting": {
|
||||
"title": "Embed this meeting"
|
||||
},
|
||||
"virtualBackground": {
|
||||
"title": "Backgrounds",
|
||||
"enableBlur": "Enable blur",
|
||||
"removeBackground": "Remove background"
|
||||
},
|
||||
"feedback": {
|
||||
"average": "Average",
|
||||
"bad": "Bad",
|
||||
@@ -483,6 +498,8 @@
|
||||
"mutedTitle": "You're muted!",
|
||||
"mutedRemotelyTitle": "You have been muted by {{participantDisplayName}}!",
|
||||
"mutedRemotelyDescription": "You can always unmute when you're ready to speak. Mute back when you're done to keep noise away from the meeting.",
|
||||
"videoMutedRemotelyTitle": "Your camera has been disabled by {{participantDisplayName}}!",
|
||||
"videoMutedRemotelyDescription": "You can always turn it on again.",
|
||||
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removed by another participant",
|
||||
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) set by another participant",
|
||||
"raisedHand": "{{name}} would like to speak.",
|
||||
@@ -715,15 +732,19 @@
|
||||
"moreOptions": "Show more options",
|
||||
"mute": "Toggle mute audio",
|
||||
"muteEveryone": "Mute everyone",
|
||||
"muteEveryoneElse": "Mute everyone else",
|
||||
"muteEveryonesVideo": "Disable everyone's camera",
|
||||
"muteEveryoneElsesVideo": "Disable everyone else's camera",
|
||||
"pip": "Toggle Picture-in-Picture mode",
|
||||
"privateMessage": "Send private message",
|
||||
"profile": "Edit your profile",
|
||||
"raiseHand": "Toggle raise hand",
|
||||
"recording": "Toggle recording",
|
||||
"remoteMute": "Mute participant",
|
||||
"remoteVideoMute": "Disable camera of participant",
|
||||
"security": "Security options",
|
||||
"Settings": "Toggle settings",
|
||||
"sharedvideo": "Toggle Youtube video sharing",
|
||||
"sharedvideo": "Toggle YouTube video sharing",
|
||||
"shareRoom": "Invite someone",
|
||||
"shareYourScreen": "Toggle screenshare",
|
||||
"shortcuts": "Toggle shortcuts",
|
||||
@@ -733,9 +754,10 @@
|
||||
"toggleCamera": "Toggle camera",
|
||||
"toggleFilmstrip": "Toggle filmstrip",
|
||||
"videomute": "Toggle mute video",
|
||||
"videoblur": "Toggle video blur"
|
||||
"selectBackground": "Select Background"
|
||||
},
|
||||
"addPeople": "Add people to your call",
|
||||
"audioSettings": "Audio settings",
|
||||
"audioOnlyOff": "Disable low bandwidth mode",
|
||||
"audioOnlyOn": "Enable low bandwidth mode",
|
||||
"audioRoute": "Select the sound device",
|
||||
@@ -765,6 +787,7 @@
|
||||
"moreOptions": "More options",
|
||||
"mute": "Mute / Unmute",
|
||||
"muteEveryone": "Mute everyone",
|
||||
"muteEveryonesVideo": "Disable everyone's camera",
|
||||
"noAudioSignalTitle": "There is no input coming from your mic!",
|
||||
"noAudioSignalDesc": "If you did not purposely mute it from system settings or hardware, consider switching the device.",
|
||||
"noAudioSignalDescSuggestion": "If you did not purposely mute it from system settings or hardware, consider switching to the suggested device.",
|
||||
@@ -793,8 +816,7 @@
|
||||
"tileViewToggle": "Toggle tile view",
|
||||
"toggleCamera": "Toggle camera",
|
||||
"videomute": "Start / Stop camera",
|
||||
"startvideoblur": "Blur my background",
|
||||
"stopvideoblur": "Disable background blur"
|
||||
"selectBackground": "Select background"
|
||||
},
|
||||
"transcribing": {
|
||||
"ccButtonTooltip": "Start / Stop subtitles",
|
||||
@@ -849,13 +871,16 @@
|
||||
"videothumbnail": {
|
||||
"connectionInfo": "Connection Info",
|
||||
"domute": "Mute",
|
||||
"domuteVideo": "Disable camera",
|
||||
"domuteOthers": "Mute everyone else",
|
||||
"domuteVideoOfOthers": "Disable camera of everyone else",
|
||||
"flip": "Flip",
|
||||
"grantModerator": "Grant Moderator",
|
||||
"kick": "Kick out",
|
||||
"moderator": "Moderator",
|
||||
"mute": "Participant is muted",
|
||||
"muted": "Muted",
|
||||
"videoMuted": "Camera disabled",
|
||||
"remoteControl": "Start / Stop remote control",
|
||||
"show": "Show on stage",
|
||||
"videomute": "Participant has stopped the camera"
|
||||
|
||||
@@ -12,9 +12,17 @@ import {
|
||||
setPassword,
|
||||
setSubject
|
||||
} from '../../react/features/base/conference';
|
||||
import { overwriteConfig, getWhitelistedJSON } from '../../react/features/base/config';
|
||||
import { parseJWTFromURLParams } from '../../react/features/base/jwt';
|
||||
import JitsiMeetJS, { JitsiRecordingConstants } from '../../react/features/base/lib-jitsi-meet';
|
||||
import { pinParticipant, getParticipantById, kickParticipant } from '../../react/features/base/participants';
|
||||
import { MEDIA_TYPE } from '../../react/features/base/media';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantById,
|
||||
participantUpdated,
|
||||
pinParticipant,
|
||||
kickParticipant
|
||||
} from '../../react/features/base/participants';
|
||||
import { setPrivateMessageRecipient } from '../../react/features/chat/actions';
|
||||
import { openChat } from '../../react/features/chat/actions.web';
|
||||
import {
|
||||
@@ -31,8 +39,8 @@ import {
|
||||
import { toggleLobbyMode } from '../../react/features/lobby/actions.web';
|
||||
import { RECORDING_TYPES } from '../../react/features/recording/constants';
|
||||
import { getActiveSession } from '../../react/features/recording/functions';
|
||||
import { muteAllParticipants } from '../../react/features/remote-video-menu/actions';
|
||||
import { toggleTileView } from '../../react/features/video-layout';
|
||||
import { toggleTileView, setTileView } from '../../react/features/video-layout';
|
||||
import { muteAllParticipants } from '../../react/features/video-menu/actions';
|
||||
import { setVideoQuality } from '../../react/features/video-quality';
|
||||
import { getJitsiMeetTransport } from '../transport';
|
||||
|
||||
@@ -79,7 +87,9 @@ function initCommands() {
|
||||
sendAnalytics(createApiEvent('display.name.changed'));
|
||||
APP.conference.changeLocalDisplayName(displayName);
|
||||
},
|
||||
'mute-everyone': () => {
|
||||
'mute-everyone': mediaType => {
|
||||
const muteMediaType = mediaType ? mediaType : MEDIA_TYPE.AUDIO;
|
||||
|
||||
sendAnalytics(createApiEvent('muted-everyone'));
|
||||
const participants = APP.store.getState()['features/base/participants'];
|
||||
const localIds = participants
|
||||
@@ -87,7 +97,7 @@ function initCommands() {
|
||||
.filter(participant => participant.role === 'moderator')
|
||||
.map(participant => participant.id);
|
||||
|
||||
APP.store.dispatch(muteAllParticipants(localIds));
|
||||
APP.store.dispatch(muteAllParticipants(localIds, muteMediaType));
|
||||
},
|
||||
'toggle-lobby': isLobbyEnabled => {
|
||||
APP.store.dispatch(toggleLobbyMode(isLobbyEnabled));
|
||||
@@ -163,6 +173,23 @@ function initCommands() {
|
||||
sendAnalytics(createApiEvent('chat.toggled'));
|
||||
APP.UI.toggleChat();
|
||||
},
|
||||
'toggle-raise-hand': () => {
|
||||
const localParticipant = getLocalParticipant(APP.store.getState());
|
||||
|
||||
if (!localParticipant) {
|
||||
return;
|
||||
}
|
||||
const { raisedHand } = localParticipant;
|
||||
|
||||
sendAnalytics(createApiEvent('raise-hand.toggled'));
|
||||
APP.store.dispatch(
|
||||
participantUpdated({
|
||||
id: APP.conference.getMyUserId(),
|
||||
local: true,
|
||||
raisedHand: !raisedHand
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Callback to invoke when the "toggle-share-screen" command is received.
|
||||
@@ -182,6 +209,9 @@ function initCommands() {
|
||||
|
||||
APP.store.dispatch(toggleTileView());
|
||||
},
|
||||
'set-tile-view': enabled => {
|
||||
APP.store.dispatch(setTileView(enabled));
|
||||
},
|
||||
'video-hangup': (showFeedbackDialog = true) => {
|
||||
sendAnalytics(createApiEvent('video.hangup'));
|
||||
APP.conference.hangup(showFeedbackDialog);
|
||||
@@ -353,6 +383,11 @@ function initCommands() {
|
||||
},
|
||||
'kick-participant': participantId => {
|
||||
APP.store.dispatch(kickParticipant(participantId));
|
||||
},
|
||||
'overwrite-config': config => {
|
||||
const whitelistedConfig = getWhitelistedJSON('config', config);
|
||||
|
||||
APP.store.dispatch(overwriteConfig(whitelistedConfig));
|
||||
}
|
||||
};
|
||||
transport.on('event', ({ data, name }) => {
|
||||
|
||||
7
modules/API/external/external_api.js
vendored
7
modules/API/external/external_api.js
vendored
@@ -37,12 +37,14 @@ const commands = {
|
||||
intiatePrivateChat: 'initiate-private-chat',
|
||||
kickParticipant: 'kick-participant',
|
||||
muteEveryone: 'mute-everyone',
|
||||
overwriteConfig: 'overwrite-config',
|
||||
password: 'password',
|
||||
pinParticipant: 'pin-participant',
|
||||
resizeLargeVideo: 'resize-large-video',
|
||||
sendEndpointTextMessage: 'send-endpoint-text-message',
|
||||
sendTones: 'send-tones',
|
||||
setLargeVideoParticipant: 'set-large-video-participant',
|
||||
setTileView: 'set-tile-view',
|
||||
setVideoQuality: 'set-video-quality',
|
||||
startRecording: 'start-recording',
|
||||
stopRecording: 'stop-recording',
|
||||
@@ -51,6 +53,7 @@ const commands = {
|
||||
toggleAudio: 'toggle-audio',
|
||||
toggleChat: 'toggle-chat',
|
||||
toggleFilmStrip: 'toggle-film-strip',
|
||||
toggleRaiseHand: 'toggle-raise-hand',
|
||||
toggleShareScreen: 'toggle-share-screen',
|
||||
toggleTileView: 'toggle-tile-view',
|
||||
toggleVideo: 'toggle-video'
|
||||
@@ -196,7 +199,7 @@ function parseArguments(args) {
|
||||
* @param {any} value - The value to be parsed.
|
||||
* @returns {string|undefined} The parsed value that can be used for setting
|
||||
* sizes through the style property. If invalid value is passed the method
|
||||
* retuns undefined.
|
||||
* returns undefined.
|
||||
*/
|
||||
function parseSizeParam(value) {
|
||||
let parsedValue;
|
||||
@@ -320,7 +323,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
const frameName = `jitsiConferenceFrame${id}`;
|
||||
|
||||
this._frame = document.createElement('iframe');
|
||||
this._frame.allow = 'camera; microphone; display-capture; autoplay;';
|
||||
this._frame.allow = 'camera; microphone; display-capture; autoplay; clipboard-write';
|
||||
this._frame.src = this._url;
|
||||
this._frame.name = frameName;
|
||||
this._frame.id = frameName;
|
||||
|
||||
@@ -115,7 +115,6 @@ UI.start = function() {
|
||||
// Set the defaults for prompt dialogs.
|
||||
$.prompt.setDefaults({ persistent: false });
|
||||
|
||||
VideoLayout.init(eventEmitter);
|
||||
VideoLayout.initLargeVideo();
|
||||
|
||||
// Do not animate the video area on UI start (second argument passed into
|
||||
@@ -135,7 +134,6 @@ UI.start = function() {
|
||||
if (config.iAmRecorder) {
|
||||
// in case of iAmSipGateway keep local video visible
|
||||
if (!config.iAmSipGateway) {
|
||||
VideoLayout.setLocalVideoVisible(false);
|
||||
APP.store.dispatch(setNotificationsEnabled(false));
|
||||
}
|
||||
|
||||
@@ -179,14 +177,6 @@ UI.unbindEvents = () => {
|
||||
$(window).off('resize');
|
||||
};
|
||||
|
||||
/**
|
||||
* Show local video stream on UI.
|
||||
* @param {JitsiTrack} track stream to show
|
||||
*/
|
||||
UI.addLocalVideoStream = track => {
|
||||
VideoLayout.changeLocalVideo(track);
|
||||
};
|
||||
|
||||
/**
|
||||
* Setup and show Etherpad.
|
||||
* @param {string} name etherpad id
|
||||
@@ -227,14 +217,6 @@ UI.addUser = function(user) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update videotype for specified user.
|
||||
* @param {string} id user id
|
||||
* @param {string} newVideoType new videotype
|
||||
*/
|
||||
UI.onPeerVideoTypeChanged
|
||||
= (id, newVideoType) => VideoLayout.onVideoTypeChanged(id, newVideoType);
|
||||
|
||||
/**
|
||||
* Updates the user status.
|
||||
*
|
||||
@@ -289,19 +271,14 @@ UI.setAudioMuted = function(id) {
|
||||
* Sets muted video state for participant
|
||||
*/
|
||||
UI.setVideoMuted = function(id) {
|
||||
VideoLayout.onVideoMute(id);
|
||||
VideoLayout._updateLargeVideoIfDisplayed(id, true);
|
||||
|
||||
if (APP.conference.isLocalId(id)) {
|
||||
APP.conference.updateVideoIconEnabled();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Triggers an update of remote video and large video displays so they may pick
|
||||
* up any state changes that have occurred elsewhere.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
UI.updateAllVideos = () => VideoLayout.updateAllVideos();
|
||||
UI.updateLargeVideo = (id, forceUpdate) => VideoLayout.updateLargeVideo(id, forceUpdate);
|
||||
|
||||
/**
|
||||
* Adds a listener that would be notified on the given type of event.
|
||||
@@ -340,8 +317,6 @@ UI.removeListener = function(type, listener) {
|
||||
*/
|
||||
UI.emitEvent = (type, ...options) => eventEmitter.emit(type, ...options);
|
||||
|
||||
UI.clickOnVideo = videoNumber => VideoLayout.togglePin(videoNumber);
|
||||
|
||||
// Used by torture.
|
||||
UI.showToolbar = timeout => APP.store.dispatch(showToolbox(timeout));
|
||||
|
||||
@@ -491,6 +466,25 @@ UI.onSharedVideoStop = function(id, attributes) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Show shared video.
|
||||
* @param {string} url video url
|
||||
*/
|
||||
UI.startSharedVideoEmitter = function(url) {
|
||||
if (sharedVideoManager) {
|
||||
sharedVideoManager.startSharedVideoEmitter(url);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Stop shared video.
|
||||
*/
|
||||
UI.stopSharedVideoEmitter = function() {
|
||||
if (sharedVideoManager) {
|
||||
sharedVideoManager.stopSharedVideoEmitter();
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Export every function separately. For now there is no point of doing
|
||||
// this because we are importing everything.
|
||||
export default UI;
|
||||
|
||||
@@ -63,7 +63,7 @@ function doExternalAuth(room, lockPassword) {
|
||||
|
||||
/**
|
||||
* Redirect the user to the token authentication service for the login to be
|
||||
* performed. Once complete it is expected that the service wil bring the user
|
||||
* performed. Once complete it is expected that the service will bring the user
|
||||
* back with "?jwt={the JWT token}" query parameter added.
|
||||
* @param {string} [roomName] the name of the conference room.
|
||||
*/
|
||||
@@ -76,7 +76,7 @@ function redirectToTokenAuthService(roomName) {
|
||||
/**
|
||||
* Initializes 'message' listener that will wait for a JWT token to be received
|
||||
* from the token authentication service opened in a popup window.
|
||||
* @param room the name fo the conference room.
|
||||
* @param room the name of the conference room.
|
||||
*/
|
||||
function initJWTTokenListener(room) {
|
||||
/**
|
||||
@@ -108,7 +108,7 @@ function initJWTTokenListener(room) {
|
||||
roomName, APP.conference._getConferenceOptions());
|
||||
|
||||
// Authenticate from the new connection to get
|
||||
// the session-ID from the focus, which wil then be used
|
||||
// the session-ID from the focus, which will then be used
|
||||
// to upgrade current connection's user role
|
||||
|
||||
newRoom.room.moderator.authenticate()
|
||||
@@ -116,7 +116,7 @@ function initJWTTokenListener(room) {
|
||||
connection.disconnect();
|
||||
|
||||
// At this point we'll have session-ID stored in
|
||||
// the settings. It wil be used in the call below
|
||||
// the settings. It will be used in the call below
|
||||
// to upgrade user's role
|
||||
room.room.moderator.authenticate()
|
||||
.then(() => {
|
||||
|
||||
@@ -12,11 +12,10 @@ import {
|
||||
participantLeft,
|
||||
pinParticipant
|
||||
} from '../../../react/features/base/participants';
|
||||
import { VIDEO_PLAYER_PARTICIPANT_NAME } from '../../../react/features/shared-video/constants';
|
||||
import { dockToolbox, showToolbox } from '../../../react/features/toolbox/actions.web';
|
||||
import { getToolboxHeight } from '../../../react/features/toolbox/functions.web';
|
||||
import { YOUTUBE_PARTICIPANT_NAME } from '../../../react/features/youtube-player/constants';
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
import UIUtil from '../util/UIUtil';
|
||||
import Filmstrip from '../videolayout/Filmstrip';
|
||||
import LargeContainer from '../videolayout/LargeContainer';
|
||||
import VideoLayout from '../videolayout/VideoLayout';
|
||||
@@ -29,14 +28,8 @@ export const SHARED_VIDEO_CONTAINER_TYPE = 'sharedvideo';
|
||||
* Example shared video link.
|
||||
* @type {string}
|
||||
*/
|
||||
const defaultSharedVideoLink = 'https://youtu.be/TB7LlM4erx8';
|
||||
const updateInterval = 5000; // milliseconds
|
||||
|
||||
/**
|
||||
* The dialog for user input (video link).
|
||||
* @type {null}
|
||||
*/
|
||||
let dialog = null;
|
||||
|
||||
/**
|
||||
* Manager of shared video.
|
||||
@@ -76,52 +69,37 @@ export default class SharedVideoManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts shared video by asking user for url, or if its already working
|
||||
* asks whether the user wants to stop sharing the video.
|
||||
* Start shared video event emitter if a video is not shown.
|
||||
*
|
||||
* @param url of the video
|
||||
*/
|
||||
toggleSharedVideo() {
|
||||
if (dialog) {
|
||||
return;
|
||||
}
|
||||
startSharedVideoEmitter(url) {
|
||||
|
||||
if (!this.isSharedVideoShown) {
|
||||
requestVideoLink().then(
|
||||
url => {
|
||||
this.emitter.emit(
|
||||
UIEvents.UPDATE_SHARED_VIDEO, url, 'start');
|
||||
sendAnalytics(createEvent('started'));
|
||||
},
|
||||
err => {
|
||||
logger.log('SHARED VIDEO CANCELED', err);
|
||||
sendAnalytics(createEvent('canceled'));
|
||||
}
|
||||
);
|
||||
if (url) {
|
||||
this.emitter.emit(
|
||||
UIEvents.UPDATE_SHARED_VIDEO, url, 'start');
|
||||
sendAnalytics(createEvent('started'));
|
||||
}
|
||||
|
||||
return;
|
||||
logger.log('SHARED VIDEO CANCELED');
|
||||
sendAnalytics(createEvent('canceled'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop shared video event emitter done by the one who shared the video.
|
||||
*/
|
||||
stopSharedVideoEmitter() {
|
||||
|
||||
if (APP.conference.isLocalId(this.from)) {
|
||||
showStopVideoPropmpt().then(
|
||||
() => {
|
||||
// make sure we stop updates for playing before we send stop
|
||||
// if we stop it after receiving self presence, we can end
|
||||
// up sending stop playing, and on the other end it will not
|
||||
// stop
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
}
|
||||
this.emitter.emit(
|
||||
UIEvents.UPDATE_SHARED_VIDEO, this.url, 'stop');
|
||||
sendAnalytics(createEvent('stopped'));
|
||||
},
|
||||
() => {}); // eslint-disable-line no-empty-function
|
||||
} else {
|
||||
APP.UI.messageHandler.showWarning({
|
||||
descriptionKey: 'dialog.alreadySharedVideoMsg',
|
||||
titleKey: 'dialog.alreadySharedVideoTitle'
|
||||
});
|
||||
sendAnalytics(createEvent('already.shared'));
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
}
|
||||
this.emitter.emit(
|
||||
UIEvents.UPDATE_SHARED_VIDEO, this.url, 'stop');
|
||||
sendAnalytics(createEvent('stopped'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,7 +281,7 @@ export default class SharedVideoManager {
|
||||
conference: APP.conference._room,
|
||||
id: self.url,
|
||||
isFakeParticipant: true,
|
||||
name: YOUTUBE_PARTICIPANT_NAME
|
||||
name: VIDEO_PLAYER_PARTICIPANT_NAME
|
||||
}));
|
||||
|
||||
APP.store.dispatch(pinParticipant(self.url));
|
||||
@@ -520,7 +498,7 @@ export default class SharedVideoManager {
|
||||
* Receives events for local audio mute/unmute by local user.
|
||||
* @param muted boolena whether it is muted or not.
|
||||
* @param {boolean} indicates if this mute was a result of user interaction,
|
||||
* i.e. pressing the mute button or it was programatically triggerred
|
||||
* i.e. pressing the mute button or it was programmatically triggered
|
||||
*/
|
||||
onLocalAudioMuted(muted, userInteraction) {
|
||||
if (!this.player) {
|
||||
@@ -675,134 +653,3 @@ class SharedVideoContainer extends LargeContainer {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if given string is youtube url.
|
||||
* @param {string} url string to check.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function getYoutubeLink(url) {
|
||||
const p = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;// eslint-disable-line max-len
|
||||
|
||||
|
||||
return url.match(p) ? RegExp.$1 : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask user if he want to close shared video.
|
||||
*/
|
||||
function showStopVideoPropmpt() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const submitFunction = function(e, v) {
|
||||
if (v) {
|
||||
resolve();
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
};
|
||||
|
||||
const closeFunction = function() {
|
||||
dialog = null;
|
||||
};
|
||||
|
||||
dialog = APP.UI.messageHandler.openTwoButtonDialog({
|
||||
titleKey: 'dialog.removeSharedVideoTitle',
|
||||
msgKey: 'dialog.removeSharedVideoMsg',
|
||||
leftButtonKey: 'dialog.Remove',
|
||||
submitFunction,
|
||||
closeFunction
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask user for shared video url to share with others.
|
||||
* Dialog validates client input to allow only youtube urls.
|
||||
*/
|
||||
function requestVideoLink() {
|
||||
const i18n = APP.translation;
|
||||
const cancelButton = i18n.generateTranslationHTML('dialog.Cancel');
|
||||
const shareButton = i18n.generateTranslationHTML('dialog.Share');
|
||||
const backButton = i18n.generateTranslationHTML('dialog.Back');
|
||||
const linkError
|
||||
= i18n.generateTranslationHTML('dialog.shareVideoLinkError');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
dialog = APP.UI.messageHandler.openDialogWithStates({
|
||||
state0: {
|
||||
titleKey: 'dialog.shareVideoTitle',
|
||||
html: `
|
||||
<input name='sharedVideoUrl' type='text'
|
||||
class='input-control'
|
||||
data-i18n='[placeholder]defaultLink'
|
||||
autofocus>`,
|
||||
persistent: false,
|
||||
buttons: [
|
||||
{ title: cancelButton,
|
||||
value: false },
|
||||
{ title: shareButton,
|
||||
value: true }
|
||||
],
|
||||
focus: ':input:first',
|
||||
defaultButton: 1,
|
||||
submit(e, v, m, f) { // eslint-disable-line max-params
|
||||
e.preventDefault();
|
||||
if (!v) {
|
||||
reject('cancelled');
|
||||
dialog.close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const sharedVideoUrl = f.sharedVideoUrl;
|
||||
|
||||
if (!sharedVideoUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const urlValue
|
||||
= encodeURI(UIUtil.escapeHtml(sharedVideoUrl));
|
||||
const yVideoId = getYoutubeLink(urlValue);
|
||||
|
||||
if (!yVideoId) {
|
||||
dialog.goToState('state1');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
resolve(yVideoId);
|
||||
dialog.close();
|
||||
}
|
||||
},
|
||||
|
||||
state1: {
|
||||
titleKey: 'dialog.shareVideoTitle',
|
||||
html: linkError,
|
||||
persistent: false,
|
||||
buttons: [
|
||||
{ title: cancelButton,
|
||||
value: false },
|
||||
{ title: backButton,
|
||||
value: true }
|
||||
],
|
||||
focus: ':input:first',
|
||||
defaultButton: 1,
|
||||
submit(e, v) {
|
||||
e.preventDefault();
|
||||
if (v === 0) {
|
||||
reject();
|
||||
dialog.close();
|
||||
} else {
|
||||
dialog.goToState('state0');
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
close() {
|
||||
dialog = null;
|
||||
}
|
||||
}, {
|
||||
url: defaultSharedVideoLink
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
/* global $, APP */
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { i18next } from '../../../react/features/base/i18n';
|
||||
import { Thumbnail } from '../../../react/features/filmstrip';
|
||||
import SmallVideo from '../videolayout/SmallVideo';
|
||||
/* eslint-enable no-unused-vars */
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default class SharedVideoThumb extends SmallVideo {
|
||||
/**
|
||||
*
|
||||
* @param {*} participant
|
||||
*/
|
||||
constructor(participant) {
|
||||
super();
|
||||
this.id = participant.id;
|
||||
this.isLocal = false;
|
||||
this.url = participant.id;
|
||||
this.videoSpanId = 'sharedVideoContainer';
|
||||
this.container = this.createContainer(this.videoSpanId);
|
||||
this.$container = $(this.container);
|
||||
this.renderThumbnail();
|
||||
this._setThumbnailSize();
|
||||
this.bindHoverHandler();
|
||||
this.container.onclick = this._onContainerClick;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} spanId
|
||||
*/
|
||||
createContainer(spanId) {
|
||||
const container = document.createElement('span');
|
||||
|
||||
container.id = spanId;
|
||||
container.className = 'videocontainer';
|
||||
|
||||
const remoteVideosContainer
|
||||
= document.getElementById('filmstripRemoteVideosContainer');
|
||||
const localVideoContainer
|
||||
= document.getElementById('localVideoTileViewContainer');
|
||||
|
||||
remoteVideosContainer.insertBefore(container, localVideoContainer);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the thumbnail.
|
||||
*/
|
||||
renderThumbnail(isHovered = false) {
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
|
||||
</I18nextProvider>
|
||||
</Provider>, this.container);
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ const UIUtil = {
|
||||
* @param {string} url - The redirect URL.
|
||||
* NOTE: Currently used to redirect to 3rd party location for
|
||||
* authentication. In most cases redirectWithStoredParams action must be
|
||||
* used instead of this method in order to preserve curent URL params.
|
||||
* used instead of this method in order to preserve current URL params.
|
||||
*/
|
||||
redirect(url) {
|
||||
window.location.href = url;
|
||||
|
||||
@@ -25,129 +25,6 @@ const Filmstrip = {
|
||||
*/
|
||||
getVerticalFilmstripWidth() {
|
||||
return isFilmstripVisible(APP.store) ? getVerticalFilmstripVisibleAreaWidth() : 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Resizes thumbnails for tile view.
|
||||
*
|
||||
* @param {number} width - The new width of the thumbnails.
|
||||
* @param {number} height - The new height of the thumbnails.
|
||||
* @param {boolean} forceUpdate
|
||||
* @returns {void}
|
||||
*/
|
||||
resizeThumbnailsForTileView(width, height, forceUpdate = false) {
|
||||
const thumbs = this._getThumbs(!forceUpdate);
|
||||
|
||||
if (thumbs.localThumb) {
|
||||
thumbs.localThumb.css({
|
||||
'padding-top': '',
|
||||
height: `${height}px`,
|
||||
'min-height': `${height}px`,
|
||||
'min-width': `${width}px`,
|
||||
width: `${width}px`
|
||||
});
|
||||
}
|
||||
|
||||
if (thumbs.remoteThumbs) {
|
||||
thumbs.remoteThumbs.css({
|
||||
'padding-top': '',
|
||||
height: `${height}px`,
|
||||
'min-height': `${height}px`,
|
||||
'min-width': `${width}px`,
|
||||
width: `${width}px`
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Resizes thumbnails for horizontal view.
|
||||
*
|
||||
* @param {Object} dimensions - The new dimensions of the thumbnails.
|
||||
* @param {boolean} forceUpdate
|
||||
* @returns {void}
|
||||
*/
|
||||
resizeThumbnailsForHorizontalView({ local = {}, remote = {} }, forceUpdate = false) {
|
||||
const thumbs = this._getThumbs(!forceUpdate);
|
||||
|
||||
if (thumbs.localThumb) {
|
||||
const { height, width } = local;
|
||||
|
||||
thumbs.localThumb.css({
|
||||
height: `${height}px`,
|
||||
'min-height': `${height}px`,
|
||||
'min-width': `${width}px`,
|
||||
width: `${width}px`
|
||||
});
|
||||
}
|
||||
|
||||
if (thumbs.remoteThumbs) {
|
||||
const { height, width } = remote;
|
||||
|
||||
thumbs.remoteThumbs.css({
|
||||
height: `${height}px`,
|
||||
'min-height': `${height}px`,
|
||||
'min-width': `${width}px`,
|
||||
width: `${width}px`
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Resizes thumbnails for vertical view.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
resizeThumbnailsForVerticalView() {
|
||||
const thumbs = this._getThumbs(true);
|
||||
|
||||
if (thumbs.localThumb) {
|
||||
const heightToWidthPercent = 100 / interfaceConfig.LOCAL_THUMBNAIL_RATIO;
|
||||
|
||||
thumbs.localThumb.css({
|
||||
'padding-top': `${heightToWidthPercent}%`,
|
||||
width: '',
|
||||
height: '',
|
||||
'min-width': '',
|
||||
'min-height': ''
|
||||
});
|
||||
}
|
||||
|
||||
if (thumbs.remoteThumbs) {
|
||||
const heightToWidthPercent = 100 / interfaceConfig.REMOTE_THUMBNAIL_RATIO;
|
||||
|
||||
thumbs.remoteThumbs.css({
|
||||
'padding-top': `${heightToWidthPercent}%`,
|
||||
width: '',
|
||||
height: '',
|
||||
'min-width': '',
|
||||
'min-height': ''
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns thumbnails of the filmstrip
|
||||
* @param onlyVisible
|
||||
* @returns {object} thumbnails
|
||||
*/
|
||||
_getThumbs(onlyVisible = false) {
|
||||
let selector = 'span';
|
||||
|
||||
if (onlyVisible) {
|
||||
selector += ':visible';
|
||||
}
|
||||
|
||||
const localThumb = $('#localVideoContainer');
|
||||
const remoteThumbs = $('#filmstripRemoteVideosContainer').children(selector);
|
||||
|
||||
// Exclude the local video container if it has been hidden.
|
||||
if (localThumb.hasClass('hidden')) {
|
||||
return { remoteThumbs };
|
||||
}
|
||||
|
||||
return { remoteThumbs,
|
||||
localThumb };
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
import { PresenceLabel } from '../../../react/features/presence-status';
|
||||
import { shouldDisplayTileView } from '../../../react/features/video-layout';
|
||||
/* eslint-enable no-unused-vars */
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
import { createDeferred } from '../../util/helpers';
|
||||
import AudioLevels from '../audio_levels/AudioLevels';
|
||||
|
||||
@@ -51,21 +50,19 @@ export default class LargeVideoManager {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
constructor(emitter) {
|
||||
constructor() {
|
||||
/**
|
||||
* The map of <tt>LargeContainer</tt>s where the key is the video
|
||||
* container type.
|
||||
* @type {Object.<string, LargeContainer>}
|
||||
*/
|
||||
this.containers = {};
|
||||
this.eventEmitter = emitter;
|
||||
|
||||
this.state = VIDEO_CONTAINER_TYPE;
|
||||
|
||||
// FIXME: We are passing resizeContainer as parameter which is calling
|
||||
// Container.resize. Probably there's better way to implement this.
|
||||
this.videoContainer = new VideoContainer(
|
||||
() => this.resizeContainer(VIDEO_CONTAINER_TYPE), emitter);
|
||||
this.videoContainer = new VideoContainer(() => this.resizeContainer(VIDEO_CONTAINER_TYPE));
|
||||
this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer);
|
||||
|
||||
// use the same video container to handle desktop tracks
|
||||
@@ -300,7 +297,6 @@ export default class LargeVideoManager {
|
||||
// after everything is done check again if there are any pending
|
||||
// new streams.
|
||||
this.updateInProcess = false;
|
||||
this.eventEmitter.emit(UIEvents.LARGE_VIDEO_ID_CHANGED, this.id);
|
||||
this.scheduleLargeVideoUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
/* global $, config, APP */
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { i18next } from '../../../react/features/base/i18n';
|
||||
import { JitsiTrackEvents } from '../../../react/features/base/lib-jitsi-meet';
|
||||
import { VideoTrack } from '../../../react/features/base/media';
|
||||
import { updateSettings } from '../../../react/features/base/settings';
|
||||
import { getLocalVideoTrack } from '../../../react/features/base/tracks';
|
||||
import Thumbnail from '../../../react/features/filmstrip/components/web/Thumbnail';
|
||||
import { shouldDisplayTileView } from '../../../react/features/video-layout';
|
||||
/* eslint-enable no-unused-vars */
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
|
||||
import SmallVideo from './SmallVideo';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default class LocalVideo extends SmallVideo {
|
||||
/**
|
||||
*
|
||||
* @param {*} emitter
|
||||
* @param {*} streamEndedCallback
|
||||
*/
|
||||
constructor(emitter, streamEndedCallback) {
|
||||
super();
|
||||
this.videoSpanId = 'localVideoContainer';
|
||||
this.streamEndedCallback = streamEndedCallback;
|
||||
this.container = this.createContainer();
|
||||
this.$container = $(this.container);
|
||||
this.isLocal = true;
|
||||
this._setThumbnailSize();
|
||||
this.updateDOMLocation();
|
||||
this.renderThumbnail();
|
||||
|
||||
this.localVideoId = null;
|
||||
this.bindHoverHandler();
|
||||
if (!config.disableLocalVideoFlip) {
|
||||
this._buildContextMenu();
|
||||
}
|
||||
this.emitter = emitter;
|
||||
|
||||
Object.defineProperty(this, 'id', {
|
||||
get() {
|
||||
return APP.conference.getMyUserId();
|
||||
}
|
||||
});
|
||||
this.initBrowserSpecificProperties();
|
||||
|
||||
this.container.onclick = this._onContainerClick;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
createContainer() {
|
||||
const containerSpan = document.createElement('span');
|
||||
|
||||
containerSpan.classList.add('videocontainer');
|
||||
containerSpan.id = this.videoSpanId;
|
||||
|
||||
return containerSpan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the thumbnail.
|
||||
*/
|
||||
renderThumbnail(isHovered = false) {
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
|
||||
</I18nextProvider>
|
||||
</Provider>, this.container);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} stream
|
||||
*/
|
||||
changeVideo(stream) {
|
||||
this.localVideoId = `localVideo_${stream.getId()}`;
|
||||
|
||||
// eslint-disable-next-line eqeqeq
|
||||
const isVideo = stream.videoType != 'desktop';
|
||||
const settings = APP.store.getState()['features/base/settings'];
|
||||
|
||||
this._enableDisableContextMenu(isVideo);
|
||||
this.setFlipX(isVideo ? settings.localFlipX : false);
|
||||
|
||||
const endedHandler = () => {
|
||||
this._notifyOfStreamEnded();
|
||||
stream.off(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
|
||||
};
|
||||
|
||||
stream.on(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify any subscribers of the local video stream ending.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_notifyOfStreamEnded() {
|
||||
if (this.streamEndedCallback) {
|
||||
this.streamEndedCallback(this.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows or hides the local video container.
|
||||
* @param {boolean} true to make the local video container visible, false
|
||||
* otherwise
|
||||
*/
|
||||
setVisible(visible) {
|
||||
// We toggle the hidden class as an indication to other interested parties
|
||||
// that this container has been hidden on purpose.
|
||||
this.$container.toggleClass('hidden');
|
||||
|
||||
// We still show/hide it as we need to overwrite the style property if we
|
||||
// want our action to take effect. Toggling the display property through
|
||||
// the above css class didn't succeed in overwriting the style.
|
||||
if (visible) {
|
||||
this.$container.show();
|
||||
} else {
|
||||
this.$container.hide();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the flipX state of the video.
|
||||
* @param val {boolean} true for flipped otherwise false;
|
||||
*/
|
||||
setFlipX(val) {
|
||||
this.emitter.emit(UIEvents.LOCAL_FLIPX_CHANGED, val);
|
||||
if (!this.localVideoId) {
|
||||
return;
|
||||
}
|
||||
if (val) {
|
||||
this.selectVideoElement().addClass('flipVideoX');
|
||||
} else {
|
||||
this.selectVideoElement().removeClass('flipVideoX');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the context menu for the local video.
|
||||
*/
|
||||
_buildContextMenu() {
|
||||
$.contextMenu({
|
||||
selector: `#${this.videoSpanId}`,
|
||||
zIndex: 10000,
|
||||
items: {
|
||||
flip: {
|
||||
name: 'Flip',
|
||||
callback: () => {
|
||||
const { store } = APP;
|
||||
const val = !store.getState()['features/base/settings']
|
||||
.localFlipX;
|
||||
|
||||
this.setFlipX(val);
|
||||
store.dispatch(updateSettings({
|
||||
localFlipX: val
|
||||
}));
|
||||
}
|
||||
}
|
||||
},
|
||||
events: {
|
||||
show(options) {
|
||||
options.items.flip.name
|
||||
= APP.translation.generateTranslationHTML(
|
||||
'videothumbnail.flip');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables the context menu for the local video.
|
||||
* @param enable {boolean} true for enable, false for disable
|
||||
*/
|
||||
_enableDisableContextMenu(enable) {
|
||||
if (this.$container.contextMenu) {
|
||||
this.$container.contextMenu(enable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Places the {@code LocalVideo} in the DOM based on the current video layout.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
updateDOMLocation() {
|
||||
if (!this.container) {
|
||||
return;
|
||||
}
|
||||
if (this.container.parentElement) {
|
||||
this.container.parentElement.removeChild(this.container);
|
||||
}
|
||||
|
||||
const appendTarget = shouldDisplayTileView(APP.store.getState())
|
||||
? document.getElementById('localVideoTileViewContainer')
|
||||
: document.getElementById('filmstripLocalVideoThumbnail');
|
||||
|
||||
appendTarget && appendTarget.appendChild(this.container);
|
||||
}
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
/* global $, APP, config */
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
||||
import Logger from 'jitsi-meet-logger';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { i18next } from '../../../react/features/base/i18n';
|
||||
import {
|
||||
JitsiParticipantConnectionStatus
|
||||
} from '../../../react/features/base/lib-jitsi-meet';
|
||||
import { getParticipantById } from '../../../react/features/base/participants';
|
||||
import { isTestModeEnabled } from '../../../react/features/base/testing';
|
||||
import { updateLastTrackVideoMediaEvent } from '../../../react/features/base/tracks';
|
||||
import { Thumbnail, isVideoPlayable } from '../../../react/features/filmstrip';
|
||||
import { PresenceLabel } from '../../../react/features/presence-status';
|
||||
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';
|
||||
|
||||
import SmallVideo from './SmallVideo';
|
||||
|
||||
const logger = Logger.getLogger(__filename);
|
||||
|
||||
/**
|
||||
* List of container events that we are going to process, will be added as listener to the
|
||||
* container for every event in the list. The latest event will be stored in redux.
|
||||
*/
|
||||
const containerEvents = [
|
||||
'abort', 'canplay', 'canplaythrough', 'emptied', 'ended', 'error', 'loadeddata', 'loadedmetadata', 'loadstart',
|
||||
'pause', 'play', 'playing', 'ratechange', 'stalled', 'suspend', 'waiting'
|
||||
];
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} spanId
|
||||
*/
|
||||
function createContainer(spanId) {
|
||||
const container = document.createElement('span');
|
||||
|
||||
container.id = spanId;
|
||||
container.className = 'videocontainer';
|
||||
|
||||
const remoteVideosContainer
|
||||
= document.getElementById('filmstripRemoteVideosContainer');
|
||||
const localVideoContainer
|
||||
= document.getElementById('localVideoTileViewContainer');
|
||||
|
||||
remoteVideosContainer.insertBefore(container, localVideoContainer);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default class RemoteVideo extends SmallVideo {
|
||||
/**
|
||||
* Creates new instance of the <tt>RemoteVideo</tt>.
|
||||
* @param user {JitsiParticipant} the user for whom remote video instance will
|
||||
* be created.
|
||||
* @constructor
|
||||
*/
|
||||
constructor(user) {
|
||||
super();
|
||||
|
||||
this.user = user;
|
||||
this.id = user.getId();
|
||||
this.videoSpanId = `participant_${this.id}`;
|
||||
|
||||
this.addRemoteVideoContainer();
|
||||
this.bindHoverHandler();
|
||||
this.flipX = false;
|
||||
this.isLocal = false;
|
||||
|
||||
/**
|
||||
* The flag is set to <tt>true</tt> after the 'canplay' event has been
|
||||
* triggered on the current video element. It goes back to <tt>false</tt>
|
||||
* when the stream is removed. It is used to determine whether the video
|
||||
* playback has ever started.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this._canPlayEventReceived = false;
|
||||
|
||||
this.container.onclick = this._onContainerClick;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
addRemoteVideoContainer() {
|
||||
this.container = createContainer(this.videoSpanId);
|
||||
this.$container = $(this.container);
|
||||
this.renderThumbnail();
|
||||
this._setThumbnailSize();
|
||||
this.initBrowserSpecificProperties();
|
||||
|
||||
return this.container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the thumbnail.
|
||||
*/
|
||||
renderThumbnail(isHovered = false) {
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
|
||||
</I18nextProvider>
|
||||
</Provider>, this.container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the remote stream element corresponding to the given stream and
|
||||
* parent container.
|
||||
*
|
||||
* @param stream the MediaStream
|
||||
* @param isVideo <tt>true</tt> if given <tt>stream</tt> is a video one.
|
||||
*/
|
||||
removeRemoteStreamElement(stream) {
|
||||
if (!this.container) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isVideo = stream.isVideoTrack();
|
||||
const elementID = `remoteVideo_${stream.getId()}`;
|
||||
const select = $(`#${elementID}`);
|
||||
|
||||
select.remove();
|
||||
if (isVideo) {
|
||||
this._canPlayEventReceived = false;
|
||||
}
|
||||
|
||||
logger.info(`Video removed ${this.id}`, select);
|
||||
|
||||
this.updateView();
|
||||
}
|
||||
|
||||
/**
|
||||
* The remote video is considered "playable" once the can play event has been received.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @override
|
||||
*/
|
||||
isVideoPlayable() {
|
||||
return isVideoPlayable(APP.store.getState(), this.id) && this._canPlayEventReceived;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
updateView() {
|
||||
this.$container.toggleClass('audio-only', APP.conference.isAudioOnly());
|
||||
super.updateView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes RemoteVideo from the page.
|
||||
*/
|
||||
remove() {
|
||||
ReactDOM.unmountComponentAtNode(this.container);
|
||||
super.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} streamElement
|
||||
* @param {*} stream
|
||||
*/
|
||||
waitForPlayback(streamElement, stream) {
|
||||
$(streamElement).hide();
|
||||
|
||||
const webRtcStream = stream.getOriginalStream();
|
||||
const isVideo = stream.isVideoTrack();
|
||||
|
||||
if (!isVideo || webRtcStream.id === 'mixedmslabel') {
|
||||
return;
|
||||
}
|
||||
|
||||
const listener = () => {
|
||||
this._canPlayEventReceived = true;
|
||||
|
||||
logger.info(`${this.id} video is now active`, streamElement);
|
||||
if (streamElement) {
|
||||
$(streamElement).show();
|
||||
}
|
||||
|
||||
streamElement.removeEventListener('canplay', listener);
|
||||
|
||||
// Refresh to show the video
|
||||
this.updateView();
|
||||
};
|
||||
|
||||
streamElement.addEventListener('canplay', listener);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} stream
|
||||
*/
|
||||
addRemoteStreamElement(stream) {
|
||||
if (!this.container) {
|
||||
logger.debug('Not attaching remote stream due to no container');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const isVideo = stream.isVideoTrack();
|
||||
|
||||
if (!stream.getOriginalStream()) {
|
||||
logger.debug('Remote video stream has no original stream');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let streamElement = document.createElement('video');
|
||||
|
||||
streamElement.autoplay = !config.testing?.noAutoPlayVideo;
|
||||
streamElement.id = `remoteVideo_${stream.getId()}`;
|
||||
|
||||
// Put new stream element always in front
|
||||
streamElement = UIUtils.prependChild(this.container, streamElement);
|
||||
|
||||
this.waitForPlayback(streamElement, stream);
|
||||
stream.attach(streamElement);
|
||||
|
||||
if (isVideo && isTestModeEnabled(APP.store.getState())) {
|
||||
|
||||
const cb = name => APP.store.dispatch(updateLastTrackVideoMediaEvent(stream, name));
|
||||
|
||||
containerEvents.forEach(event => {
|
||||
streamElement.addEventListener(event, cb.bind(this, event));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,509 +0,0 @@
|
||||
/* global $, APP, interfaceConfig */
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
||||
import Logger from 'jitsi-meet-logger';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { createScreenSharingIssueEvent, sendAnalytics } from '../../../react/features/analytics';
|
||||
import { AudioLevelIndicator } from '../../../react/features/audio-level-indicator';
|
||||
import { Avatar as AvatarDisplay } from '../../../react/features/base/avatar';
|
||||
import { i18next } from '../../../react/features/base/i18n';
|
||||
import { MEDIA_TYPE } from '../../../react/features/base/media';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantById,
|
||||
getParticipantCount,
|
||||
getPinnedParticipant,
|
||||
pinParticipant
|
||||
} from '../../../react/features/base/participants';
|
||||
import {
|
||||
getLocalVideoTrack,
|
||||
getTrackByMediaTypeAndParticipant,
|
||||
isLocalTrackMuted,
|
||||
isRemoteTrackMuted
|
||||
} from '../../../react/features/base/tracks';
|
||||
import { ConnectionIndicator } from '../../../react/features/connection-indicator';
|
||||
import { DisplayName } from '../../../react/features/display-name';
|
||||
import {
|
||||
DominantSpeakerIndicator,
|
||||
RaisedHandIndicator,
|
||||
StatusIndicators,
|
||||
isVideoPlayable
|
||||
} from '../../../react/features/filmstrip';
|
||||
import {
|
||||
LAYOUTS,
|
||||
getCurrentLayout,
|
||||
setTileView,
|
||||
shouldDisplayTileView
|
||||
} from '../../../react/features/video-layout';
|
||||
/* eslint-enable no-unused-vars */
|
||||
|
||||
const logger = Logger.getLogger(__filename);
|
||||
|
||||
/**
|
||||
* Display mode constant used when video is being displayed on the small video.
|
||||
* @type {number}
|
||||
* @constant
|
||||
*/
|
||||
const DISPLAY_VIDEO = 0;
|
||||
|
||||
/**
|
||||
* Display mode constant used when the user's avatar is being displayed on
|
||||
* the small video.
|
||||
* @type {number}
|
||||
* @constant
|
||||
*/
|
||||
const DISPLAY_AVATAR = 1;
|
||||
|
||||
/**
|
||||
* Display mode constant used when neither video nor avatar is being displayed
|
||||
* on the small video. And we just show the display name.
|
||||
* @type {number}
|
||||
* @constant
|
||||
*/
|
||||
const DISPLAY_BLACKNESS_WITH_NAME = 2;
|
||||
|
||||
/**
|
||||
* Display mode constant used when video is displayed and display name
|
||||
* at the same time.
|
||||
* @type {number}
|
||||
* @constant
|
||||
*/
|
||||
const DISPLAY_VIDEO_WITH_NAME = 3;
|
||||
|
||||
/**
|
||||
* Display mode constant used when neither video nor avatar is being displayed
|
||||
* on the small video. And we just show the display name.
|
||||
* @type {number}
|
||||
* @constant
|
||||
*/
|
||||
const DISPLAY_AVATAR_WITH_NAME = 4;
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default class SmallVideo {
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
constructor() {
|
||||
this.videoIsHovered = false;
|
||||
this.videoType = undefined;
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this.updateView = this.updateView.bind(this);
|
||||
|
||||
this._onContainerClick = this._onContainerClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the identifier of this small video.
|
||||
*
|
||||
* @returns the identifier of this small video
|
||||
*/
|
||||
getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this small video is currently visible.
|
||||
*
|
||||
* @return <tt>true</tt> if this small video isn't currently visible and
|
||||
* <tt>false</tt> - otherwise.
|
||||
*/
|
||||
isVisible() {
|
||||
return this.$container.is(':visible');
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures hoverIn/hoverOut handlers. Depends on connection indicator.
|
||||
*/
|
||||
bindHoverHandler() {
|
||||
// Add hover handler
|
||||
this.$container.hover(
|
||||
() => {
|
||||
this.videoIsHovered = true;
|
||||
this.renderThumbnail(true);
|
||||
this.updateView();
|
||||
},
|
||||
() => {
|
||||
this.videoIsHovered = false;
|
||||
this.renderThumbnail(false);
|
||||
this.updateView();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the thumbnail.
|
||||
*/
|
||||
renderThumbnail() {
|
||||
// Should be implemented by in subclasses.
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an especially interesting function. A naive reader might think that
|
||||
* it returns this SmallVideo's "video" element. But it is much more exciting.
|
||||
* It first finds this video's parent element using jquery, then uses a utility
|
||||
* from lib-jitsi-meet to extract the video element from it (with two more
|
||||
* jquery calls), and finally uses jquery again to encapsulate the video element
|
||||
* in an array. This last step allows (some might prefer "forces") users of
|
||||
* this function to access the video element via the 0th element of the returned
|
||||
* array (after checking its length of course!).
|
||||
*/
|
||||
selectVideoElement() {
|
||||
return $($(this.container).find('video')[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables / disables the css responsible for focusing/pinning a video
|
||||
* thumbnail.
|
||||
*
|
||||
* @param isFocused indicates if the thumbnail should be focused/pinned or not
|
||||
*/
|
||||
focus(isFocused) {
|
||||
const focusedCssClass = 'videoContainerFocused';
|
||||
const isFocusClassEnabled = this.$container.hasClass(focusedCssClass);
|
||||
|
||||
if (!isFocused && isFocusClassEnabled) {
|
||||
this.$container.removeClass(focusedCssClass);
|
||||
} else if (isFocused && !isFocusClassEnabled) {
|
||||
this.$container.addClass(focusedCssClass);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
hasVideo() {
|
||||
return this.selectVideoElement().length !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the user associated with this <tt>SmallVideo</tt> is currently
|
||||
* being displayed on the "large video".
|
||||
*
|
||||
* @return {boolean} <tt>true</tt> if the user is displayed on the large video
|
||||
* or <tt>false</tt> otherwise.
|
||||
*/
|
||||
isCurrentlyOnLargeVideo() {
|
||||
return APP.store.getState()['features/large-video']?.participantId === this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether there is a playable video stream available for the user
|
||||
* associated with this <tt>SmallVideo</tt>.
|
||||
*
|
||||
* @return {boolean} <tt>true</tt> if there is a playable video stream available
|
||||
* or <tt>false</tt> otherwise.
|
||||
*/
|
||||
isVideoPlayable() {
|
||||
return isVideoPlayable(APP.store.getState(), this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines what should be display on the thumbnail.
|
||||
*
|
||||
* @return {number} one of <tt>DISPLAY_VIDEO</tt>,<tt>DISPLAY_AVATAR</tt>
|
||||
* or <tt>DISPLAY_BLACKNESS_WITH_NAME</tt>.
|
||||
*/
|
||||
selectDisplayMode(input) {
|
||||
if (!input.tileViewActive && input.isScreenSharing) {
|
||||
return input.isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
|
||||
} else if (input.isCurrentlyOnLargeVideo && !input.tileViewActive) {
|
||||
// Display name is always and only displayed when user is on the stage
|
||||
return input.isVideoPlayable && !input.isAudioOnly ? DISPLAY_BLACKNESS_WITH_NAME : DISPLAY_AVATAR_WITH_NAME;
|
||||
} else if (input.isVideoPlayable && input.hasVideo && !input.isAudioOnly) {
|
||||
// check hovering and change state to video with name
|
||||
return input.isHovered ? DISPLAY_VIDEO_WITH_NAME : DISPLAY_VIDEO;
|
||||
}
|
||||
|
||||
// check hovering and change state to avatar with name
|
||||
return input.isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes information that determine the display mode.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
computeDisplayModeInput() {
|
||||
let isScreenSharing = false;
|
||||
let connectionStatus;
|
||||
const state = APP.store.getState();
|
||||
const id = this.id;
|
||||
const participant = getParticipantById(state, id);
|
||||
const isLocal = participant?.local ?? true;
|
||||
const tracks = state['features/base/tracks'];
|
||||
const videoTrack
|
||||
= isLocal ? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
|
||||
|
||||
if (typeof participant !== 'undefined' && !participant.isFakeParticipant && !participant.local) {
|
||||
isScreenSharing = videoTrack?.videoType === 'desktop';
|
||||
connectionStatus = participant.connectionStatus;
|
||||
}
|
||||
|
||||
return {
|
||||
isCurrentlyOnLargeVideo: this.isCurrentlyOnLargeVideo(),
|
||||
isHovered: this._isHovered(),
|
||||
isAudioOnly: APP.conference.isAudioOnly(),
|
||||
tileViewActive: shouldDisplayTileView(state),
|
||||
isVideoPlayable: this.isVideoPlayable(),
|
||||
hasVideo: Boolean(this.selectVideoElement().length),
|
||||
connectionStatus,
|
||||
canPlayEventReceived: this._canPlayEventReceived,
|
||||
videoStream: Boolean(videoTrack),
|
||||
isScreenSharing,
|
||||
videoStreamMuted: videoTrack ? videoTrack.muted : 'no stream'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether current video is considered hovered. Currently it is hovered
|
||||
* if the mouse is over the video, or if the connection
|
||||
* indicator is shown(hovered).
|
||||
* @private
|
||||
*/
|
||||
_isHovered() {
|
||||
return this.videoIsHovered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the css classes of the thumbnail based on the current state.
|
||||
*/
|
||||
updateView() {
|
||||
this.$container.removeClass((index, classNames) =>
|
||||
classNames.split(' ').filter(name => name.startsWith('display-')));
|
||||
|
||||
const oldDisplayMode = this.displayMode;
|
||||
let displayModeString = '';
|
||||
|
||||
const displayModeInput = this.computeDisplayModeInput();
|
||||
|
||||
// Determine whether video, avatar or blackness should be displayed
|
||||
this.displayMode = this.selectDisplayMode(displayModeInput);
|
||||
|
||||
switch (this.displayMode) {
|
||||
case DISPLAY_AVATAR_WITH_NAME:
|
||||
displayModeString = 'avatar-with-name';
|
||||
this.$container.addClass('display-avatar-with-name');
|
||||
break;
|
||||
case DISPLAY_BLACKNESS_WITH_NAME:
|
||||
displayModeString = 'blackness-with-name';
|
||||
this.$container.addClass('display-name-on-black');
|
||||
break;
|
||||
case DISPLAY_VIDEO:
|
||||
displayModeString = 'video';
|
||||
this.$container.addClass('display-video');
|
||||
break;
|
||||
case DISPLAY_VIDEO_WITH_NAME:
|
||||
displayModeString = 'video-with-name';
|
||||
this.$container.addClass('display-name-on-video');
|
||||
break;
|
||||
case DISPLAY_AVATAR:
|
||||
default:
|
||||
displayModeString = 'avatar';
|
||||
this.$container.addClass('display-avatar-only');
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.displayMode !== oldDisplayMode) {
|
||||
logger.debug(`Displaying ${displayModeString} for ${this.id}, data: [${JSON.stringify(displayModeInput)}]`);
|
||||
}
|
||||
|
||||
if (this.displayMode !== DISPLAY_VIDEO
|
||||
&& this.displayMode !== DISPLAY_VIDEO_WITH_NAME
|
||||
&& displayModeInput.tileViewActive
|
||||
&& displayModeInput.isScreenSharing
|
||||
&& !displayModeInput.isAudioOnly) {
|
||||
// send the event
|
||||
sendAnalytics(createScreenSharingIssueEvent({
|
||||
source: 'thumbnail',
|
||||
...displayModeInput
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows or hides the dominant speaker indicator.
|
||||
* @param show whether to show or hide.
|
||||
*/
|
||||
showDominantSpeakerIndicator(show) {
|
||||
// Don't create and show dominant speaker indicator if
|
||||
// DISABLE_DOMINANT_SPEAKER_INDICATOR is true
|
||||
if (interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.container) {
|
||||
logger.warn(`Unable to set dominant speaker indicator - ${this.videoSpanId} does not exist`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.$container.toggleClass('active-speaker', show);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initalizes any browser specific properties. Currently sets the overflow
|
||||
* property for Qt browsers on Windows to hidden, thus fixing the following
|
||||
* problem:
|
||||
* Some browsers don't have full support of the object-fit property for the
|
||||
* video element and when we set video object-fit to "cover" the video
|
||||
* actually overflows the boundaries of its container, so it's important
|
||||
* to indicate that the "overflow" should be hidden.
|
||||
*
|
||||
* Setting this property for all browsers will result in broken audio levels,
|
||||
* which makes this a temporary solution, before reworking audio levels.
|
||||
*/
|
||||
initBrowserSpecificProperties() {
|
||||
const userAgent = window.navigator.userAgent;
|
||||
|
||||
if (userAgent.indexOf('QtWebEngine') > -1
|
||||
&& (userAgent.indexOf('Windows') > -1 || userAgent.indexOf('Linux') > -1)) {
|
||||
this.$container.css('overflow', 'hidden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up components on {@code SmallVideo} and removes itself from the DOM.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
remove() {
|
||||
logger.log('Remove thumbnail', this.id);
|
||||
this._unmountThumbnail();
|
||||
|
||||
// Remove whole container
|
||||
if (this.container.parentNode) {
|
||||
this.container.parentNode.removeChild(this.container);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for re-rendering multiple react components of the small
|
||||
* video.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
rerender() {
|
||||
this.updateView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked when the thumbnail is clicked and potentially trigger
|
||||
* pinning of the participant.
|
||||
*
|
||||
* @param {MouseEvent} event - The click event to intercept.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onContainerClick(event) {
|
||||
const triggerPin = this._shouldTriggerPin(event);
|
||||
|
||||
if (event.stopPropagation && triggerPin) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
if (triggerPin) {
|
||||
this.togglePin();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not a click event is targeted at certain elements which
|
||||
* should not trigger a pin.
|
||||
*
|
||||
* @param {MouseEvent} event - The click event to intercept.
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_shouldTriggerPin(event) {
|
||||
// TODO Checking the classes is a workround to allow events to bubble into
|
||||
// the DisplayName component if it was clicked. React's synthetic events
|
||||
// will fire after jQuery handlers execute, so stop propogation at this
|
||||
// point will prevent DisplayName from getting click events. This workaround
|
||||
// should be removeable once LocalVideo is a React Component because then
|
||||
// the components share the same eventing system.
|
||||
const $source = $(event.target || event.srcElement);
|
||||
|
||||
return $source.parents('.displayNameContainer').length === 0
|
||||
&& $source.parents('.popover').length === 0
|
||||
&& !event.target.classList.contains('popover');
|
||||
}
|
||||
|
||||
/**
|
||||
* Pins the participant displayed by this thumbnail or unpins if already pinned.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
togglePin() {
|
||||
const pinnedParticipant = getPinnedParticipant(APP.store.getState()) || {};
|
||||
const participantIdToPin = pinnedParticipant && pinnedParticipant.id === this.id ? null : this.id;
|
||||
|
||||
APP.store.dispatch(pinParticipant(participantIdToPin));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmounts the thumbnail.
|
||||
*/
|
||||
_unmountThumbnail() {
|
||||
ReactDOM.unmountComponentAtNode(this.container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the size of the thumbnail.
|
||||
*/
|
||||
_setThumbnailSize() {
|
||||
const layout = getCurrentLayout(APP.store.getState());
|
||||
const heightToWidthPercent = 100
|
||||
/ (this.isLocal ? interfaceConfig.LOCAL_THUMBNAIL_RATIO : interfaceConfig.REMOTE_THUMBNAIL_RATIO);
|
||||
|
||||
switch (layout) {
|
||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
|
||||
this.$container.css('padding-top', `${heightToWidthPercent}%`);
|
||||
break;
|
||||
}
|
||||
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
|
||||
const state = APP.store.getState();
|
||||
const { local, remote } = state['features/filmstrip'].horizontalViewDimensions;
|
||||
const size = this.isLocal ? local : remote;
|
||||
|
||||
if (typeof size !== 'undefined') {
|
||||
const { height, width } = size;
|
||||
|
||||
this.$container.css({
|
||||
height: `${height}px`,
|
||||
'min-height': `${height}px`,
|
||||
'min-width': `${width}px`,
|
||||
width: `${width}px`
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LAYOUTS.TILE_VIEW: {
|
||||
const state = APP.store.getState();
|
||||
const { thumbnailSize } = state['features/filmstrip'].tileViewDimensions;
|
||||
|
||||
if (typeof thumbnailSize !== 'undefined') {
|
||||
const { height, width } = thumbnailSize;
|
||||
|
||||
this.$container.css({
|
||||
height: `${height}px`,
|
||||
'min-height': `${height}px`,
|
||||
'min-width': `${width}px`,
|
||||
width: `${width}px`
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import { isTestModeEnabled } from '../../../react/features/base/testing';
|
||||
import { ORIENTATION, LargeVideoBackground, updateLastLargeVideoMediaEvent } from '../../../react/features/large-video';
|
||||
import { LAYOUTS, getCurrentLayout } from '../../../react/features/video-layout';
|
||||
/* eslint-enable no-unused-vars */
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
import UIUtil from '../util/UIUtil';
|
||||
|
||||
import Filmstrip from './Filmstrip';
|
||||
@@ -46,7 +45,7 @@ function computeDesktopVideoSize( // eslint-disable-line max-params
|
||||
videoSpaceWidth,
|
||||
videoSpaceHeight) {
|
||||
if (videoWidth === 0 || videoHeight === 0 || videoSpaceWidth === 0 || videoSpaceHeight === 0) {
|
||||
// Avoid NaN values caused by devision by 0.
|
||||
// Avoid NaN values caused by division by 0.
|
||||
return [ 0, 0 ];
|
||||
}
|
||||
|
||||
@@ -94,7 +93,7 @@ function computeCameraVideoSize( // eslint-disable-line max-params
|
||||
videoSpaceHeight,
|
||||
videoLayoutFit) {
|
||||
if (videoWidth === 0 || videoHeight === 0 || videoSpaceWidth === 0 || videoSpaceHeight === 0) {
|
||||
// Avoid NaN values caused by devision by 0.
|
||||
// Avoid NaN values caused by division by 0.
|
||||
return [ 0, 0 ];
|
||||
}
|
||||
|
||||
@@ -187,16 +186,13 @@ export class VideoContainer extends LargeContainer {
|
||||
* Creates new VideoContainer instance.
|
||||
* @param resizeContainer {Function} function that takes care of the size
|
||||
* of the video container.
|
||||
* @param emitter {EventEmitter} the event emitter that will be used by
|
||||
* this instance.
|
||||
*/
|
||||
constructor(resizeContainer, emitter) {
|
||||
constructor(resizeContainer) {
|
||||
super();
|
||||
this.stream = null;
|
||||
this.userId = null;
|
||||
this.videoType = null;
|
||||
this.localFlipX = true;
|
||||
this.emitter = emitter;
|
||||
this.resizeContainer = resizeContainer;
|
||||
|
||||
/**
|
||||
@@ -411,7 +407,7 @@ export class VideoContainer extends LargeContainer {
|
||||
const [ width, height ] = this._getVideoSize(containerWidth, containerHeight);
|
||||
|
||||
if (width === 0 || height === 0) {
|
||||
// We don't need to set 0 for width or height since the visibility is controled by the visibility css prop
|
||||
// We don't need to set 0 for width or height since the visibility is controlled by the visibility css prop
|
||||
// on the largeVideoElementsContainer. Also if the width/height of the video element is 0 the attached
|
||||
// stream won't be played. Normally if we attach a new stream we won't resize the video element until the
|
||||
// stream has been played. But setting width/height to 0 will prevent the video from playing.
|
||||
@@ -492,7 +488,7 @@ export class VideoContainer extends LargeContainer {
|
||||
|
||||
stream.attach(this.$video[0]);
|
||||
|
||||
const flipX = stream.isLocal() && this.localFlipX;
|
||||
const flipX = stream.isLocal() && this.localFlipX && !this.isScreenSharing();
|
||||
|
||||
this.$video.css({
|
||||
transform: flipX ? 'scaleX(-1)' : 'none'
|
||||
@@ -534,7 +530,6 @@ export class VideoContainer extends LargeContainer {
|
||||
this.$avatar.css('visibility', show ? 'visible' : 'hidden');
|
||||
this.avatarDisplayed = show;
|
||||
|
||||
this.emitter.emit(UIEvents.LARGE_VIDEO_AVATAR_VISIBLE, show);
|
||||
APP.API.notifyLargeVideoVisibilityChanged(show);
|
||||
}
|
||||
|
||||
@@ -610,7 +605,7 @@ export class VideoContainer extends LargeContainer {
|
||||
// explicitly disabled.
|
||||
if (interfaceConfig.DISABLE_VIDEO_BACKGROUND
|
||||
|| browser.isFirefox()
|
||||
|| browser.isSafari()) {
|
||||
|| browser.isWebKitBased()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,88 +4,29 @@ import Logger from 'jitsi-meet-logger';
|
||||
|
||||
import { MEDIA_TYPE, VIDEO_TYPE } from '../../../react/features/base/media';
|
||||
import {
|
||||
getLocalParticipant as getLocalParticipantFromStore,
|
||||
getPinnedParticipant,
|
||||
getParticipantById,
|
||||
pinParticipant
|
||||
getParticipantById
|
||||
} from '../../../react/features/base/participants';
|
||||
import { getTrackByMediaTypeAndParticipant } from '../../../react/features/base/tracks';
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
import { SHARED_VIDEO_CONTAINER_TYPE } from '../shared_video/SharedVideo';
|
||||
import SharedVideoThumb from '../shared_video/SharedVideoThumb';
|
||||
|
||||
import LargeVideoManager from './LargeVideoManager';
|
||||
import LocalVideo from './LocalVideo';
|
||||
import RemoteVideo from './RemoteVideo';
|
||||
import { VIDEO_CONTAINER_TYPE } from './VideoContainer';
|
||||
|
||||
const logger = Logger.getLogger(__filename);
|
||||
|
||||
const remoteVideos = {};
|
||||
let localVideoThumbnail = null;
|
||||
|
||||
let eventEmitter = null;
|
||||
|
||||
let largeVideo;
|
||||
|
||||
/**
|
||||
* flipX state of the localVideo
|
||||
*/
|
||||
let localFlipX = null;
|
||||
|
||||
/**
|
||||
* Handler for local flip X changed event.
|
||||
* @param {Object} val
|
||||
*/
|
||||
function onLocalFlipXChanged(val) {
|
||||
localFlipX = val;
|
||||
if (largeVideo) {
|
||||
largeVideo.onLocalFlipXChange(val);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all thumbnails in the filmstrip.
|
||||
*
|
||||
* @private
|
||||
* @returns {Array}
|
||||
*/
|
||||
function getAllThumbnails() {
|
||||
return [
|
||||
...localVideoThumbnail ? [ localVideoThumbnail ] : [],
|
||||
...Object.values(remoteVideos)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Private helper to get the redux representation of the local participant.
|
||||
*
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
function getLocalParticipant() {
|
||||
return getLocalParticipantFromStore(APP.store.getState());
|
||||
}
|
||||
|
||||
const VideoLayout = {
|
||||
init(emitter) {
|
||||
eventEmitter = emitter;
|
||||
|
||||
localVideoThumbnail = new LocalVideo(
|
||||
emitter,
|
||||
this._updateLargeVideoIfDisplayed.bind(this));
|
||||
|
||||
this.registerListeners();
|
||||
},
|
||||
|
||||
/**
|
||||
* Registering listeners for UI events in Video layout component.
|
||||
*
|
||||
* @returns {void}
|
||||
* Handler for local flip X changed event.
|
||||
*/
|
||||
registerListeners() {
|
||||
eventEmitter.addListener(UIEvents.LOCAL_FLIPX_CHANGED,
|
||||
onLocalFlipXChanged);
|
||||
onLocalFlipXChanged() {
|
||||
if (largeVideo) {
|
||||
const { store } = APP;
|
||||
const { localFlipX } = store.getState()['features/base/settings'];
|
||||
|
||||
largeVideo.onLocalFlipXChange(localFlipX);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -95,14 +36,17 @@ const VideoLayout = {
|
||||
*/
|
||||
reset() {
|
||||
this._resetLargeVideo();
|
||||
this._resetFilmstrip();
|
||||
},
|
||||
|
||||
initLargeVideo() {
|
||||
this._resetLargeVideo();
|
||||
|
||||
largeVideo = new LargeVideoManager(eventEmitter);
|
||||
if (localFlipX) {
|
||||
largeVideo = new LargeVideoManager();
|
||||
|
||||
const { store } = APP;
|
||||
const { localFlipX } = store.getState()['features/base/settings'];
|
||||
|
||||
if (typeof localFlipX === 'boolean') {
|
||||
largeVideo.onLocalFlipXChange(localFlipX);
|
||||
}
|
||||
largeVideo.updateContainerSize();
|
||||
@@ -120,55 +64,6 @@ const VideoLayout = {
|
||||
}
|
||||
},
|
||||
|
||||
changeLocalVideo(stream) {
|
||||
const localId = getLocalParticipant().id;
|
||||
|
||||
this.onVideoTypeChanged(localId, stream.videoType);
|
||||
|
||||
localVideoThumbnail.changeVideo(stream);
|
||||
|
||||
this._updateLargeVideoIfDisplayed(localId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows/hides local video.
|
||||
* @param {boolean} true to make the local video visible, false - otherwise
|
||||
*/
|
||||
setLocalVideoVisible(visible) {
|
||||
localVideoThumbnail.setVisible(visible);
|
||||
},
|
||||
|
||||
onRemoteStreamAdded(stream) {
|
||||
const id = stream.getParticipantId();
|
||||
const remoteVideo = remoteVideos[id];
|
||||
|
||||
logger.debug(`Received a new ${stream.getType()} stream for ${id}`);
|
||||
|
||||
if (!remoteVideo) {
|
||||
logger.debug('No remote video element to add stream');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
remoteVideo.addRemoteStreamElement(stream);
|
||||
|
||||
this.onVideoMute(id);
|
||||
remoteVideo.updateView();
|
||||
},
|
||||
|
||||
onRemoteStreamRemoved(stream) {
|
||||
const id = stream.getParticipantId();
|
||||
const remoteVideo = remoteVideos[id];
|
||||
|
||||
// Remote stream may be removed after participant left the conference.
|
||||
if (remoteVideo) {
|
||||
remoteVideo.removeRemoteStreamElement(stream);
|
||||
remoteVideo.updateView();
|
||||
}
|
||||
|
||||
this.updateVideoMutedForNoTracks(id);
|
||||
},
|
||||
|
||||
/**
|
||||
* FIXME get rid of this method once muted indicator are reactified (by
|
||||
* making sure that user with no tracks is displayed as muted )
|
||||
@@ -180,7 +75,7 @@ const VideoLayout = {
|
||||
const participant = APP.conference.getParticipantById(participantId);
|
||||
|
||||
if (participant && !participant.getTracksByMediaType('video').length) {
|
||||
APP.UI.setVideoMuted(participantId);
|
||||
VideoLayout._updateLargeVideoIfDisplayed(participantId, true);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -202,110 +97,12 @@ const VideoLayout = {
|
||||
return videoTrack?.videoType;
|
||||
},
|
||||
|
||||
isPinned(id) {
|
||||
return id === this.getPinnedId();
|
||||
},
|
||||
|
||||
getPinnedId() {
|
||||
const { id } = getPinnedParticipant(APP.store.getState()) || {};
|
||||
|
||||
return id || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Triggers a thumbnail to pin or unpin itself.
|
||||
*
|
||||
* @param {number} videoNumber - The index of the video to toggle pin on.
|
||||
* @private
|
||||
*/
|
||||
togglePin(videoNumber) {
|
||||
const videos = getAllThumbnails();
|
||||
const videoView = videos[videoNumber];
|
||||
|
||||
videoView && videoView.togglePin();
|
||||
},
|
||||
|
||||
/**
|
||||
* Callback invoked to update display when the pin participant has changed.
|
||||
*
|
||||
* @paramn {string|null} pinnedParticipantID - The participant ID of the
|
||||
* participant that is pinned or null if no one is pinned.
|
||||
* @returns {void}
|
||||
*/
|
||||
onPinChange(pinnedParticipantID) {
|
||||
getAllThumbnails().forEach(thumbnail =>
|
||||
thumbnail.focus(pinnedParticipantID === thumbnail.getId()));
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a participant container for the given id.
|
||||
*
|
||||
* @param {Object} participant - The redux representation of a remote
|
||||
* participant.
|
||||
* @returns {void}
|
||||
*/
|
||||
addRemoteParticipantContainer(participant) {
|
||||
if (!participant || participant.local) {
|
||||
return;
|
||||
} else if (participant.isFakeParticipant) {
|
||||
const sharedVideoThumb = new SharedVideoThumb(participant);
|
||||
|
||||
this.addRemoteVideoContainer(participant.id, sharedVideoThumb);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const id = participant.id;
|
||||
const jitsiParticipant = APP.conference.getParticipantById(id);
|
||||
const remoteVideo = new RemoteVideo(jitsiParticipant);
|
||||
|
||||
this.addRemoteVideoContainer(id, remoteVideo);
|
||||
this.updateVideoMutedForNoTracks(id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds remote video container for the given id and <tt>SmallVideo</tt>.
|
||||
*
|
||||
* @param {string} the id of the video to add
|
||||
* @param {SmallVideo} smallVideo the small video instance to add as a
|
||||
* remote video
|
||||
*/
|
||||
addRemoteVideoContainer(id, remoteVideo) {
|
||||
remoteVideos[id] = remoteVideo;
|
||||
|
||||
// Initialize the view
|
||||
remoteVideo.updateView();
|
||||
},
|
||||
|
||||
/**
|
||||
* On video muted event.
|
||||
*/
|
||||
onVideoMute(id) {
|
||||
if (APP.conference.isLocalId(id)) {
|
||||
localVideoThumbnail && localVideoThumbnail.updateView();
|
||||
} else {
|
||||
const remoteVideo = remoteVideos[id];
|
||||
|
||||
if (remoteVideo) {
|
||||
remoteVideo.updateView();
|
||||
}
|
||||
}
|
||||
|
||||
// large video will show avatar instead of muted stream
|
||||
this._updateLargeVideoIfDisplayed(id, true);
|
||||
},
|
||||
|
||||
/**
|
||||
* On dominant speaker changed event.
|
||||
*
|
||||
* @param {string} id - The participant ID of the new dominant speaker.
|
||||
* @returns {void}
|
||||
*/
|
||||
onDominantSpeakerChanged(id) {
|
||||
getAllThumbnails().forEach(thumbnail =>
|
||||
thumbnail.showDominantSpeakerIndicator(id === thumbnail.getId()));
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows/hides warning about a user's connectivity issues.
|
||||
*
|
||||
@@ -321,12 +118,6 @@ const VideoLayout = {
|
||||
// We have to trigger full large video update to transition from
|
||||
// avatar to video on connectivity restored.
|
||||
this._updateLargeVideoIfDisplayed(id, true);
|
||||
|
||||
const remoteVideo = remoteVideos[id];
|
||||
|
||||
if (remoteVideo) {
|
||||
remoteVideo.updateView();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -339,58 +130,14 @@ const VideoLayout = {
|
||||
*/
|
||||
onLastNEndpointsChanged(endpointsLeavingLastN, endpointsEnteringLastN) {
|
||||
if (endpointsLeavingLastN) {
|
||||
endpointsLeavingLastN.forEach(this._updateRemoteVideo, this);
|
||||
endpointsLeavingLastN.forEach(this._updateLargeVideoIfDisplayed, this);
|
||||
}
|
||||
|
||||
if (endpointsEnteringLastN) {
|
||||
endpointsEnteringLastN.forEach(this._updateRemoteVideo, this);
|
||||
endpointsEnteringLastN.forEach(this._updateLargeVideoIfDisplayed, this);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates remote video by id if it exists.
|
||||
* @param {string} id of the remote video
|
||||
* @private
|
||||
*/
|
||||
_updateRemoteVideo(id) {
|
||||
const remoteVideo = remoteVideos[id];
|
||||
|
||||
if (remoteVideo) {
|
||||
remoteVideo.updateView();
|
||||
this._updateLargeVideoIfDisplayed(id);
|
||||
}
|
||||
},
|
||||
|
||||
removeParticipantContainer(id) {
|
||||
// Unlock large video
|
||||
if (this.getPinnedId() === id) {
|
||||
logger.info('Focused video owner has left the conference');
|
||||
APP.store.dispatch(pinParticipant(null));
|
||||
}
|
||||
|
||||
const remoteVideo = remoteVideos[id];
|
||||
|
||||
if (remoteVideo) {
|
||||
// Remove remote video
|
||||
logger.info(`Removing remote video: ${id}`);
|
||||
delete remoteVideos[id];
|
||||
remoteVideo.remove();
|
||||
} else {
|
||||
logger.warn(`No remote video for ${id}`);
|
||||
}
|
||||
},
|
||||
|
||||
onVideoTypeChanged(id, newVideoType) {
|
||||
const remoteVideo = remoteVideos[id];
|
||||
|
||||
if (!remoteVideo) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('Peer video type changed: ', id, newVideoType);
|
||||
remoteVideo.updateView();
|
||||
},
|
||||
|
||||
/**
|
||||
* Resizes the video area.
|
||||
*/
|
||||
@@ -401,15 +148,6 @@ const VideoLayout = {
|
||||
}
|
||||
},
|
||||
|
||||
getSmallVideo(id) {
|
||||
if (APP.conference.isLocalId(id)) {
|
||||
return localVideoThumbnail;
|
||||
}
|
||||
|
||||
return remoteVideos[id];
|
||||
|
||||
},
|
||||
|
||||
changeUserAvatar(id, avatarUrl) {
|
||||
if (this.isCurrentlyOnLarge(id)) {
|
||||
largeVideo.updateAvatar(avatarUrl);
|
||||
@@ -432,24 +170,6 @@ const VideoLayout = {
|
||||
return largeVideo && largeVideo.id === id;
|
||||
},
|
||||
|
||||
/**
|
||||
* Triggers an update of remote video and large video displays so they may
|
||||
* pick up any state changes that have occurred elsewhere.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
updateAllVideos() {
|
||||
const displayedUserId = this.getLargeVideoID();
|
||||
|
||||
if (displayedUserId) {
|
||||
this.updateLargeVideo(displayedUserId, true);
|
||||
}
|
||||
|
||||
Object.keys(remoteVideos).forEach(video => {
|
||||
remoteVideos[video].updateView();
|
||||
});
|
||||
},
|
||||
|
||||
updateLargeVideo(id, forceUpdate) {
|
||||
if (!largeVideo) {
|
||||
return;
|
||||
@@ -510,13 +230,6 @@ const VideoLayout = {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const currentId = largeVideo.id;
|
||||
let oldSmallVideo;
|
||||
|
||||
if (currentId) {
|
||||
oldSmallVideo = this.getSmallVideo(currentId);
|
||||
}
|
||||
|
||||
let containerTypeToShow = type;
|
||||
|
||||
// if we are hiding a container and there is focusedVideo
|
||||
@@ -533,12 +246,7 @@ const VideoLayout = {
|
||||
}
|
||||
}
|
||||
|
||||
return largeVideo.showContainer(containerTypeToShow)
|
||||
.then(() => {
|
||||
if (oldSmallVideo) {
|
||||
oldSmallVideo && oldSmallVideo.updateView();
|
||||
}
|
||||
});
|
||||
return largeVideo.showContainer(containerTypeToShow);
|
||||
},
|
||||
|
||||
isLargeContainerTypeVisible(type) {
|
||||
@@ -561,14 +269,6 @@ const VideoLayout = {
|
||||
return largeVideo;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the flipX state of the local video.
|
||||
* @param {boolean} true for flipped otherwise false;
|
||||
*/
|
||||
setLocalFlipX(val) {
|
||||
this.localFlipX = val;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the wrapper jquery selector for the largeVideo
|
||||
* @returns {JQuerySelector} the wrapper jquery selector for the largeVideo
|
||||
@@ -577,15 +277,6 @@ const VideoLayout = {
|
||||
return this.getCurrentlyOnLargeContainer().$wrapper;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the number of remove video ids.
|
||||
*
|
||||
* @returns {number} The number of remote videos.
|
||||
*/
|
||||
getRemoteVideosCount() {
|
||||
return Object.keys(remoteVideos).length;
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper method to invoke when the video layout has changed and elements
|
||||
* have to be re-arranged and resized.
|
||||
@@ -593,12 +284,7 @@ const VideoLayout = {
|
||||
* @returns {void}
|
||||
*/
|
||||
refreshLayout() {
|
||||
localVideoThumbnail && localVideoThumbnail.updateDOMLocation();
|
||||
VideoLayout.resizeVideoArea();
|
||||
|
||||
// Rerender the thumbnails since they are dependant on the layout because of the tooltip positioning.
|
||||
localVideoThumbnail && localVideoThumbnail.rerender();
|
||||
Object.values(remoteVideos).forEach(remoteVideoThumbnail => remoteVideoThumbnail.rerender());
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -615,26 +301,6 @@ const VideoLayout = {
|
||||
largeVideo = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Cleans up filmstrip state. While a separate {@code Filmstrip} exists, its
|
||||
* implementation is mainly for querying and manipulating the DOM while
|
||||
* state mostly remains in {@code VideoLayout}.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_resetFilmstrip() {
|
||||
Object.keys(remoteVideos).forEach(remoteVideoId => {
|
||||
this.removeParticipantContainer(remoteVideoId);
|
||||
delete remoteVideos[remoteVideoId];
|
||||
});
|
||||
|
||||
if (localVideoThumbnail) {
|
||||
localVideoThumbnail.remove();
|
||||
localVideoThumbnail = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Triggers an update of large video if the passed in participant is
|
||||
* currently displayed on large video.
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
sendAnalytics
|
||||
} from '../../react/features/analytics';
|
||||
import { toggleDialog } from '../../react/features/base/dialog';
|
||||
import { clickOnVideo } from '../../react/features/filmstrip/actions';
|
||||
import { KeyboardShortcutsDialog }
|
||||
from '../../react/features/keyboard-shortcuts';
|
||||
import { SpeakerStats } from '../../react/features/speaker-stats';
|
||||
@@ -54,7 +55,7 @@ const KeyboardShortcut = {
|
||||
if (_shortcuts.has(key)) {
|
||||
_shortcuts.get(key).function(e);
|
||||
} else if (!isNaN(num) && num >= 0 && num <= 9) {
|
||||
APP.UI.clickOnVideo(num);
|
||||
APP.store.dispatch(clickOnVideo(num));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -46,7 +46,14 @@ export class TaskQueue {
|
||||
this._currentTask = this._queue.shift() || null;
|
||||
|
||||
if (this._currentTask) {
|
||||
this._currentTask(this._onTaskComplete);
|
||||
logger.debug('Executing a task.');
|
||||
|
||||
try {
|
||||
this._currentTask(this._onTaskComplete);
|
||||
} catch (error) {
|
||||
logger.error(`Task execution failed: ${error}`);
|
||||
this._onTaskComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +65,7 @@ export class TaskQueue {
|
||||
*/
|
||||
_onTaskComplete() {
|
||||
this._currentTask = null;
|
||||
logger.debug('Task completed.');
|
||||
this._executeNext();
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user