Merge remote-tracking branch 'remotes/upstream/v2.6.x-release' into tldrawpdfexport

This commit is contained in:
Daniel Petri Rocha 2022-07-09 13:06:57 +02:00
commit 283cd6be67
81 changed files with 1343 additions and 321 deletions

View File

@ -16,6 +16,7 @@ jobs:
- run: ./build/setup.sh bbb-apps-akka
- run: ./build/setup.sh bbb-config
- run: ./build/setup.sh bbb-etherpad
- run: ./build/setup.sh bbb-export-annotations
- run: ./build/setup.sh bbb-freeswitch-core
- run: ./build/setup.sh bbb-freeswitch-sounds
- run: ./build/setup.sh bbb-fsesl-akka
@ -109,8 +110,8 @@ jobs:
- name: Install BBB
run: |
sudo sh -c '
cd /root/ && wget -q https://ubuntu.bigbluebutton.org/bbb-install-2.5.sh -O bbb-install.sh
cat bbb-install.sh | sed "s|> /etc/apt/sources.list.d/bigbluebutton.list||g" | bash -s -- -v focal-25-dev -s bbb-ci.test -d /certs/
cd /root/ && wget -q https://ubuntu.bigbluebutton.org/bbb-install-2.6.sh -O bbb-install.sh
cat bbb-install.sh | sed "s|> /etc/apt/sources.list.d/bigbluebutton.list||g" | bash -s -- -v focal-26-dev -s bbb-ci.test -d /certs/
bbb-conf --salt bbbci
echo "NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/bbb-dev/bbb-dev-ca.crt" >> /usr/share/meteor/bundle/bbb-html5-with-roles.conf
bbb-conf --restart
@ -137,4 +138,4 @@ jobs:
name: tests-report
path: |
bigbluebutton-tests/playwright/playwright-report
bigbluebutton-tests/playwright/test-results
bigbluebutton-tests/playwright/test-results

View File

@ -0,0 +1,65 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.core.record.events
import org.bigbluebutton.common2.domain.SimpleVoteOutVO
import scala.collection.immutable.List
import scala.collection.Map
import scala.collection.mutable.ArrayBuffer
import spray.json._
import DefaultJsonProtocol._
class AddTldrawShapeWhiteboardRecordEvent extends AbstractWhiteboardRecordEvent {
import AddTldrawShapeWhiteboardRecordEvent._
implicit object AnyJsonFormat extends JsonFormat[Any] {
def write(x: Any) = x match {
case n: Int => JsNumber(n)
case s: String => JsString(s)
case d: Double => JsNumber(d)
case m: scala.collection.immutable.Map[String, _] => mapFormat[String, Any].write(m)
case l: List[_] => listFormat[Any].write(l)
case b: Boolean if b == true => JsTrue
case b: Boolean if b == false => JsFalse
}
def read(value: JsValue) = {}
}
setEvent("AddTldrawShapeEvent")
def setUserId(id: String) {
eventMap.put(USER_ID, id)
}
def setAnnotationId(id: String) {
eventMap.put(SHAPE_ID, id)
}
def addAnnotation(annotation: scala.collection.immutable.Map[String, Any]) {
eventMap.put(SHAPE_DATA, annotation.toJson.compactPrint)
}
}
object AddTldrawShapeWhiteboardRecordEvent {
protected final val USER_ID = "userId"
protected final val SHAPE_ID = "shapeId"
protected final val SHAPE_DATA = "shapeData"
}

View File

@ -0,0 +1,39 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.core.record.events
class DeleteTldrawShapeRecordEvent extends AbstractWhiteboardRecordEvent {
import DeleteTldrawShapeRecordEvent._
setEvent("DeleteTldrawShapeEvent")
def setUserId(userId: String) {
eventMap.put(USER_ID, userId)
}
def setShapeId(shapeId: String) {
eventMap.put(SHAPE_ID, shapeId)
}
}
object DeleteTldrawShapeRecordEvent {
protected final val USER_ID = "userId"
protected final val SHAPE_ID = "shapeId"
}

View File

@ -32,23 +32,28 @@ class ResizeAndMoveSlideRecordEvent extends AbstractPresentationRecordEvent {
eventMap.put(ID, id)
}
def setXCamera(xCamera: Double) {
eventMap.put(X_CAMERA, xCamera.toString)
def setXOffset(offset: Double) {
eventMap.put(X_OFFSET, offset.toString)
}
def setYCamera(yCamera: Double) {
eventMap.put(Y_CAMERA, yCamera.toString)
def setYOffset(offset: Double) {
eventMap.put(Y_OFFSET, offset.toString)
}
def setZoom(zoom: Double) {
eventMap.put(ZOOM, zoom.toString)
def setWidthRatio(ratio: Double) {
eventMap.put(WIDTH_RATIO, ratio.toString)
}
def setHeightRatio(ratio: Double) {
eventMap.put(HEIGHT_RATIO, ratio.toString)
}
}
object ResizeAndMoveSlideRecordEvent {
protected final val PRES_NAME = "presentationName"
protected final val ID = "id"
protected final val X_CAMERA = "xCamera"
protected final val Y_CAMERA = "yCamera"
protected final val ZOOM = "zoom"
protected final val X_OFFSET = "xOffset"
protected final val Y_OFFSET = "yOffset"
protected final val WIDTH_RATIO = "widthRatio"
protected final val HEIGHT_RATIO = "heightRatio"
}

View File

@ -0,0 +1,54 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2017 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.core.record.events
class TldrawCameraChangedRecordEvent extends AbstractPresentationRecordEvent {
import TldrawCameraChangedRecordEvent._
setEvent("TldrawCameraChangedEvent")
def setPresentationName(name: String) {
eventMap.put(PRES_NAME, name)
}
def setId(id: String) {
eventMap.put(ID, id)
}
def setXCamera(xCamera: Double) {
eventMap.put(X_CAMERA, xCamera.toString)
}
def setYCamera(yCamera: Double) {
eventMap.put(Y_CAMERA, yCamera.toString)
}
def setZoom(zoom: Double) {
eventMap.put(ZOOM, zoom.toString)
}
}
object TldrawCameraChangedRecordEvent {
protected final val PRES_NAME = "presentationName"
protected final val ID = "id"
protected final val X_CAMERA = "xCamera"
protected final val Y_CAMERA = "yCamera"
protected final val ZOOM = "zoom"
}

View File

@ -184,7 +184,7 @@ class RedisRecorderActor(
}
private def handleResizeAndMovePageEvtMsg(msg: ResizeAndMovePageEvtMsg) {
val ev = new ResizeAndMoveSlideRecordEvent()
val ev = new TldrawCameraChangedRecordEvent()
ev.setMeetingId(msg.header.meetingId)
ev.setPodId(msg.body.podId)
ev.setPresentationName(msg.body.presentationId)
@ -280,7 +280,7 @@ class RedisRecorderActor(
private def handleSendWhiteboardAnnotationsEvtMsg(msg: SendWhiteboardAnnotationsEvtMsg) {
msg.body.annotations.foreach(annotation => {
val ev = new AddShapeWhiteboardRecordEvent()
val ev = new AddTldrawShapeWhiteboardRecordEvent()
ev.setMeetingId(msg.header.meetingId)
ev.setPresentation(getPresentationId(annotation.wbId))
ev.setPageNumber(getPageNum(annotation.wbId))
@ -320,7 +320,7 @@ class RedisRecorderActor(
private def handleDeleteWhiteboardAnnotationsEvtMsg(msg: DeleteWhiteboardAnnotationsEvtMsg) {
msg.body.annotationsIds.foreach(annotationId => {
val ev = new UndoAnnotationRecordEvent()
val ev = new DeleteTldrawShapeRecordEvent()
ev.setMeetingId(msg.header.meetingId)
ev.setPresentation(getPresentationId(msg.body.whiteboardId))
ev.setPageNumber(getPageNum(msg.body.whiteboardId))

View File

@ -3,6 +3,7 @@ Description=BigBlueButton Apps (Akka)
Requires=network.target
Wants=redis-server.service
After=redis-server.service
PartOf=bigbluebutton.target
[Service]
Type=simple
@ -22,5 +23,4 @@ PermissionsStartOnly=true
LimitNOFILE=1024
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target bigbluebutton.target

View File

@ -3,6 +3,7 @@ Description=BigBlueButton FS-ESL (Akka)
Requires=network.target
Wants=redis-server.service
After=redis-server.service
PartOf= bigbluebutton.target
[Service]
Type=simple
@ -22,5 +23,5 @@ PermissionsStartOnly=true
LimitNOFILE=1024
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target bigbluebutton.target

View File

@ -7,7 +7,7 @@ const { Worker } = require('worker_threads');
const path = require('path');
const logger = new Logger('presAnn Master');
logger.info("Running export-annotations");
logger.info("Running bbb-export-annotations");
const kickOffCollectorWorker = (jobId) => {
return new Promise((resolve, reject) => {

View File

@ -1,11 +1,11 @@
{
"name": "export-annotations",
"name": "bbb-export-annotations",
"version": "0.0.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "export-annotations",
"name": "bbb-export-annotations",
"version": "0.0.1",
"dependencies": {
"axios": "^0.26.0",

View File

@ -1,5 +1,5 @@
{
"name": "export-annotations",
"name": "bbb-export-annotations",
"version": "0.0.1",
"description": "BigBlueButton's Presentation Annotation Exporter",
"scripts": {

View File

@ -1 +1 @@
git clone --branch v2.9.0-alpha.3 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu
git clone --branch v2.9.0-alpha.4 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu

View File

@ -342,35 +342,11 @@ uncomment () {
stop_bigbluebutton () {
echo "Stopping BigBlueButton"
if [ -f /usr/lib/systemd/system/bbb-html5.service ]; then
HTML5="mongod bbb-html5 bbb-webrtc-sfu kurento-media-server"
fi
if [ -f /usr/lib/systemd/system/bbb-webhooks.service ]; then
WEBHOOKS=bbb-webhooks
fi
if [ -f /usr/lib/systemd/system/bbb-pads.service ]; then
PADS=bbb-pads
fi
if [ -f /usr/share/etherpad-lite/settings.json ]; then
ETHERPAD=etherpad
fi
if [ -f /lib/systemd/system/bbb-web.service ]; then
BBB_WEB=bbb-web
fi
if [ -f /usr/share/bbb-lti/WEB-INF/classes/lti-config.properties ]; then
BBB_LTI=bbb-lti
fi
if systemctl list-units --full -all | grep -q $TOMCAT_USER.service; then
TOMCAT_SERVICE=$TOMCAT_USER
fi
systemctl stop $TOMCAT_SERVICE nginx freeswitch $REDIS_SERVICE bbb-apps-akka bbb-fsesl-akka bbb-rap-resque-worker.service bbb-rap-starter.service bbb-rap-caption-inbox.service $HTML5 $WEBHOOKS $PADS $ETHERPAD $BBB_WEB $BBB_LTI
systemctl stop $TOMCAT_SERVICE bigbluebutton.target
}
start_bigbluebutton () {
@ -392,44 +368,24 @@ start_bigbluebutton () {
fi
fi
echo "Reloading NginX configuration"
systemctl reload nginx
echo "Starting BigBlueButton"
if [ -f /usr/lib/systemd/system/bbb-html5.service ]; then
HTML5="mongod bbb-html5 bbb-webrtc-sfu kurento-media-server"
fi
if [ -f /usr/lib/systemd/system/bbb-webhooks.service ]; then
WEBHOOKS=bbb-webhooks
fi
if [ -f /usr/lib/systemd/system/bbb-pads.service ]; then
PADS=bbb-pads
fi
if [ -f /usr/share/etherpad-lite/settings.json ]; then
ETHERPAD=etherpad
fi
if [ -f /lib/systemd/system/bbb-web.service ]; then
BBB_WEB=bbb-web
fi
if [ -f /usr/share/bbb-lti/WEB-INF/classes/lti-config.properties ]; then
BBB_LTI=bbb-lti
fi
if systemctl list-units --full -all | grep -q $TOMCAT_USER.service; then
TOMCAT_SERVICE=$TOMCAT_USER
systemctl start $TOMCAT_SERVICE || {
echo
echo "# Warning: $TOMCAT_SERVICE could not be started. Please, check BBB-LTI or BBB-Demo."
echo "# Run the command:"
echo "# sudo journalctl -u $TOMCAT_SERVICE"
echo "# To better understand the ERROR"
}
fi
systemctl start $TOMCAT_SERVICE || {
echo
echo "# Warning: $TOMCAT_SERVICE could not be started. Please, check BBB-LTI or BBB-Demo."
echo "# Run the command:"
echo "# sudo journalctl -u $TOMCAT_SERVICE"
echo "# To better understand the ERROR"
}
systemctl start nginx freeswitch $REDIS_SERVICE bbb-apps-akka bbb-fsesl-akka bbb-rap-resque-worker bbb-rap-starter.service bbb-rap-caption-inbox.service $HTML5 $WEBHOOKS $ETHERPAD $PADS $BBB_WEB $BBB_LTI
systemctl start bigbluebutton.target
if [ -f /usr/lib/systemd/system/bbb-html5.service ]; then
systemctl start mongod
@ -492,6 +448,10 @@ display_bigbluebutton_status () {
units="$units bbb-pads"
fi
if [ -f /usr/lib/systemd/system/bbb-export-annotations.service ]; then
units="$units bbb-export-annotations"
fi
if systemctl list-units --full -all | grep -q $TOMCAT_USER.service; then
TOMCAT_SERVICE=$TOMCAT_USER
fi
@ -1142,17 +1102,19 @@ check_state() {
fi
if bbb-conf --status | grep -q inactive; then
if systemctl list-units --full -all | grep -q $TOMCAT_USER.service; then
TOMCAT_SERVICE=$TOMCAT_USER
if bbb-conf --status | grep -q tomcat9; then
echo "# Warning: $TOMCAT_SERVICE is not started correctly"
echo "#"
echo "# $(bbb-conf --status | grep inactive | grep $TOMCAT_SERVICE)"
echo "#"
if bbb-conf --status | grep -q inactive | grep -q $TOMCAT_SERVICE; then
echo "# Warning: $TOMCAT_SERVICE is not started correctly"
echo "#"
fi
fi
if bbb-conf --status | grep inactive | grep -vq tomcat9; then
if bbb-conf --status | grep inactive; then
echo "# Error: Detected some processes have not started correctly"
echo "#"
echo "# $(bbb-conf --status | grep inactive | grep -v $TOMCAT_SERVICE)"
echo "# $(bbb-conf --status | grep inactive)"
echo "#"
fi
fi
@ -1721,7 +1683,7 @@ String BigBlueButtonURL = \"$BBB_WEB_URL/bigbluebutton/\";
sudo xmlstarlet edit --inplace --update 'configuration/settings//param[@name="password"]/@value' --value $ESL_PASSWORD /opt/freeswitch/etc/freeswitch/autoload_configs/event_socket.conf.xml
echo "Restarting the BigBlueButton $BIGBLUEBUTTON_RELEASE ..."
echo "Restarting BigBlueButton $BIGBLUEBUTTON_RELEASE ..."
stop_bigbluebutton
start_bigbluebutton

View File

@ -5,7 +5,7 @@
meteor-base@1.5.1
mobile-experience@1.1.0
mongo@1.14.6
mongo@1.15.0
reactive-var@1.0.11
standard-minifier-css@1.8.1

View File

@ -1 +1 @@
METEOR@2.7.1
METEOR@2.7.3

View File

@ -1,10 +1,10 @@
allow-deny@1.1.1
autoupdate@1.8.0
babel-compiler@7.9.0
babel-runtime@1.5.0
babel-runtime@1.5.1
base64@1.0.12
binary-heap@1.0.11
blaze-tools@1.1.2
blaze-tools@1.1.3
boilerplate-generator@1.7.1
caching-compiler@1.2.2
caching-html-compiler@1.2.1
@ -25,7 +25,7 @@ es5-shim@4.8.0
fetch@0.1.1
geojson-utils@1.0.10
hot-code-push@1.0.4
html-tools@1.1.2
html-tools@1.1.3
htmljs@1.1.1
http@2.0.0
id-map@1.1.1
@ -43,11 +43,11 @@ minifier-js@2.7.4
minimongo@1.8.0
mobile-experience@1.1.0
mobile-status-bar@1.1.0
modern-browsers@0.1.7
modern-browsers@0.1.8
modules@0.18.0
modules-runtime@0.13.0
mongo@1.14.6
mongo-decimal@0.1.2
mongo@1.15.0
mongo-decimal@0.1.3
mongo-dev-server@1.1.0
mongo-id@1.0.8
npm-mongo@4.3.1
@ -55,7 +55,7 @@ ordered-dict@1.1.0
promise@0.12.0
random@1.2.0
react-fast-refresh@0.2.3
react-meteor-data@2.4.0
react-meteor-data@2.5.1
reactive-dict@1.3.0
reactive-var@1.0.11
reload@1.3.1
@ -64,12 +64,12 @@ rocketchat:streamer@1.1.0
routepolicy@1.1.1
session@1.2.0
shell-server@0.5.0
socket-stream-client@0.4.0
spacebars-compiler@1.3.0
socket-stream-client@0.5.0
spacebars-compiler@1.3.1
standard-minifier-css@1.8.1
standard-minifier-js@2.8.0
static-html@1.3.2
templating-tools@1.2.1
templating-tools@1.2.2
tracker@1.2.0
typescript@4.5.4
underscore@1.0.10

View File

@ -21,6 +21,7 @@ import React from 'react';
import { Meteor } from 'meteor/meteor';
import { render } from 'react-dom';
import logger from '/imports/startup/client/logger';
import '/imports/ui/services/mobile-app';
import Base from '/imports/startup/client/base';
import JoinHandler from '/imports/ui/components/join-handler/component';
import AuthenticatedHandler from '/imports/ui/components/authenticated-handler/component';

View File

@ -15,7 +15,7 @@ import {
import { SCREENSHARING_ERRORS } from '/imports/api/screenshare/client/bridge/errors';
const { isMobile } = deviceInfo;
const { isSafari } = browserInfo;
const { isSafari, isMobileApp } = browserInfo;
const propTypes = {
intl: PropTypes.objectOf(Object).isRequired,
@ -167,7 +167,7 @@ const ScreenshareButton = ({
? intlMessages.stopDesktopShareDesc : intlMessages.desktopShareDesc;
const shouldAllowScreensharing = enabled
&& !isMobile
&& ( !isMobile || isMobileApp)
&& amIPresenter;
const dataTest = !screenshareDataSavingSetting ? 'screenshareLocked'

View File

@ -289,17 +289,24 @@ const LayoutEngine = ({ layoutType }) => {
isMobile,
isTablet,
};
const layout = document.getElementById('layout');
switch (layoutType) {
case LAYOUT_TYPE.CUSTOM_LAYOUT:
layout?.setAttribute("data-layout", LAYOUT_TYPE.CUSTOM_LAYOUT);
return <CustomLayout {...common} />;
case LAYOUT_TYPE.SMART_LAYOUT:
layout?.setAttribute("data-layout", LAYOUT_TYPE.SMART_LAYOUT);
return <SmartLayout {...common} />;
case LAYOUT_TYPE.PRESENTATION_FOCUS:
layout?.setAttribute("data-layout", LAYOUT_TYPE.PRESENTATION_FOCUS);
return <PresentationFocusLayout {...common} />;
case LAYOUT_TYPE.VIDEO_FOCUS:
layout?.setAttribute("data-layout",LAYOUT_TYPE.VIDEO_FOCUS);
return <VideoFocusLayout {...common} />;
default:
layout?.setAttribute("data-layout", LAYOUT_TYPE.CUSTOM_LAYOUT);
return <CustomLayout {...common} />;
}
};

View File

@ -10,6 +10,7 @@ import AudioService from '/imports/ui/components/audio/service';
import { Meteor } from "meteor/meteor";
import MediaStreamUtils from '/imports/utils/media-stream-utils';
import ConnectionStatusService from '/imports/ui/components/connection-status/service';
import browserInfo from '/imports/utils/browserInfo';
const VOLUME_CONTROL_ENABLED = Meteor.settings.public.kurento.screenshare.enableVolumeControl;
const SCREENSHARE_MEDIA_ELEMENT_NAME = 'screenshareVideo';
@ -122,6 +123,11 @@ const getVolume = () => KurentoBridge.getVolume();
const shouldEnableVolumeControl = () => VOLUME_CONTROL_ENABLED && screenshareHasAudio();
const attachLocalPreviewStream = (mediaElement) => {
const {isMobileApp} = browserInfo;
if (isMobileApp) {
// We don't show preview for mobile app, as the stream is only available in native code
return;
}
const stream = KurentoBridge.gdmStream;
if (stream && mediaElement) {
// Always muted, presenter preview.

View File

@ -118,6 +118,7 @@ const VideoListItem = (props) => {
onHandleVideoFocus={onHandleVideoFocus}
focused={focused}
onHandleMirror={() => setIsMirrored((value) => !value)}
isRTL={isRTL}
/>
);
@ -141,6 +142,7 @@ const VideoListItem = (props) => {
onHandleVideoFocus={onHandleVideoFocus}
focused={focused}
onHandleMirror={() => setIsMirrored((value) => !value)}
isRTL={isRTL}
/>
<UserStatus
voiceUser={voiceUser}
@ -186,6 +188,7 @@ const VideoListItem = (props) => {
onHandleVideoFocus={onHandleVideoFocus}
focused={focused}
onHandleMirror={() => setIsMirrored((value) => !value)}
isRTL={isRTL}
/>
<UserStatus
voiceUser={voiceUser}
@ -203,50 +206,6 @@ const VideoListItem = (props) => {
animations={animations}
{...makeDragOperations(onVirtualBgDrop, user?.userId)}
>
{
videoIsReady
? (
<>
<Styled.TopBar>
<PinArea
user={user}
/>
<ViewActions
videoContainer={videoContainer}
name={name}
cameraId={cameraId}
isFullscreenContext={isFullscreenContext}
layoutContextDispatch={layoutContextDispatch}
/>
</Styled.TopBar>
<Styled.BottomBar>
<UserActions
name={name}
user={user}
cameraId={cameraId}
numOfStreams={numOfStreams}
onHandleVideoFocus={onHandleVideoFocus}
focused={focused}
onHandleMirror={() => setIsMirrored((value) => !value)}
isRTL={isRTL}
/>
<UserStatus
voiceUser={voiceUser}
/>
</Styled.BottomBar>
</>
)
: (
<Styled.WebcamConnecting
data-test="webcamConnecting"
talking={talking}
animations={animations}
>
<Styled.LoadingText>{name}</Styled.LoadingText>
</Styled.WebcamConnecting>
)
}
<Styled.VideoContainer>
<Styled.Video
mirrored={isMirrored}

View File

@ -71,6 +71,9 @@ const WebcamComponent = ({
);
Storage.setItem('webcamSize', { width: newCameraMaxWidth, height: lastHeight });
}
const cams = document.getElementById('cameraDock');
cams?.setAttribute("data-position", cameraDock.position);
}, [cameraDock.position, cameraDock.maxWidth, isPresenter, displayPresentation]);
const handleVideoFocus = (id) => {
@ -122,6 +125,8 @@ const WebcamComponent = ({
setIsDragging(false);
setDraggedAtLeastOneTime(false);
document.body.style.overflow = 'auto';
const layout = document.getElementById('layout');
layout?.setAttribute("data-cam-position", e?.target?.id);
if (Object.values(CAMERADOCK_POSITION).includes(e.target.id) && draggedAtLeastOneTime) {
layoutContextDispatch({

View File

@ -184,7 +184,7 @@ export default function Whiteboard(props) {
tldrawAPI?.setCamera(camera.point, camera.zoom);
}
}
}, [presentationBounds, curPageId]);
}, [presentationBounds, curPageId, document?.documentElement?.dir]);
// change tldraw page when presentation page changes
React.useEffect(() => {
@ -228,7 +228,7 @@ export default function Whiteboard(props) {
isMultiUserActive={isMultiUserActive}
>
<Tldraw
key={`wb-${!hasWBAccess && !isPresenter}`}
key={`wb-${!hasWBAccess && !isPresenter}-${document?.documentElement?.dir}`}
document={doc}
// disable the ability to drag and drop files onto the whiteboard
// until we handle saving of assets in akka.

View File

@ -1,6 +1,10 @@
import * as React from "react";
import { _ } from "lodash";
const RESIZE_HANDLE_HEIGHT = 8;
const RESIZE_HANDLE_WIDTH = 18;
const BOTTOM_CAM_HANDLE_HEIGHT = 10;
function usePrevious(value) {
const ref = React.useRef();
React.useEffect(() => {
@ -80,7 +84,7 @@ const PositionLabel = (props) => {
isMultiUserActive,
} = props;
const { name, color, userId, presenter } = currentUser;
const { name, color } = currentUser;
const prevCurrentPoint = usePrevious(currentPoint);
React.useEffect(() => {
@ -121,14 +125,15 @@ export default function Cursors(props) {
isViewersCursorLocked,
hasMultiUserAccess,
isMultiUserActive,
application,
} = props;
const start = () => setActive(true);
const end = () => {
publishCursorUpdate({
xPercent: null,
yPercent: null,
xPercent: -1.0,
yPercent: -1.0,
whiteboardId: whiteboardId,
});
setActive(false);
@ -136,15 +141,78 @@ export default function Cursors(props) {
const moved = (event) => {
const { type } = event;
const yOffset = parseFloat(document.getElementById('Navbar')?.style?.height);
const nav = document.getElementById('Navbar');
let yOffset = parseFloat(nav?.style?.height);
const getSibling = (el) => el?.previousSibling || null;
const panel = getSibling(document.getElementById('Navbar'));
const panel = getSibling(nav);
const webcams = document.getElementById('cameraDock');
const subPanel = panel && getSibling(panel);
const xOffset = (parseFloat(panel?.style?.width) || 0) + (parseFloat(subPanel?.style?.width) || 0);
let xOffset = (parseFloat(panel?.style?.width) || 0) + (parseFloat(subPanel?.style?.width) || 0);
const camPosition = document.getElementById('layout')?.getAttribute('data-cam-position') || null;
const sl = document.getElementById('layout')?.getAttribute('data-layout');
if (type === 'touchmove') {
!active && setActive(true);
return setPos({ x: event?.changedTouches[0]?.clientX - xOffset, y: event?.changedTouches[0]?.clientY - yOffset });
}
const handleCustomYOffsets = () => {
if (camPosition === 'contentTop' || !camPosition) {
yOffset += (parseFloat(webcams?.style?.height) + RESIZE_HANDLE_HEIGHT);
}
if (camPosition === 'contentBottom') {
yOffset -= BOTTOM_CAM_HANDLE_HEIGHT;
}
}
if (document?.documentElement?.dir === 'rtl') {
xOffset = 0;
if (webcams && sl?.includes('custom')) {
handleCustomYOffsets();
if (camPosition === 'contentRight') {
xOffset += (parseFloat(webcams?.style?.width) + RESIZE_HANDLE_WIDTH);
}
}
if (webcams && sl?.includes('smart')) {
if (panel || subPanel) {
const dockPos = webcams?.getAttribute("data-position");
if (dockPos === 'contentRight') {
xOffset += (parseFloat(webcams?.style?.width) + RESIZE_HANDLE_WIDTH);
}
if (dockPos === 'contentTop') {
yOffset += (parseFloat(webcams?.style?.height) + RESIZE_HANDLE_WIDTH);
}
}
if (!panel && !subPanel) {
xOffset = 0;
}
}
} else {
if (webcams && sl?.includes('custom')) {
handleCustomYOffsets();
if (camPosition === 'contentLeft') {
xOffset += (parseFloat(webcams?.style?.width) + RESIZE_HANDLE_WIDTH);
}
}
if (webcams && sl?.includes('smart')) {
if (panel || subPanel) {
const dockPos = webcams?.getAttribute("data-position");
if (dockPos === 'contentLeft') {
xOffset += (parseFloat(webcams?.style?.width) + RESIZE_HANDLE_WIDTH);
}
if (dockPos === 'contentTop') {
yOffset += (parseFloat(webcams?.style?.height) + RESIZE_HANDLE_WIDTH);
}
}
if (!panel && !subPanel) {
xOffset = (parseFloat(webcams?.style?.width) + RESIZE_HANDLE_WIDTH);
}
}
}
return setPos({ x: event.x - xOffset, y: event.y - yOffset });
}
@ -167,18 +235,22 @@ export default function Cursors(props) {
React.useEffect(() => {
return () => {
cursorWrapper.removeEventListener('mouseenter', start);
cursorWrapper.removeEventListener('mouseleave', end);
cursorWrapper.removeEventListener('mousemove', moved);
cursorWrapper.removeEventListener('touchend', end);
cursorWrapper.removeEventListener('touchmove', moved);
if (cursorWrapper) {
cursorWrapper.removeEventListener('mouseenter', start);
cursorWrapper.removeEventListener('mouseleave', end);
cursorWrapper.removeEventListener('mousemove', moved);
cursorWrapper.removeEventListener('touchend', end);
cursorWrapper.removeEventListener('touchmove', moved);
}
}
}, []);
});
const multiUserAccess = hasMultiUserAccess(whiteboardId, currentUser?.userId);
return (
<span disabled={true} ref={(r) => (cursorWrapper = r)}>
<div style={{ height: "100%", cursor: "none" }}>
{active && (
<div style={{ height: "100%", cursor: multiUserAccess || currentUser?.presenter ? "none" : "default" }}>
{(active && multiUserAccess || (active && currentUser?.presenter)) && (
<PositionLabel
pos={pos}
otherCursors={otherCursors}

View File

@ -1,5 +1,6 @@
import { withTracker } from "meteor/react-meteor-data";
import React from "react";
import SettingsService from '/imports/ui/services/settings';
import Cursors from "./component";
import Service from "./service";
@ -10,6 +11,7 @@ const CursorsContainer = (props) => {
export default
withTracker((params) => {
return {
application: SettingsService?.application,
currentUser: params.currentUser,
publishCursorUpdate: Service.publishCursorUpdate,
otherCursors: Service.getCurrentCursors(params.whiteboardId),

View File

@ -0,0 +1,296 @@
import browserInfo from '/imports/utils/browserInfo';
import logger from '/imports/startup/client/logger';
(function (){
// This function must be executed during the import time, that's why it's not exported to the caller component.
// It's needed because it changes some functions provided by browser, and these functions are verified during
// import time (like in ScreenshareBridgeService)
if(browserInfo.isMobileApp) {
logger.debug(`BBB-MOBILE - Mobile APP detected`);
const WEBRTC_CALL_TYPE_FULL_AUDIO = 'full_audio';
const WEBRTC_CALL_TYPE_SCREEN_SHARE = 'screen_share';
const WEBRTC_CALL_TYPE_STANDARD = 'standard';
// This function detects if the call happened to publish a screenshare
function detectWebRtcCallType(caller, peerConnection = null, args = null) {
// Keep track of how many webRTC evaluations was done
if(!peerConnection.detectWebRtcCallTypeEvaluations)
peerConnection.detectWebRtcCallTypeEvaluations = 0;
peerConnection.detectWebRtcCallTypeEvaluations ++;
// If already successfully evaluated, reuse
if(peerConnection && peerConnection.webRtcCallType !== undefined ) {
logger.info(`BBB-MOBILE - detectWebRtcCallType (already evaluated as ${peerConnection.webRtcCallType})`, {caller, peerConnection});
return peerConnection.webRtcCallType;
}
// Evaluate context otherwise
const e = new Error('dummy');
const stackTrace = e.stack;
logger.info(`BBB-MOBILE - detectWebRtcCallType (evaluating)`, {caller, peerConnection, stackTrace: stackTrace.split('\n'), detectWebRtcCallTypeEvaluations: peerConnection.detectWebRtcCallTypeEvaluations, args});
// addTransceiver is the first call for screensharing and it has a startScreensharing in its stackTrace
if( peerConnection.detectWebRtcCallTypeEvaluations == 1) {
if(caller == 'addTransceiver' && stackTrace.indexOf('startScreensharing') !== -1) {
peerConnection.webRtcCallType = WEBRTC_CALL_TYPE_SCREEN_SHARE; // this uses mobile app broadcast upload extension
} else if(caller == 'addEventListener' && stackTrace.indexOf('invite') !== -1) {
peerConnection.webRtcCallType = WEBRTC_CALL_TYPE_FULL_AUDIO; // this uses mobile app webRTC
} else {
peerConnection.webRtcCallType = WEBRTC_CALL_TYPE_STANDARD; // this uses the webview webRTC
}
return peerConnection.webRtcCallType;
}
}
// Store the method call sequential
const sequenceHolder = {sequence: 0};
// Store the promise for each method call
const promisesHolder = {};
// Call a method in the mobile application, returning a promise for its execution
function callNativeMethod(method, args=[]) {
try {
const sequence = ++sequenceHolder.sequence;
return new Promise ( (resolve, reject) => {
promisesHolder[sequence] = {
resolve, reject
};
window.ReactNativeWebView.postMessage(JSON.stringify({
sequence: sequenceHolder.sequence,
method: method,
arguments: args,
}));
} );
} catch(e) {
logger.error(`Error on callNativeMethod ${e.message}`, e);
}
}
// This method is called from the mobile app to notify us about a method invocation result
window.nativeMethodCallResult = (sequence, isResolve, resultOrException) => {
const promise = promisesHolder[sequence];
if(promise) {
if(isResolve) {
promise.resolve( resultOrException );
delete promisesHolder[sequence];
} else {
promise.reject( resultOrException );
delete promisesHolder[sequence];
}
}
return true;
}
// WebRTC replacement functions
const buildVideoTrack = function () {}
const stream = {};
// Navigator
navigator.getDisplayMedia = function() {
logger.info(`BBB-MOBILE - getDisplayMedia called`, arguments);
return new Promise((resolve, reject) => {
callNativeMethod('initializeScreenShare').then(
() => {
const fakeVideoTrack = {};
fakeVideoTrack.applyConstraints = function (constraints) {
return new Promise(
(resolve, reject) => {
resolve();
}
);
};
fakeVideoTrack.onended = null; // callbacks added from screenshare (we can use it later)
fakeVideoTrack.oninactive = null; // callbacks added from screenshare (we can use it later)
fakeVideoTrack.addEventListener = function() {}; // skip listeners
const videoTracks = [
fakeVideoTrack
];
stream.getTracks = stream.getVideoTracks = function () {
return videoTracks;
};
stream.active=true;
resolve(stream);
}
).catch(
(e) => {
logger.error(`Failure calling native initializeScreenShare`, e.message)
}
);
});
}
// RTCPeerConnection
const prototype = window.RTCPeerConnection.prototype;
prototype.originalCreateOffer = prototype.createOffer;
prototype.createOffer = function (options) {
const webRtcCallType = detectWebRtcCallType('createOffer', this);
if(webRtcCallType === WEBRTC_CALL_TYPE_STANDARD){
return prototype.originalCreateOffer.call(this, ...arguments);
}
logger.info(`BBB-MOBILE - createOffer called`, {options});
const createOfferMethod = (webRtcCallType === WEBRTC_CALL_TYPE_SCREEN_SHARE) ? 'createScreenShareOffer' : 'createFullAudioOffer';
return new Promise( (resolve, reject) => {
callNativeMethod(createOfferMethod).then ( sdp => {
logger.info(`BBB-MOBILE - createOffer resolved`, {sdp});
// send offer to BBB code
resolve({
type: 'offer',
sdp
});
});
} );
};
prototype.originalAddEventListener = prototype.addEventListener;
prototype.addEventListener = function (event, callback) {
if(WEBRTC_CALL_TYPE_STANDARD === detectWebRtcCallType('addEventListener', this, arguments)){
return prototype.originalAddEventListener.call(this, ...arguments);
}
logger.info(`BBB-MOBILE - addEventListener called`, {event, callback});
switch(event) {
case 'icecandidate':
window.bbbMobileScreenShareIceCandidateCallback = function () {
logger.info("Received a bbbMobileScreenShareIceCandidateCallback call with arguments", arguments);
if(callback){
callback.apply(this, arguments);
}
return true;
}
break;
case 'signalingstatechange':
window.bbbMobileScreenShareSignalingStateChangeCallback = function (newState) {
this.signalingState = newState;
callback();
};
break;
}
}
prototype.originalSetLocalDescription = prototype.setLocalDescription;
prototype.setLocalDescription = function (description, successCallback, failureCallback) {
if(WEBRTC_CALL_TYPE_STANDARD === detectWebRtcCallType('setLocalDescription', this)){
return prototype.originalSetLocalDescription.call(this, ...arguments);
}
logger.info(`BBB-MOBILE - setLocalDescription called`, {description, successCallback, failureCallback});
// store the value
this._localDescription = JSON.parse(JSON.stringify(description));
// replace getter of localDescription to return this value
Object.defineProperty(this, 'localDescription', {get: function() {return this._localDescription;},set: function(newValue) {}});
// return a promise that resolves immediately
return new Promise( (resolve, reject) => {
resolve();
})
}
prototype.originalSetRemoteDescription = prototype.setRemoteDescription;
prototype.setRemoteDescription = function (description, successCallback, failureCallback) {
const webRtcCallType = detectWebRtcCallType('setRemoteDescription', this);
if(WEBRTC_CALL_TYPE_STANDARD === webRtcCallType){
return prototype.originalSetRemoteDescription.call(this, ...arguments);
}
logger.info(`BBB-MOBILE - setRemoteDescription called`, {description, successCallback, failureCallback});
this._remoteDescription = JSON.parse(JSON.stringify(description));
Object.defineProperty(this, 'remoteDescription', {get: function() {return this._remoteDescription;},set: function(newValue) {}});
const setRemoteDescriptionMethod = (webRtcCallType === WEBRTC_CALL_TYPE_SCREEN_SHARE) ? 'setScreenShareRemoteSDP' : 'setFullAudioRemoteSDP';
return new Promise( (resolve, reject) => {
callNativeMethod(setRemoteDescriptionMethod, [description]).then ( () => {
logger.info(`BBB-MOBILE - setRemoteDescription resolved`);
resolve();
if(webRtcCallType === WEBRTC_CALL_TYPE_FULL_AUDIO) {
Object.defineProperty(this, "iceGatheringState", {get: function() { return "complete" }, set: ()=>{} });
Object.defineProperty(this, "iceConnectionState", {get: function() { return "completed" }, set: ()=>{} });
this.onicegatheringstatechange && this.onicegatheringstatechange({target: this});
this.oniceconnectionstatechange && this.oniceconnectionstatechange({target: this});
}
});
} );
}
prototype.originalAddTrack = prototype.addTrack;
prototype.addTrack = function (description, successCallback, failureCallback) {
if(WEBRTC_CALL_TYPE_STANDARD === detectWebRtcCallType('addTrack', this)){
return prototype.originalAddTrack.call(this, ...arguments);
}
logger.info(`BBB-MOBILE - addTrack called`, {description, successCallback, failureCallback});
}
prototype.originalGetLocalStreams = prototype.getLocalStreams;
prototype.getLocalStreams = function() {
if(WEBRTC_CALL_TYPE_STANDARD === detectWebRtcCallType('getLocalStreams', this)){
return prototype.originalGetLocalStreams.call(this, ...arguments);
}
logger.info(`BBB-MOBILE - getLocalStreams called`, arguments);
//
return [
stream
];
}
prototype.originalAddTransceiver = prototype.addTransceiver;
prototype.addTransceiver = function() {
if(WEBRTC_CALL_TYPE_STANDARD === detectWebRtcCallType('addTransceiver', this)){
return prototype.originalAddTransceiver.call(this, ...arguments);
}
logger.info(`BBB-MOBILE - addTransceiver called`, arguments);
}
prototype.originalAddIceCandidate = prototype.addIceCandidate;
prototype.addIceCandidate = function (candidate) {
if(WEBRTC_CALL_TYPE_STANDARD === detectWebRtcCallType('addIceCandidate', this)){
return prototype.originalAddIceCandidate.call(this, ...arguments);
}
logger.info(`BBB-MOBILE - addIceCandidate called`, {candidate});
return new Promise( (resolve, reject) => {
callNativeMethod('addRemoteIceCandidate', [candidate]).then ( () => {
logger.info("BBB-MOBILE - addRemoteIceCandidate resolved");
resolve();
});
} );
}
// Handle screenshare stop
const KurentoScreenShareBridge = require('/imports/api/screenshare/client/bridge/index.js').default;
//Kurento Screen Share
var stopOriginal = KurentoScreenShareBridge.stop.bind(KurentoScreenShareBridge);
KurentoScreenShareBridge.stop = function(){
callNativeMethod('stopScreenShare')
logger.debug(`BBB-MOBILE - Click on stop screen share`);
stopOriginal()
}
// Handle screenshare stop requested by application (i.e. stopped the broadcast extension)
window.bbbMobileScreenShareBroadcastFinishedCallback = function () {
document.querySelector('[data-test="stopScreenShare"]')?.click();
}
}
})();

View File

@ -1,6 +1,7 @@
import Bowser from 'bowser';
const BOWSER_RESULTS = Bowser.parse(window.navigator.userAgent);
const userAgent = window.navigator.userAgent;
const BOWSER_RESULTS = Bowser.parse(userAgent);
const isChrome = BOWSER_RESULTS.browser.name === 'Chrome';
const isSafari = BOWSER_RESULTS.browser.name === 'Safari';
@ -11,10 +12,12 @@ const isFirefox = BOWSER_RESULTS.browser.name === 'Firefox';
const browserName = BOWSER_RESULTS.browser.name;
const versionNumber = BOWSER_RESULTS.browser.version;
const isValidSafariVersion = Bowser.getParser(window.navigator.userAgent).satisfies({
const isValidSafariVersion = Bowser.getParser(userAgent).satisfies({
safari: '>12',
});
const isMobileApp = !!(userAgent.match(/BBBMobile/i));
const browserInfo = {
isChrome,
isSafari,
@ -24,6 +27,7 @@ const browserInfo = {
browserName,
versionNumber,
isValidSafariVersion,
isMobileApp
};
export default browserInfo;

View File

@ -1,6 +1,7 @@
import Bowser from 'bowser';
const BOWSER_RESULTS = Bowser.parse(window.navigator.userAgent);
const userAgent = window.navigator.userAgent;
const BOWSER_RESULTS = Bowser.parse(userAgent);
const isPhone = BOWSER_RESULTS.platform.type === 'mobile';
// we need a 'hack' to correctly detect ipads with ios > 13
@ -11,7 +12,7 @@ const osName = BOWSER_RESULTS.os.name;
const osVersion = BOWSER_RESULTS.os.version;
const isIos = osName === 'iOS';
const isMacos = osName === 'macOS';
const isIphone = !!(window.navigator.userAgent.match(/iPhone/i));
const isIphone = !!(userAgent.match(/iPhone/i));
const SUPPORTED_IOS_VERSION = 12.2;
const isIosVersionSupported = () => parseFloat(osVersion) >= SUPPORTED_IOS_VERSION;

View File

@ -3595,24 +3595,6 @@
"integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=",
"dev": true
},
"is-extendable": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
"integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
"requires": {
"is-plain-object": "^2.0.4"
},
"dependencies": {
"is-plain-object": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
"integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
"requires": {
"isobject": "^3.0.1"
}
}
}
},
"is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@ -3733,11 +3715,6 @@
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
"dev": true
},
"isobject": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
},
"isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
@ -5337,14 +5314,6 @@
}
}
},
"object.omit": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object.omit/-/object.omit-3.0.0.tgz",
"integrity": "sha512-EO+BCv6LJfu+gBIF3ggLicFebFLN5zqzz/WWJlMFfkMyGth+oBkhxzDl0wx2W4GkLzuQs/FsSkXZb2IMWQqmBQ==",
"requires": {
"is-extendable": "^1.0.0"
}
},
"object.values": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz",
@ -5913,16 +5882,6 @@
"tinycolor2": "^1.4.1"
}
},
"react-cursor-position": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/react-cursor-position/-/react-cursor-position-3.0.3.tgz",
"integrity": "sha512-jrmHFKQtfNdvfJ5hIH+FOb2h9+mgcD8/POOY7LngmsYCJNlg6IYdnGdbMGMTeyue/iUvY+t20t20RDrH+qW5dw==",
"requires": {
"object-assign": "^4.1.1",
"object.omit": "^3.0.0",
"prop-types": "^15.6.0"
}
},
"react-dom": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",

View File

@ -363,7 +363,7 @@
"app.endMeeting.noLabel": "لا",
"app.about.title": "حول",
"app.about.version": "نسخة التطبيق:",
"app.about.version_label": "إصدار BigBlueButton:",
"app.about.version_label": "إصدار بك بلو بتن:",
"app.about.copyright": "حقوق الملكية :",
"app.about.confirmLabel": "موافق",
"app.about.confirmDesc": "موافق",
@ -613,7 +613,7 @@
"app.guest.errorSeeConsole": "خطأ: مزيد من التفاصيل في وحدة التحكم.",
"app.guest.noModeratorResponse": "لا يوجد رد من المشرف.",
"app.guest.noSessionToken": "لم يتم استلام رمز جلسة.",
"app.guest.windowTitle": "BigBlueButton - ردهة الضيوف",
"app.guest.windowTitle": "بك بلو بتن - ردهة الضيوف",
"app.guest.missingToken": "الضيف يفتقد رمز الجلسة.",
"app.guest.missingSession": "الضيف فقد الجلسة.",
"app.guest.missingMeeting": "الاجتماع غير موجود.",
@ -859,8 +859,8 @@
"app.whiteboard.toolbar.fontSize": "قائمة حجم الخط",
"app.whiteboard.toolbarAriaLabel": "أدوات العرض",
"app.feedback.title": "قمت بتسجيل الخروج من المؤتمر",
"app.feedback.subtitle": "نود أن نسمع عن تجربتك مع BigBlueButton (اختياري)",
"app.feedback.textarea": "كيف يمكننا جعل BigBlueButton أفضل؟",
"app.feedback.subtitle": "نود أن نسمع عن تجربتك مع بك بلو بتن (اختياري)",
"app.feedback.textarea": "كيف يمكننا جعل بك بلو بتن أفضل؟",
"app.feedback.sendFeedback": "ارسل رأيك",
"app.feedback.sendFeedbackDesc": "أرسل ملاحظاتك وغادر الاجتماع",
"app.videoDock.webcamMirrorLabel": "عكس",
@ -1050,7 +1050,7 @@
"mobileApp.portals.list.empty.orUseOurDemoServer.label": "أو استخدم خادمنا التجريبي.",
"mobileApp.portals.list.add.button.label": "أضف بوابة",
"mobileApp.portals.fields.name.label": "اسم البوابة",
"mobileApp.portals.fields.name.placeholder": "BigBlueButton التجريبي",
"mobileApp.portals.fields.name.placeholder": "بك بلو بتن التجريبي",
"mobileApp.portals.fields.url.label": "عنوان رابط للخادم",
"mobileApp.portals.addPortalPopup.confirm.button.label": "حفظ",
"mobileApp.portals.drawerNavigation.button.label": "البوابات",

View File

@ -132,6 +132,8 @@
"app.userList.userOptions.savedNames.title": "Llista dels usuaris en la reunió {0} a {1}",
"app.userList.userOptions.sortedFirstName.heading": "Ordenat pel nom:",
"app.userList.userOptions.sortedLastName.heading": "Ordenat pel cognom:",
"app.userList.userOptions.hideViewersCursor": "Els cursors de l'espectador estan bloquejats",
"app.userList.userOptions.showViewersCursor": "Els cursors de l'espectador estan desbloquejats",
"app.media.label": "Media",
"app.media.autoplayAlertDesc": "Permet accés",
"app.media.screenshare.start": "Inici de pantalla compartida",
@ -171,6 +173,7 @@
"app.presentation.options.fullscreen": "Pantalla completa",
"app.presentation.options.exitFullscreen": "Surt de la pantalla completa",
"app.presentation.options.minimize": "Minimitzar",
"app.presentation.options.snapshot": "Instantània de la diapositiva actual",
"app.presentation.options.downloading": "Descarregant...",
"app.presentation.options.downloaded": "S'ha descarregat la presentació actual",
"app.presentation.options.downloadFailed": "No s'ha pogut descarregar la presentació actual",
@ -503,6 +506,8 @@
"app.breakoutJoinConfirmation.freeJoinMessage": "Escull la sala separada per a unir-se",
"app.breakoutTimeRemainingMessage": "Temps restant a la sala separada: {0}",
"app.breakoutWillCloseMessage": "Temps finalitzat. La reunió separada es tancarà aviat",
"app.breakout.dropdown.manageDuration": "Canviar la durada",
"app.breakout.dropdown.destroyAll": "Finalitza les sales externes",
"app.breakout.dropdown.options": "Opcions de sales externes",
"app.calculatingBreakoutTimeRemaining": "Calculant temps restant ...",
"app.audioModal.ariaTitle": "Entra a l'àudio modal",
@ -559,6 +564,7 @@
"app.audio.audioSettings.descriptionLabel": "Tingueu en compte que apareixerà un diàleg al navegador que requereix que accepteu compartir el micròfon.",
"app.audio.audioSettings.microphoneSourceLabel": "Font del micròfon",
"app.audio.audioSettings.speakerSourceLabel": "Font d'altaveu",
"app.audio.audioSettings.testSpeakerLabel": "Prova l'altaveu",
"app.audio.audioSettings.microphoneStreamLabel": "El seu volum d'emissió",
"app.audio.audioSettings.retryLabel": "Reintentar",
"app.audio.listenOnly.backLabel": "Enrere",
@ -597,6 +603,7 @@
"app.error.500": "Oh oh, quelcom ha anat malament",
"app.error.userLoggedOut": "L'usuari té un sessionToken no vàlid a causa del tancament de sessió",
"app.error.ejectedUser": "L'usuari té un sessionToken no vàlid a causa de l'expulsió",
"app.error.joinedAnotherWindow": "Aquesta sessió sembla estar oberta en una altra finestra del navegador.",
"app.error.userBanned": "L'usuari ha estat expulsat",
"app.error.leaveLabel": "Inicieu la sessió de nou",
"app.error.fallback.presentation.title": "Hi ha hagut un error",
@ -677,6 +684,10 @@
"app.shortcut-help.toggleFullscreen": "Alternar la pantalla completa (Presentador)",
"app.shortcut-help.nextSlideDesc": "Diapositiva següent (presentador)",
"app.shortcut-help.previousSlideDesc": "Diapositiva anterior (presentador)",
"app.shortcut-help.togglePanKey": "Barra espaiadora",
"app.shortcut-help.toggleFullscreenKey": "Enter",
"app.shortcut-help.nextSlideKey": "Fletxa dreta",
"app.shortcut-help.previousSlideKey": "Fletxa esquerra",
"app.lock-viewers.title": "Bloqueja espectadors",
"app.lock-viewers.description": "Aquestes opcions permeten restringir els espectadors a l'ús de funcions específiques.",
"app.lock-viewers.featuresLable": "Característica",
@ -692,6 +703,7 @@
"app.lock-viewers.button.apply": "Aplica",
"app.lock-viewers.button.cancel": "Cancel·la",
"app.lock-viewers.locked": "Bloquejat/da",
"app.lock-viewers.hideViewersCursor": "Veure altres cursors dels espectadors",
"app.guest-policy.ariaTitle": "Modalitat de configuració de la política de convidats",
"app.guest-policy.title": "Política de convidats",
"app.guest-policy.description": "Canviar la configuració de la política de convidats a les reunions",
@ -795,6 +807,7 @@
"app.video.virtualBackground.genericError": "No s'ha pogut aplicar l'efecte de càmara. Intenta-ho de nou.",
"app.video.virtualBackground.camBgAriaDesc": "Estableix el fons virtual de la càmera web en {0}",
"app.video.camCapReached": "No es poden compartir més càmeres",
"app.video.meetingCamCapReached": "La reunió ha arribat al límit de càmeres simultànies",
"app.video.dropZoneLabel": "Deixar caure aquí",
"app.fullscreenButton.label": "Fer {0} a pantalla completa",
"app.fullscreenUndoButton.label": "Desfés {0} pantalla completa",
@ -918,6 +931,8 @@
"app.externalVideo.refreshLabel": "Actualitzar el reproductor de vídeo",
"app.externalVideo.fullscreenLabel": "Reproductor de vídeo",
"app.externalVideo.noteLabel": "Nota: els vídeos externs compartits no apareixeran en l'enregistrament. No s'hi admeten vídeos de YouTube, Vimeo, Instructure Media, Twithc, Dailymotion ni URL de fitxers multimèdia (p. ex. https://example.com/xy.mp4).",
"app.externalVideo.subtitlesOn": "Apagar",
"app.externalVideo.subtitlesOff": "Encendre (si està disponible)",
"app.actionsBar.actionsDropdown.shareExternalVideo": "Comparteix un vídeo extern",
"app.actionsBar.actionsDropdown.stopShareExternalVideo": "Deixa de compartir els vídeos externs",
"app.iOSWarning.label": "Actualitzeu a iOS 12.2 o superior",
@ -947,6 +962,7 @@
"playback.button.search.aria": "Cerca",
"playback.button.section.aria": "Secció lateral",
"playback.button.swap.aria": "Intercanviar continguts",
"playback.button.theme.aria": "Alternar el tema",
"playback.error.wrapper.aria": "Àrea d'error",
"playback.loader.wrapper.aria": "Àrea de càrrega",
"playback.player.wrapper.aria": "Àrea de jugadors",
@ -1030,10 +1046,17 @@
"app.learningDashboard.statusTimelineTable.thumbnail": "Presentació en miniatura.",
"app.learningDashboard.errors.invalidToken": "Token de sessió no vàlid",
"app.learningDashboard.errors.dataUnavailable": "Les dades ja no estan disponibles",
"mobileApp.portals.list.empty.addFirstPortal.label": "Afegeix el teu primer portal utilitzant el botó de dalt,",
"mobileApp.portals.list.empty.orUseOurDemoServer.label": "o utilitza el nostre servidor de demostració.",
"mobileApp.portals.list.add.button.label": "Afegir portal",
"mobileApp.portals.fields.name.label": "Nom del portal",
"mobileApp.portals.fields.name.placeholder": "BigBlueButton demo",
"mobileApp.portals.fields.url.label": "URL del servidor",
"mobileApp.portals.drawerNavigation.button.label": "Portals"
"mobileApp.portals.addPortalPopup.confirm.button.label": "Guardar",
"mobileApp.portals.drawerNavigation.button.label": "Portals",
"mobileApp.portals.addPortalPopup.validation.emptyFields": "Camps obligatoris",
"mobileApp.portals.addPortalPopup.validation.portalNameAlreadyExists": "Nom ja utilitzat",
"mobileApp.portals.addPortalPopup.validation.urlInvalid": "Error en intentar carregar la pàgina - comprovi la URL i la connexió de xarxa"
}

View File

@ -2,7 +2,7 @@
"app.home.greeting": "Zure aurkezpena laster hasiko da...",
"app.chat.submitLabel": "Bidali mezua",
"app.chat.loading": "Kargatutako txat mezuak: {0}%",
"app.chat.errorMaxMessageLength": "Mezua luzeegia da, {0} karaktere soberan ditu",
"app.chat.errorMaxMessageLength": "Mezua luzeegia da, {0} karaktere soberan d(it)u",
"app.chat.disconnected": "Deskonektatuta zaude, mezuak ezin dira bidali",
"app.chat.locked": "Txata blokeatuta dago, mezuak ezin dira bidali",
"app.chat.inputLabel": "Txatean {0} mezu sartu dira",

View File

@ -40,8 +40,16 @@
"app.captions.menu.backgroundColor": "צבע רקע",
"app.captions.menu.previewLabel": "תצוגה מקדימה",
"app.captions.menu.cancelLabel": "ביטול",
"app.captions.hide": "הסתרת כתוביות",
"app.captions.ownership": "לקיחת שליטה",
"app.captions.dictationStart": "התחלת הכתבה",
"app.captions.dictationStop": "הפסקת הכתבה",
"app.textInput.sendLabel": "נשלח",
"app.title.defaultViewLabel": "ברירת מחדל לצפייה במצגת",
"app.notes.title": "פתקים משותפים",
"app.notes.label": "פתקים",
"app.notes.hide": "הסתרת פתקים",
"app.notes.locked": "נעול",
"app.user.activityCheck": "בדיקת זמינות משתמש",
"app.user.activityCheck.label": "בדיקה אם המשתמש עדיין במפגש ({0})",
"app.user.activityCheck.check": "בדיקה",
@ -49,12 +57,16 @@
"app.userList.participantsTitle": "משתתפים",
"app.userList.messagesTitle": "הודעות",
"app.userList.notesTitle": "הערות",
"app.userList.notesListItem.unreadContent": "תוכן חדש זמין באזור הערות משותפות",
"app.userList.captionsTitle": "כתוביות",
"app.userList.presenter": "מנחה",
"app.userList.you": "את/ה",
"app.userList.locked": "נעול",
"app.userList.byModerator": "מאת (מנחה)",
"app.userList.label": "משתתפים",
"app.userList.toggleCompactView.label": "מעבר למצב צפיה מצומצם",
"app.userList.moderator": "מנהל/ת",
"app.userList.mobile": "מכשיר סלולרי",
"app.userList.guest": "אורח",
"app.userList.sharingWebcam": "מצלמה",
"app.userList.menuTitleContext": "אפשרויות",
@ -64,9 +76,11 @@
"app.userList.menu.clearStatus.label": "איפוס מצב",
"app.userList.menu.removeUser.label": "הוצאת משתתף",
"app.userList.menu.removeConfirmation.label": "הסרת משתתף ({0})",
"app.userlist.menu.removeConfirmation.desc": "מניעת משתמש זה מלהצטרף שוב להפעלה.",
"app.userList.menu.muteUserAudio.label": "השתקה",
"app.userList.menu.unmuteUserAudio.label": "ביטול השתקת משתמש",
"app.userList.menu.giveWhiteboardAccess.label" : "מתן גישה ללוח־ציור משותף",
"app.userList.menu.ejectUserCameras.label": "סגירת מצלמות",
"app.userList.userAriaLabel": "{0} {1} {2} במצב {3}",
"app.userList.menu.promoteUser.label": "עדכון תפקיד למנחה",
"app.userList.menu.demoteUser.label": "עדכון תפקיד לצופה",
@ -84,6 +98,9 @@
"app.userList.userOptions.unmuteAllLabel": "ביטול השתקה במפגש",
"app.userList.userOptions.unmuteAllDesc": "ביטול השתקה במפגש",
"app.userList.userOptions.lockViewersLabel": "נעילת מצב צופים",
"app.userList.userOptions.lockViewersDesc": "נעילת פונקציות מסוימות עבור המשתתפים במפגש",
"app.userList.userOptions.guestPolicyLabel": "מדיניות אורחים",
"app.userList.userOptions.guestPolicyDesc": "שינוי הגדרת המדיניות עבור אורחים במפגש",
"app.userList.userOptions.disableCam": "מצלמות הצופים בוטלו",
"app.userList.userOptions.disableMic": "כל הצופים הושתקו",
"app.userList.userOptions.disablePrivChat": "שיחה פרטית מבוטלת",
@ -97,22 +114,44 @@
"app.userList.userOptions.enablePubChat": "רב־שיח ציבורי מאופשר",
"app.userList.userOptions.showUserList": "רשימת המשתתפים גלויה לצופים",
"app.userList.userOptions.enableOnlyModeratorWebcam": "ניתן להדליק את מצלמת הרשת שלך כעת! כולם יראו אותך",
"app.userList.userOptions.sortedFirstName.heading": "ממוין לפי שם פרטי:",
"app.userList.userOptions.sortedLastName.heading": "ממומין לפי שם משפחה:",
"app.media.label": "מדיה",
"app.media.autoplayAlertDesc": "אפשרות גישה",
"app.media.screenshare.start": "שיתוף מסך החל",
"app.media.screenshare.end": "שיתוף מסך הסתיים",
"app.media.screenshare.endDueToDataSaving": "שיתוף המסך הופסק עקב שמירת נתונים",
"app.media.screenshare.unavailable": "שיתוף מסך לא זמין",
"app.media.screenshare.notSupported": "שיתוף מסך אינו נתמך ע\"י דפדפן זה.",
"app.media.screenshare.autoplayBlockedDesc": "נדרש אישור להצגת מסך המנחה.",
"app.media.screenshare.autoplayAllowLabel": "צפייה בשיתוף מסך",
"app.screenshare.presenterLoadingLabel": "שיתוף המסך שלך באמצע טעינה",
"app.screenshare.viewerLoadingLabel": "מסך המגיש באמצע טעינה",
"app.screenshare.presenterSharingLabel": "שיתוף המסך שלך מתבצע כעת",
"app.meeting.ended": "המפגש הסתיים",
"app.meeting.meetingTimeRemaining": "זמן נותר למפגש: {0}",
"app.meeting.meetingTimeHasEnded": "המפגש הסתיים ויסגר בקרוב",
"app.meeting.endedByUserMessage": "מפגש זה הסתיים ע\"י {0}",
"app.meeting.endedByNoModeratorMessageSingular": "הפגישה הסתיימה עקב אי נוכחות המנחה לאחר דקה אחת",
"app.meeting.endedByNoModeratorMessagePlural": "הפגישה הסתיימה בגלל שאין מנחה נוכח לאחר {0} אחת",
"app.meeting.endedMessage": "הפניה למסך הבית",
"app.meeting.alertMeetingEndsUnderMinutesSingular": "הפגישה עומדת להסתיים עוד דקה אחת.",
"app.meeting.alertMeetingEndsUnderMinutesPlural": "הפגישה עומדת להסתיים עוד {0} אחת.",
"app.meeting.alertBreakoutEndsUnderMinutesPlural": "ההפסקה עומדת להסתיים עוד {0} אחת.",
"app.meeting.alertBreakoutEndsUnderMinutesSingular": "ההפסקה עומדת להסתיים עוד דקה אחת.",
"app.presentation.hide": "הסתרת מצגת",
"app.presentation.notificationLabel": "מצגת נוכחית",
"app.presentation.downloadLabel": "הורדת קובץ",
"app.presentation.slideContent": "תוכן עמוד המצגת",
"app.presentation.startSlideContent": "התחלת המצגת",
"app.presentation.endSlideContent": "סיום המצגת",
"app.presentation.emptySlideContent": "עמוד מצגת נוכחי ללא תוכן",
"app.presentation.options.fullscreen": "מסך מלא",
"app.presentation.options.exitFullscreen": "יציאה ממסך מלא",
"app.presentation.options.snapshot": "תמונת מצב של התמונה הנוכחית במצגת",
"app.presentation.options.downloading": "הורדת קובץ ...",
"app.presentation.options.downloaded": "התבצעה הורדה של המצגת הנוכחית ",
"app.presentation.options.downloadFailed": " הורדה של המצגת הנוכחית נכשלה",
"app.presentation.presentationToolbar.noNextSlideDesc": "סיום מצגת",
"app.presentation.presentationToolbar.noPrevSlideDesc": "התחלת מצגת",
"app.presentation.presentationToolbar.selectLabel": "בחירת עמוד",
@ -137,6 +176,7 @@
"app.presentation.presentationToolbar.fitToWidth": "התאמת רוחב",
"app.presentation.presentationToolbar.fitToPage": "התאמה לעמוד",
"app.presentation.presentationToolbar.goToSlide": "עמוד {0}",
"app.presentation.placeholder": "אין כרגע מצגת פעילה",
"app.presentationUploder.title": "מצגת",
"app.presentationUploder.message": "כמגיש/ה יש לך יכולת להעלות כל מסמך אופיס או קובץ PDF. אנו ממליצים על קובץ PDF לתוצאות טובות ביותר. אנא ודאו שמצגת נבחרת באמצעות תיבת הסימון המעגלית בצד ימין.",
"app.presentationUploder.uploadLabel": "העלאה",
@ -151,25 +191,46 @@
"app.presentationUploder.fileToUpload": "להעלאה ...",
"app.presentationUploder.currentBadge": "נוכחי",
"app.presentationUploder.rejectedError": "לא ניתן להעלות קבצים מסוג זה.",
"app.presentationUploder.connectionClosedError": "תקלה עקב קליטה ירודה. נא נסו שוב.",
"app.presentationUploder.upload.progress": "מעלה ({0}%)",
"app.presentationUploder.upload.413": "הקובץ גדול מדי, חרג מהמקסימום של {0} MB",
"app.presentationUploder.genericError": "אופס, משהו השתבש..",
"app.presentationUploder.conversion.conversionProcessingSlides": "מעבד דף {0} מתוך {1}",
"app.presentationUploder.conversion.genericConversionStatus": "ממיר קבצים ...",
"app.presentationUploder.conversion.generatingThumbnail": "מייצר תצוגה מקדימה ...",
"app.presentationUploder.conversion.generatedSlides": "שקופיות הומרו בהצלחה ...",
"app.presentationUploder.conversion.generatingSvg": "ממיר קבצי SVG...",
"app.presentationUploder.conversion.pageCountExceeded": "מספר הדפים חרג מהמקסימום של {0}",
"app.presentationUploder.conversion.officeDocConversionInvalid": "אירעה שגיאה במהלך הנסיון להציג את מסמך האופיס, אנא העלה מסמך PDF במקום",
"app.presentationUploder.conversion.officeDocConversionFailed": "אירעה שגיאה במהלך הנסיון להציג את מסמך האופיס, אנא העלה מסמך PDF במקום",
"app.presentationUploder.conversion.pdfHasBigPage": "נכשל נסיון המרת הקובץ PDF, אנא נסו לשנות את הגודל שלו. גודל מקסימלי של דף הוא {0}",
"app.presentationUploder.conversion.timeout": "המרת הקובץ ארכה זמן רב מדי, אנא נסה שנית",
"app.presentationUploder.conversion.pageCountFailed": "אירעה שגיאה בנסיון לקבוע את מספר העמודים במצגת",
"app.presentationUploder.conversion.unsupportedDocument": "סיומת הקובץ אינה נתמכת",
"app.presentationUploder.isDownloadableLabel": "הורדת מצגת אינה מותרת - לחצו כדי לאפשר הורדת מצגת",
"app.presentationUploder.isNotDownloadableLabel": "הורדת המצגת מותרת - לחצו כדי לבטל אפשרות של הורדת המצגת",
"app.presentationUploder.removePresentationLabel": "הסרת מצגת",
"app.presentationUploder.setAsCurrentPresentation": "הגדירו את המצגת הנוכחית כעדכנית",
"app.presentationUploder.tableHeading.filename": "שם הקובץ",
"app.presentationUploder.tableHeading.options": "אפשרויות",
"app.presentationUploder.tableHeading.status": "מצב",
"app.presentationUploder.uploadStatus": "{0} מתוך {1} העלאות הושלמו",
"app.presentationUploder.completed": " {0} העלאות הושלמו",
"app.presentationUploder.item" : "פריט",
"app.presentationUploder.itemPlural" : "פריטים",
"app.presentationUploder.clearErrors": "ניקוי שגיאות",
"app.presentationUploder.clearErrorsDesc": "מנקה העלאות של מצגות שנכשלו ",
"app.presentationUploder.uploadViewTitle": "העלאות של מצגות",
"app.poll.pollPaneTitle": "סקר",
"app.poll.enableMultipleResponseLabel": "לאפשר מספר תשובות לכל משיב?",
"app.poll.quickPollTitle": "סקר מהיר",
"app.poll.hidePollDesc": "הסתרת תפריט הסקר",
"app.poll.quickPollInstruction": "בחירת אפשרות להתחלת הסקר.",
"app.poll.activePollInstruction": "יציאה ממסך זה על מנת לראות את התשובות לסקר בזמן אמת, כשאת/ה מוכן - יש לבחור 'פרסום תוצאות סקר' כדי לפרסם את תוצאותיו לשאר המשתתפים",
"app.poll.dragDropPollInstruction": "כדי למלא את ערכי הסקר, יש לגרור קובץ טקסט עם ערכי הסקר אל השדה המודגש",
"app.poll.customPollTextArea": "מלאו את ערכי הסקר",
"app.poll.publishLabel": "פרסום הסקר",
"app.poll.cancelPollLabel": "ביטול",
"app.poll.backLabel": "התחלת סקר",
"app.poll.closeLabel": "סגירה",
"app.poll.waitingLabel": "מחכה למענה ({0}/{1})",
@ -177,12 +238,29 @@
"app.poll.customPlaceholder": "הוספת אפשרות מענה",
"app.poll.noPresentationSelected": "לא נבחרה מצגת! אנא בחרו אחת.",
"app.poll.clickHereToSelect": "לחיצה כאן לבחירה",
"app.poll.question.label" : "נא הקלידו את השאלה שלכם...",
"app.poll.optionalQuestion.label" : "נא הקלידו את השאלה שלכם (לא חובה)...",
"app.poll.userResponse.label" : "תגובת המשתמש",
"app.poll.responseTypes.label" : "סוגי תגובה",
"app.poll.optionDelete.label" : "מחיקה",
"app.poll.responseChoices.label" : "אפשרויות תגובה",
"app.poll.typedResponse.desc" : "למשתמשים תוצג תיבת טקסט למילוי תגובתם.",
"app.poll.addItem.label" : "הוספת פריט",
"app.poll.start.label" : "התחלת סקר",
"app.poll.secretPoll.label" : "סקר אנונימי",
"app.poll.secretPoll.isSecretLabel": "הסקר אנונימי. אין אפשרות לצפות בתגובות של משתמשים האחרים",
"app.poll.questionErr": "נדרשת מתן שאלה.",
"app.poll.optionErr": "אנא הוסיפו אפשרות סקר",
"app.poll.startPollDesc": "התחלת הסקר",
"app.poll.showRespDesc": "תצוגת הגדרת תגובה",
"app.poll.deleteRespDesc": "הסרת אפשרות {0}",
"app.poll.t": "אמת",
"app.poll.f": "שקר",
"app.poll.tf": "אמת / שקר",
"app.poll.y": "כן",
"app.poll.n": "לא",
"app.poll.abstention": "הימנעות",
"app.poll.yna": "כן / לא / הימנעות",
"app.poll.a2": "א / ב",
"app.poll.a3": "א / ב / ג",
"app.poll.a4": "א / ב / ג / ד",
@ -191,6 +269,7 @@
"app.poll.answer.false": "שקר",
"app.poll.answer.yes": "כן",
"app.poll.answer.no": "לא",
"app.poll.answer.abstention": "הימנעות",
"app.poll.answer.a": "א",
"app.poll.answer.b": "ב",
"app.poll.answer.c": "ג",
@ -198,17 +277,31 @@
"app.poll.answer.e": "ה",
"app.poll.liveResult.usersTitle": "משתתפים",
"app.poll.liveResult.responsesTitle": "תשובה",
"app.poll.liveResult.secretLabel": "זהו סקר אנונימי. תגובות אינן מוצגות",
"app.poll.removePollOpt": "הוסרה אפשרות סקר {0}",
"app.poll.emptyPollOpt": "ריק",
"app.polling.pollingTitle": "אפשרויות סקר",
"app.polling.pollQuestionTitle": "שאלת סקר",
"app.polling.submitLabel": "שליחה",
"app.polling.submitAriaLabel": "שליחת תגובה לסקר",
"app.polling.responsePlaceholder": "הוספת תשובה",
"app.polling.responseSecret": "סקר אנונימי מציג הסקר אינו יכול לראות את תשובתכם",
"app.polling.responseNotSecret": "סקר רגיל מציג הסקר יכול לראות את תשובתכם",
"app.polling.pollAnswerLabel": "מענה לסקר {0}",
"app.polling.pollAnswerDesc": "בחירת אפשרות זו לבחירה ב {0}",
"app.failedMessage": "מתנצלים, בעיה בחיבור לשרת.",
"app.downloadPresentationButton.label": "הורדת המצגת המקורית",
"app.connectingMessage": "מתחבר ...",
"app.waitingMessage": "נותקת, מנסה להתחבר בעוד {0} שניות...",
"app.retryNow": "נסו שנית",
"app.muteWarning.label": "נא הקישו על {0} כדי לבטל את ההשתקה.",
"app.muteWarning.disableMessage": "השתקת ההתראות מושבתת עד לביטול ההשתקה",
"app.muteWarning.tooltip": "נא הקישו על סגירה והשבתת אזהרה עד להשתקה הבאה",
"app.navBar.settingsDropdown.optionsLabel": "אפשרויות",
"app.navBar.settingsDropdown.fullscreenLabel": "מסך מלא",
"app.navBar.settingsDropdown.settingsLabel": "הגדרות",
"app.navBar.settingsDropdown.aboutLabel": "אודות",
"app.navBar.settingsDropdown.leaveSessionLabel": "עזיבת מפגש",
"app.navBar.settingsDropdown.exitFullscreenLabel": "יציאה ממסך מלא",
"app.navBar.settingsDropdown.fullscreenDesc": "מעבר ממסך ההגדרות למסך מלא",
"app.navBar.settingsDropdown.settingsDesc": "עדכון הגדרות כלליות",
@ -224,12 +317,16 @@
"app.navBar.userListToggleBtnLabel": "הצגת/הסתרת רשימת משתתפים",
"app.navBar.toggleUserList.ariaLabel": "הצגת/הסתרת רשימת הודעות ומשתתפים",
"app.navBar.toggleUserList.newMessages": "עם התראה על הודעות חדשות",
"app.navBar.toggleUserList.newMsgAria": "הודעה חדשה מ {0}",
"app.navBar.recording": "מפגש זה מוקלט",
"app.navBar.recording.on": "מקליט",
"app.navBar.recording.off": "לא מקליט",
"app.navBar.emptyAudioBrdige": "לא נמצא מיקרופון פעיל.",
"app.leaveConfirmation.confirmLabel": "יציאה",
"app.leaveConfirmation.confirmDesc": "התנתקות מהמפגש",
"app.endMeeting.description": "פעולה זו תסיים את ההפעלה עבור {0} משתמש(ים) פעיל(ים). האם אתם בטוחים שברצונכם לסיים את פגישה זו?",
"app.endMeeting.noUserDescription": "האם אתם בטוחים שאתם מעוניינים לסיים את המפגש?",
"app.endMeeting.contentWarning": "לא תהייה נגישות ישירה להודעות צ'אט, פתקיות משותפות, תוכן לוח ציור ומסמכים משותפים, עבור הפעלה זו.",
"app.endMeeting.yesLabel": "כן",
"app.endMeeting.noLabel": "לא",
"app.about.title": "אודות",
@ -255,6 +352,16 @@
"app.submenu.application.languageLabel": "שפה",
"app.submenu.application.languageOptionLabel": "בחירת שפה",
"app.submenu.application.noLocaleOptionLabel": "לא הוגדרה שפת ברירת מחדל",
"app.submenu.application.paginationEnabledLabel": "מספור סרטוני וידאו",
"app.submenu.application.layoutOptionLabel": "סגנון פריסה",
"app.submenu.notification.SectionTitle": "התראות",
"app.submenu.notification.Desc": "הגדירו כיצד ועל מה תקבלו התראה.",
"app.submenu.notification.audioAlertLabel": "התראות שמע",
"app.submenu.notification.pushAlertLabel": "התראות קופצות",
"app.submenu.notification.messagesLabel": "הודעת צ'אט",
"app.submenu.notification.userJoinLabel": "משתמש הצטרף",
"app.submenu.notification.userLeaveLabel": "משתמש עזב",
"app.submenu.notification.guestWaitingLabel": "אורח ממתין לאישור",
"app.submenu.audio.micSourceLabel": "מיקרופון",
"app.submenu.audio.speakerSourceLabel": "רמקול",
"app.submenu.audio.streamVolumeLabel": "עוצמת שמע",
@ -282,8 +389,11 @@
"app.switch.offLabel": "כן",
"app.talkingIndicator.ariaMuteDesc" : "בחירת השתקת משתמש",
"app.talkingIndicator.isTalking" : "{0} מדבר/ת",
"app.talkingIndicator.moreThanMaxIndicatorsTalking" : "{0}+ מדברים",
"app.talkingIndicator.moreThanMaxIndicatorsWereTalking" : "{0}+ דיברו",
"app.talkingIndicator.wasTalking" : "{0} הפסיק/ה לדבר",
"app.actionsBar.actionsDropdown.actionsLabel": "פעולות נוספות",
"app.actionsBar.actionsDropdown.presentationLabel": "ניהול מצגות",
"app.actionsBar.actionsDropdown.initPollLabel": "התחלת סקר",
"app.actionsBar.actionsDropdown.desktopShareLabel": "שיתוף מסך",
"app.actionsBar.actionsDropdown.lockedDesktopShareLabel": "שיתוף מסך לא מאופשר במפגש זה",
@ -301,6 +411,8 @@
"app.actionsBar.actionsDropdown.captionsDesc": "הצגת/הסתרת מסך הכתוביות",
"app.actionsBar.actionsDropdown.takePresenter": "הפכי/וך עצמך למנחת/ה המפגש",
"app.actionsBar.actionsDropdown.takePresenterDesc": "הפכי/וך עצמך למנחת/ה המפגש",
"app.actionsBar.actionsDropdown.selectRandUserLabel": "אנא בחרו משתמש אקראי",
"app.actionsBar.actionsDropdown.selectRandUserDesc": "אנא בחרו באופן אקראי משתמש מתוך הצופים הזמינים ",
"app.actionsBar.emojiMenu.statusTriggerLabel": "עריכת מצב",
"app.actionsBar.emojiMenu.awayLabel": "לא ליד המחשב",
"app.actionsBar.emojiMenu.awayDesc": "עריכת המצב שלך ל'לא ליד המחשב'",
@ -324,6 +436,14 @@
"app.actionsBar.currentStatusDesc": "מצב נוכחי {0}",
"app.actionsBar.captions.start": "הצגת כתוביות",
"app.actionsBar.captions.stop": "הסתרת כתוביות",
"app.audioNotification.audioFailedError1003": "גרסת הדפדפן אינה נתמכת (שגיאה 1003)",
"app.audioNotification.audioFailedError1005": "השיחה הסתיימה באופן בלתי צפוי (שגיאה 1005)",
"app.audioNotification.audioFailedError1006": "תם הזמן הקצוב לשיחה (שגיאה 1006)",
"app.audioNotification.audioFailedError1007": "כשל בחיבור (שגיאת ICE 1007)",
"app.audioNotification.audioFailedError1008": "ההעברה נכשלה (שגיאה 1008)",
"app.audioNotification.audioFailedError1011": "תם זמן החיבור (שגיאת ICE 1011)",
"app.audioNotification.audioFailedError1012": "החיבור נסגר (שגיאת ICE 1012)",
"app.audioNotification.audioFailedMessage": "חיבור האודיו נכשל",
"app.audioNotification.closeLabel": "סגירה",
"app.audioNotificaion.reconnectingAsListenOnly": "דיבור לא מאופשר במפגש זה, אתה מחובר במצב האזנה בלבד",
"app.breakoutJoinConfirmation.title": "הצטרפות לחדר למידה",
@ -334,15 +454,20 @@
"app.breakoutJoinConfirmation.freeJoinMessage": "בחירת חדר למידה שברצונך להצטרף אליו",
"app.breakoutTimeRemainingMessage": "זמן נותר לחדר הלמידה: {0}",
"app.breakoutWillCloseMessage": "נגמר הזמן. חדר הלמידה יסגר בקרוב",
"app.breakout.dropdown.manageDuration": "שינוי משך הזמן",
"app.breakout.dropdown.destroyAll": "סיום חדרי למידה",
"app.calculatingBreakoutTimeRemaining": "מחשב זמן נותר ...",
"app.audioModal.ariaTitle": "הצטרפות למפגש מקוון",
"app.audioModal.microphoneLabel": "מיקרופון",
"app.audioModal.listenOnlyLabel": "האזנה בלבד",
"app.audioModal.microphoneDesc": "הצטרפות למפגש קולי מקוון עם מיקרופון",
"app.audioModal.listenOnlyDesc": "הצטרפות למפגש קולי מקוון כמאזינים בלבד",
"app.audioModal.audioChoiceLabel": "האם להצטרף למפגש הקולי?",
"app.audioModal.iOSBrowser": "קול/וידאו לא נתמכים",
"app.audioModal.iOSErrorDescription": "כרגע וידאו וקול לא נתמכים בדפדפן כרום למכשירי אפל",
"app.audioModal.iOSErrorRecommendation": "מומלץ להשתמש בדפדפן ספארי על מכשיר זה",
"app.audioModal.audioChoiceDesc": "איך ברצונך להצטרך למפגש הקולי?",
"app.audioModal.unsupportedBrowserLabel": "נראה שאתם משתמשים בדפדפן שאינו נתמך במלואו. אנא השתמשו ב-{0} או ב-{1} לתמיכה מלאה.",
"app.audioModal.closeLabel": "סגירה",
"app.audioModal.yes": "כן",
"app.audioModal.no": "לא",
@ -351,22 +476,49 @@
"app.audioModal.echoTestTitle": "זו בדיקת הד פרטית. יש להגיד כמה מילים. האם שמעת הד ?",
"app.audioModal.settingsTitle": "עדכון הגדרות הקול שלך",
"app.audioModal.helpTitle": "אירעה שגיאה בציוד הקול/וידאו",
"app.audioModal.helpText": "האם נתתם אישור לגישה למיקרופון שלכם? שימו לב כי תיבת דו-שיח אמורה להופיע, כאשר אתה מנסה להצטרף למצב קולי , ובה בקשת גישה להרשאות מכשיר המדיה שלכם, אנא אשרו זאת על מנת שתוכלו להצטרף למפגש הקולי המקוון. אם זה לא המקרה, נסו לשנות את הרשאות המיקרופון שלכם בהגדרות הדפדפן שלכם.",
"app.audioModal.help.noSSL": "דף זה אינו מאובטח. כדי שתתאפשר גישה למיקרופון, הדף חייב להיות מוצג ב-HTTPS. אנא פנו למנהל השרת.",
"app.audioModal.help.macNotAllowed": "נראה שהעדפות מערכת ה-Mac שלכם חוסמות את הגישה למיקרופון שלכם. פתחו את העדפות מערכת > אבטחה ופרטיות > פרטיות > מיקרופון, וודאו שהדפדפן שבו אתם משתמשים, מסומן כפעיל.",
"app.audioModal.audioDialTitle": "הצטרפו באמצאות הנייד שלכם.",
"app.audioDial.audioDialDescription": "חיגו בבקשה",
"app.audioDial.audioDialConfrenceText": "נא הזינו את מספר ה-PIN של המפגש המקוון :",
"app.audioModal.autoplayBlockedDesc": "אנו זקוקים לרשותכם כדי לנגן שמע.",
"app.audioModal.playAudio": "נגינת צליל",
"app.audioModal.playAudio.arialabel" : "נגינת צליל",
"app.audioDial.tipIndicator": "עצה",
"app.audioDial.tipMessage": "לחיצה על מקש '0' בטלפון שלכם, תשתיק/תבבטל את ההשתקה.",
"app.audioModal.connecting": "יצירת חיבור שמע",
"app.audioManager.joinedAudio": "הצטרפות למפגש הקולי המקוון",
"app.audioManager.leftAudio": "עזיבת המפגש הקולי המקוון",
"app.audioManager.reconnectingAudio": "נסיון בחיבור חדש של הצליל",
"app.audioManager.genericError": "שגיאה: אירעה שגיאה, אנא נסו שוב",
"app.audioManager.connectionError": "שגיאה: שגיאת חיבור",
"app.audioManager.mediaError": "שגיאה: הייתה בעיה בהשגת מכשירי המדיה שלכם",
"app.audio.joinAudio": "הצטרפות למפגש קולי",
"app.audio.leaveAudio": "עזיבת המפגש הקולי",
"app.audio.changeAudioDevice": "שינוי התקן קולי",
"app.audio.enterSessionLabel": "כניסה למפגש",
"app.audio.playSoundLabel": "נגינת צליל",
"app.audio.backLabel": "חזרה",
"app.audio.loading": "טעינה",
"app.audio.microphones": "מיקרופונים",
"app.audio.speakers": "רמקולים",
"app.audio.noDeviceFound": "לא נמצא מכשיר",
"app.audio.audioSettings.titleLabel": "בחירת הגדרות הקול",
"app.audio.audioSettings.descriptionLabel": "שים לב, יופיע חלון דו-שיח בדפדפן שלכם, המחייב אתכם לאשר את שיתוף המיקרופון שלכם.",
"app.audio.audioSettings.microphoneSourceLabel": "מיקרופון",
"app.audio.audioSettings.speakerSourceLabel": "רמקולים",
"app.audio.audioSettings.testSpeakerLabel": "בדיקת הרמקול שלכם",
"app.audio.audioSettings.microphoneStreamLabel": "עוצמת שמע",
"app.audio.audioSettings.retryLabel": "ניסיון חוזר",
"app.audio.listenOnly.backLabel": "חזרה",
"app.audio.listenOnly.closeLabel": "סגירה",
"app.audio.permissionsOverlay.title": "אפשרו גישה למיקרופון שלכם",
"app.audio.permissionsOverlay.hint": "אנחנו צריכים שתאפשרו לנו להשתמש במכשירי המדיה שלכם כדי להצטרף אליכם למפגש הקולי :)",
"app.error.removed": "הוסרתם מהמפגש המקוון",
"app.error.meeting.ended": "התנתקתם מהמפגש המקוון",
"app.meeting.logout.permissionEjectReason": "פסילה עקב הפרת הרשאה",
"app.meeting.logout.ejectedFromMeeting": "הוסרתם מהמפגש",
"app.meeting.logout.userInactivityEjectReason": "המשתמש לא היה פעיל במשך זמן רב",
"app.meeting-ended.rating.legendLabel": "דרוג משוב",
"app.meeting-ended.rating.starLabel": "כוכב",
@ -375,10 +527,35 @@
"app.modal.confirm": "הושלם",
"app.modal.newTab": "(פתיחה בלשונית חדשה)",
"app.modal.confirm.description": "שמירת שינויים וסגירת חלונית",
"app.modal.randomUser.noViewers.description": "אין צופים זמינים שניתן לבחור אקראית מהם",
"app.modal.randomUser.selected.description": "נבחרתם באופן אקראי",
"app.modal.randomUser.title": "משתמש שנבחר באקראי",
"app.modal.randomUser.who": "מי ייבחר..?",
"app.modal.randomUser.alone": "ישנו צופה אחד בלבד",
"app.modal.randomUser.reselect.label": "אנא בחרו שוב",
"app.dropdown.close": "סגירה",
"app.dropdown.list.item.activeLabel": "פעיל",
"app.error.401": "לא מורשה",
"app.error.403": "הוסרתם מהמפגש",
"app.error.404": "לא נמצאו",
"app.error.408": "אימות נכשל",
"app.error.410": "המפגש הסתיים",
"app.error.500": "אופס, משהו השתבש",
"app.error.joinedAnotherWindow": "נראה שהמפגש הזה נפתח בחלון דפדפן אחר.",
"app.error.leaveLabel": "התחברו שוב",
"app.error.fallback.presentation.title": "התרחשה שגיאה",
"app.error.fallback.presentation.reloadButton": "טעינה מחדש",
"app.guest.waiting": "מחכים לאישורך להצטרף למפגש",
"app.guest.noModeratorResponse": "אין תגובה מהמנחה",
"app.guest.missingMeeting": "המפגש לא מתקיים",
"app.guest.meetingEnded": "המפגש הסתיים",
"app.guest.guestWait": "המתינו למנחה שיאשר את הצטרפותכם לפגישה.",
"app.guest.guestDeny": "הכחשת אורח להצטרפות למפגישה",
"app.guest.allow": "האורח אושר והופנה למפגש",
"app.guest.firstPositionInWaitingQueue": "אתם הראשונים בתור!",
"app.guest.positionInWaitingQueue": "המיקום הנוכחי שלכם בתור :",
"app.guest.guestInvalid": "משתמש אורח אינו חוקי",
"app.guest.meetingForciblyEnded": "איניכם יכולים להצטרף לפגישה שהסתיימה",
"app.userList.guest.waitingUsers": "משתמשים ממתינים",
"app.userList.guest.waitingUsersTitle": "ניהול משתמשים",
"app.userList.guest.optionTitle": "סקירת משתמשים ממתינים",
@ -387,9 +564,14 @@
"app.userList.guest.allowEveryone": "אישור לכולם",
"app.userList.guest.denyEveryone": "מניעה מכולם",
"app.userList.guest.pendingUsers": "{0} משתמשים ממתינים",
"app.userList.guest.noPendingUsers": "כרגע אין משתמשים שממתינים להצטרף..",
"app.userList.guest.pendingGuestUsers": "{0} משתתפים ממתינים להצטרף",
"app.userList.guest.pendingGuestAlert": "הצטרף/ה למפגש ומחכה לאישורך.",
"app.userList.guest.rememberChoice": "זכירת בחירה",
"app.userList.guest.emptyMessage": "כרגע אין הודעה",
"app.userList.guest.privateMessageLabel": "הודעה",
"app.userList.guest.acceptLabel": "אושר",
"app.userList.guest.denyLabel": "נדחה",
"app.user-info.title": "חיפוש תיקיה",
"app.toast.breakoutRoomEnded": "מפגש חדר למידה הסתיים. בבקשה הצטרפו מחדש למפגש הקולי.",
"app.toast.chat.public": "הודעת רב־שיח ציבורי חדשה",
@ -410,9 +592,12 @@
"app.shortcut-help.hidePrivateChat": "הסתרת רב־שיח פרטי",
"app.shortcut-help.closePrivateChat": "סגירת שיחה פרטית",
"app.shortcut-help.openActions": "פתיחת תפריט הפעולות",
"app.shortcut-help.openDebugWindow": "פתיחת חלון של ניפוי שגיאות",
"app.shortcut-help.openStatus": "פתיחת תפריט מצבים",
"app.shortcut-help.nextSlideDesc": "עמוד הבאה (מנחה)",
"app.shortcut-help.previousSlideDesc": "עמוד קודם (מנחה)",
"app.shortcut-help.nextSlideKey": "חץ ימני",
"app.shortcut-help.previousSlideKey": "חץ שמאלי",
"app.lock-viewers.title": "נעילת הגדרות משתתפים",
"app.lock-viewers.description": "מסך זה מאפשר לך לקבע את מצב השיתוף של הצופים.",
"app.lock-viewers.featuresLable": "תכונה",
@ -428,6 +613,9 @@
"app.lock-viewers.button.apply": "אישור",
"app.lock-viewers.button.cancel": "ביטול",
"app.lock-viewers.locked": "לא מאופשר",
"app.guest-policy.title": "מדיניות אורחים",
"app.guest-policy.description": "שינוי הגדרת המדיניות של אורחים במפגש",
"app.guest-policy.button.askModerator": "שאלה למנהל/ת",
"app.recording.startTitle": "התחלת הקלטה",
"app.recording.stopTitle": "השהית הקלטה",
"app.recording.resumeTitle": "הפעלת הקלטה מחדש",
@ -525,7 +713,10 @@
"app.externalVideo.input": "כתובת וידאו חיצוני URL",
"app.externalVideo.urlInput": "הוספת כתובת וידאו URL",
"app.externalVideo.close": "סגירה",
"playback.player.chat.wrapper.aria": "אזור רב־שיח"
"playback.player.chat.wrapper.aria": "אזור רב־שיח",
"app.learningDashboard.pollsTable.title": "סקרים",
"app.learningDashboard.pollsTable.anonymousRowName": "אנונימי",
"app.learningDashboard.statusTimelineTable.title": "ציר הזמן"
}

View File

@ -203,7 +203,7 @@
"app.presentation.presentationToolbar.goToSlide": "{0}. dia",
"app.presentation.placeholder": "Jelenleg nincs aktív előadás",
"app.presentationUploder.title": "Prezentáció",
"app.presentationUploder.message": "Előadóként tetszőleges office dokumentumot, illetve PDF fájlt fel tudsz tölteni. A legjobb eredmény érdekében javasoljuk PDF fájl használatát. Kérjük, ellenőrizd, hogy egy prezentációt kiválasztottál a jobb oldalon lévő jelölővel. ",
"app.presentationUploder.message": "Előadóként tetszőleges office dokumentumot, illetve PDF fájlt fel tudsz tölteni. A legjobb eredmény érdekében javasoljuk PDF fájl használatát. Kérjük, ellenőrizd, hogy egy prezentációt kiválasztottál a jobb oldalon lévő kör alakú jelölővel. ",
"app.presentationUploder.extraHint": "FONTOS: egyik fájl sem érheti el {0} MB-ot és {1} oldalt.",
"app.presentationUploder.uploadLabel": "Feltöltés",
"app.presentationUploder.confirmLabel": "Jóváhagyás",

View File

@ -173,6 +173,7 @@
"app.presentation.options.fullscreen": "全画面表示",
"app.presentation.options.exitFullscreen": "全画面表示解除",
"app.presentation.options.minimize": "最小化",
"app.presentation.options.snapshot": "現在のスライドのスナップショット",
"app.presentation.options.downloading": "ダウンロード中...",
"app.presentation.options.downloaded": "プレゼンファイルをダウンロードしました",
"app.presentation.options.downloadFailed": "プレゼンファイルがダウンロードできませんでした",

View File

@ -6,6 +6,7 @@
"app.chat.disconnected": "Jesteś rozłączony, wiadomości nie mogą zostać wysłane",
"app.chat.locked": "Czat jest zablokowany, nie można wysyłać wiadomości",
"app.chat.inputLabel": "Wyślij wiadomość do {0}",
"app.chat.inputPlaceholder": "Wiadomość {0}",
"app.chat.titlePublic": "Czat Publiczny",
"app.chat.titlePrivate": "Czat Prywatny z {0}",
"app.chat.partnerDisconnected": "{0} opuścił(a) spotkanie ",
@ -24,6 +25,8 @@
"app.chat.multi.typing": "Kilku uczestników teraz pisze",
"app.chat.one.typing": "{0} teraz pisze",
"app.chat.two.typing": "{0} oraz {1} teraz piszą",
"app.chat.copySuccess": "Skopiowano transkrypcję czatu",
"app.chat.copyErr": "Kopiowanie transkrypcji czatu nie powiodło się",
"app.captions.label": "Napisy",
"app.captions.menu.close": "Zamknij",
"app.captions.menu.start": "Rozpocznij",
@ -166,6 +169,7 @@
"app.presentation.presentationToolbar.fitToWidth": "Dopasuj do szerokości",
"app.presentation.presentationToolbar.fitToPage": "Dopasuj do strony",
"app.presentation.presentationToolbar.goToSlide": "Slajd {0}",
"app.presentation.placeholder": "Obecnie nikt nie prowadzi prezentacji",
"app.presentationUploder.title": "Prezentacja",
"app.presentationUploder.message": "Jako prezenter masz możliwość wgrania dowolnego dokumentu lub pliku PDF. Dla uzyskania lepszych rezultatów, zalecamy użycie pliku PDF. Upewnij się, że właściwa prezentacja jest wybrana. Wybierasz prezentację klikając okrągłe pole wyboru po jej prawej stronie.",
"app.presentationUploder.uploadLabel": "Prześlij",
@ -180,6 +184,7 @@
"app.presentationUploder.fileToUpload": "Do przesłania...",
"app.presentationUploder.currentBadge": "Bieżąca",
"app.presentationUploder.rejectedError": "Plik(i) odrzucono. Sprawdź typ pliku(ów)",
"app.presentationUploder.connectionClosedError": "Błąd z powodu słabej jakości połączenia. Spróbuj ponownie.",
"app.presentationUploder.upload.progress": "Przesyłanie ({0}%)",
"app.presentationUploder.upload.413": "Plik jest za duży, przekracza maksymalny rozmiar {0} MB",
"app.presentationUploder.genericError": "Ups, coś poszło nie tak ...",
@ -328,6 +333,7 @@
"app.actionsBar.label": "Pasek akcji",
"app.actionsBar.actionsDropdown.restorePresentationLabel": "Przywróć prezentację",
"app.actionsBar.actionsDropdown.minimizePresentationLabel": "Zminimalizuj prezentację",
"app.actionsBar.actionsDropdown.minimizePresentationDesc": "Przycisk używany do zminimalizowania prezentacji",
"app.screenshare.screenShareLabel" : "Udostępnianie ekranu",
"app.submenu.application.applicationSectionTitle": "Aplikacja",
"app.submenu.application.animationsLabel": "Animacje",
@ -372,6 +378,7 @@
"app.settings.dataSavingTab.description": "Aby ograniczyć wykorzystanie połączenia internetowego włącz lub wyłącz poniższe opcje.",
"app.settings.save-notification.label": "Ustawienia zostały zapisane",
"app.statusNotifier.lowerHands": "Opuść ręce",
"app.statusNotifier.lowerHandDescOneUser": "Opuść rękę dla {0}",
"app.statusNotifier.raisedHandsTitle": "Podniesione ręce",
"app.statusNotifier.raisedHandDesc": "{0} podnieśli ręce",
"app.statusNotifier.raisedHandDescOneUser": "{0} podniósł rękę",
@ -380,6 +387,8 @@
"app.switch.offLabel": "WYŁ",
"app.talkingIndicator.ariaMuteDesc" : "Wybierz aby wyciszyć uczestnika",
"app.talkingIndicator.isTalking" : "{0} mówi",
"app.talkingIndicator.moreThanMaxIndicatorsTalking" : "{0}+ mówi",
"app.talkingIndicator.moreThanMaxIndicatorsWereTalking" : "{0}+ mówiło",
"app.talkingIndicator.wasTalking" : "{0} przestał mówić",
"app.actionsBar.actionsDropdown.actionsLabel": "Akcje",
"app.actionsBar.actionsDropdown.presentationLabel": "Zarządzaj prezentacjami",
@ -571,6 +580,7 @@
"app.userList.guest.rememberChoice": "Zapamiętaj wybór",
"app.userList.guest.emptyMessage": "Obecnie brak wiadomości",
"app.userList.guest.inputPlaceholder": "Wiadomość do poczekalni gości",
"app.userList.guest.privateInputPlaceholder": "Wiadomość do {0}",
"app.userList.guest.privateMessageLabel": "Wiadomość",
"app.userList.guest.acceptLabel": "Akceptuj",
"app.userList.guest.denyLabel": "Odrzuć",
@ -589,6 +599,7 @@
"app.notification.recordingPaused": "Przerwa w rejestrowaniu sesji",
"app.notification.recordingAriaLabel": "Czas nagrania",
"app.notification.userJoinPushAlert": "{0} dołączył(a) do sesji",
"app.notification.userLeavePushAlert": "{0} opuścił(a) sesję",
"app.submenu.notification.raiseHandLabel": "Podnieś rękę",
"app.shortcut-help.title": "Skróty klawiszowe",
"app.shortcut-help.accessKeyNotAvailable": "Brak kluczy dostępowych",
@ -694,6 +705,7 @@
"app.video.pagination.prevPage": "Zobacz poprzednie video",
"app.video.pagination.nextPage": "Zobacz kolejne video",
"app.video.clientDisconnected": "Obraz kamery nie może zostać udostępniony z powodu problemów z połączeniem",
"app.video.virtualBackground.camBgAriaDesc": "Ustaw wirtualne tło kamery na {0}",
"app.fullscreenButton.label": "Przełącz {0} na pełny ekran",
"app.fullscreenUndoButton.label": "Cofnij {0} pełny ekran",
"app.sfu.mediaServerConnectionError2000": "Nie można połączyć się z serwerem multimediów (błąd 2000)",

View File

@ -173,6 +173,7 @@
"app.presentation.options.fullscreen": "Tam ekrana geç",
"app.presentation.options.exitFullscreen": "Tam ekrandan çık",
"app.presentation.options.minimize": "Küçült",
"app.presentation.options.snapshot": "Geçerli slaytın ekran görüntüsü",
"app.presentation.options.downloading": "İndiriliyor...",
"app.presentation.options.downloaded": "Geçerli sunum indirildi",
"app.presentation.options.downloadFailed": "Geçerli sunum indirilemedi",

View File

@ -6,6 +6,7 @@
"app.chat.disconnected": "Ви від'єднались, повідомлення не можуть бути надіслані",
"app.chat.locked": "Чат заблоковано, неможливо надіслати повідомлення",
"app.chat.inputLabel": "Текст повідомлення у чаті з {0}",
"app.chat.inputPlaceholder": "Повідомлення {0}",
"app.chat.titlePublic": "Загальний чат",
"app.chat.titlePrivate": "Приватний чат з {0}",
"app.chat.partnerDisconnected": "{0} вийшов з конференції",
@ -19,6 +20,7 @@
"app.chat.label": "Чат",
"app.chat.offline": "Не в мережі",
"app.chat.pollResult": "Результати опитування",
"app.chat.breakoutDurationUpdated": "Час перерви зараз - {0} хвилин",
"app.chat.emptyLogLabel": "Журнал чату порожній",
"app.chat.clearPublicChatMessage": "Історію загального чату очищено модератором",
"app.chat.multi.typing": "Учасники пишуть",
@ -41,8 +43,23 @@
"app.captions.menu.backgroundColor": "Колір фону",
"app.captions.menu.previewLabel": "Попередній перегляд",
"app.captions.menu.cancelLabel": "Скасувати",
"app.captions.hide": "Сховати субтитри",
"app.captions.ownership": "Перехоплення",
"app.captions.ownershipTooltip": "Вас призначитимуть володарем {0} субтитрів",
"app.captions.dictationStart": "Почати диктування",
"app.captions.dictationStop": "Зупинити диктування",
"app.captions.dictationOnDesc": "Вмикає ропізнавання голосу",
"app.captions.dictationOffDesc": "Вимикає розпізнавання голосу",
"app.captions.speech.start": "Розпочато розпізнавання голосу",
"app.captions.speech.stop": "Зупинено розпізнавання голосу",
"app.captions.speech.error": "Розпізнавання голосу зупинено через несумісний браузер чи тривалий час тиші",
"app.textInput.sendLabel": "Відіслати",
"app.title.defaultViewLabel": "Стандартний вигляд презентації",
"app.notes.title": "Спільні нотатки",
"app.notes.label": "Нотатки",
"app.notes.hide": "Сховати нотатки",
"app.notes.locked": "Заблоковано",
"app.pads.hint": "Натисніть Esc для переходу у панель інструментів",
"app.user.activityCheck": "Перевірка активності учасника",
"app.user.activityCheck.label": "Перевірте, чи учасник зараз на зустрiчi ({0})",
"app.user.activityCheck.check": "Перевірка",
@ -101,6 +118,7 @@
"app.userList.userOptions.disableMic": "Мікрофони учасників вимкнено",
"app.userList.userOptions.disablePrivChat": "Приватний чат вимкнено",
"app.userList.userOptions.disablePubChat": "Загальний чат вимкнено",
"app.userList.userOptions.disableNotes": "Спільні нотатки заблоковано",
"app.userList.userOptions.hideUserList": "Список учасників приховано від гостей",
"app.userList.userOptions.webcamsOnlyForModerator": "Вебкамери учасників можуть бачити лише ведучі (через налаштування блокування)",
"app.userList.content.participants.options.clearedStatus": "Статуси учасників знято",
@ -108,11 +126,14 @@
"app.userList.userOptions.enableMic": "Мікрофони учасників увімкнено",
"app.userList.userOptions.enablePrivChat": "Приватний чат увімкнено",
"app.userList.userOptions.enablePubChat": "Загальний чат увімкнено",
"app.userList.userOptions.enableNotes": "Спільні нотатки увімкнені",
"app.userList.userOptions.showUserList": "Список учасників тепер видимий гостям",
"app.userList.userOptions.enableOnlyModeratorWebcam": "Тепер можна активувати вебкамеру, всі бачитимуть вас",
"app.userList.userOptions.savedNames.title": "Список користувачів у зустрічі {0} на {1}",
"app.userList.userOptions.sortedFirstName.heading": "Відсортовано за Ім'ям:",
"app.userList.userOptions.sortedLastName.heading": "Відсортовано за Прізвищем:",
"app.userList.userOptions.hideViewersCursor": "Курсори глядачів заблоковано",
"app.userList.userOptions.showViewersCursor": "Курсори глядачів розблоковано",
"app.media.label": "Мультимедії",
"app.media.autoplayAlertDesc": "Дозволити доступ",
"app.media.screenshare.start": "Демонстрація екрану розпочалася",
@ -149,6 +170,13 @@
"app.presentation.endSlideContent": "Кінець вмісту слайду",
"app.presentation.changedSlideContent": "Слайд: {0}",
"app.presentation.emptySlideContent": "Даний слайд порожній",
"app.presentation.options.fullscreen": "Повний екран",
"app.presentation.options.exitFullscreen": "Вихід з повного екрану",
"app.presentation.options.minimize": "Мінімізувати",
"app.presentation.options.snapshot": "Знімок поточного слайду",
"app.presentation.options.downloading": "Завантаження...",
"app.presentation.options.downloaded": "Завантажено поточну презентацію",
"app.presentation.options.downloadFailed": "Не можливо завантажити поточну презентацію",
"app.presentation.presentationToolbar.noNextSlideDesc": "Кінець презентації",
"app.presentation.presentationToolbar.noPrevSlideDesc": "Початок презентації",
"app.presentation.presentationToolbar.selectLabel": "Вибрати слайд",
@ -173,6 +201,7 @@
"app.presentation.presentationToolbar.fitToWidth": "Умістити за шириною",
"app.presentation.presentationToolbar.fitToPage": "Умістити на сторінку",
"app.presentation.presentationToolbar.goToSlide": "Слайд {0}",
"app.presentation.placeholder": "Немає активної презентації",
"app.presentationUploder.title": "Презентація",
"app.presentationUploder.message": "Ведучий може завантажувати будь-який документ офісного формату, включно PDF. Ми рекомендуємо завантажувати презентації саме у форматі PDF. Після завантаження поставте прапорець навпроти імені файлу, який ви хочете показати учасникам.",
"app.presentationUploder.extraHint": "Важливо: кожен файл має бути не більший за {0} MB та {1} сторінок",
@ -188,6 +217,7 @@
"app.presentationUploder.fileToUpload": "Буде завантажено ...",
"app.presentationUploder.currentBadge": "Поточний",
"app.presentationUploder.rejectedError": "Неможливо завантажити вибрані файл(и). Перевірте тип файлу(iв).",
"app.presentationUploder.connectionClosedError": "Перервано через поганий зв'язок. Спробуйте пізніше",
"app.presentationUploder.upload.progress": "Завантаження ({0}%)",
"app.presentationUploder.upload.413": "Файл надто великий, розмір перевищує допустимі {0} МБ",
"app.presentationUploder.genericError": "Ой лишенько! Щось пішло не так ...",
@ -222,6 +252,7 @@
"app.presentationUploder.clearErrorsDesc": "Очищує помилки завантажень презентацій",
"app.presentationUploder.uploadViewTitle": "Завантажити презентацію",
"app.poll.pollPaneTitle": "Опитування",
"app.poll.enableMultipleResponseLabel": "Дозволити декілька відповідей від опитуваних?",
"app.poll.quickPollTitle": "Швидке опитування",
"app.poll.hidePollDesc": "Ховає панель меню опитувань",
"app.poll.quickPollInstruction": "Виберіть типовий шаблон опитування.",
@ -332,6 +363,7 @@
"app.endMeeting.noLabel": "Ні",
"app.about.title": "Про застосунок",
"app.about.version": "Збірка клієнта:",
"app.about.version_label": "BigBlueButton версія:",
"app.about.copyright": "Авторське право:",
"app.about.confirmLabel": "ОК",
"app.about.confirmDesc": "ОК",
@ -392,6 +424,7 @@
"app.settings.dataSavingTab.description": "Для заощадження передачі даних, будь ласка, вимкніть функції, які пов'язані з демонстрацією відео:",
"app.settings.save-notification.label": "Налаштування збережено",
"app.statusNotifier.lowerHands": "Опустити руки",
"app.statusNotifier.lowerHandDescOneUser": "Опустити {0} руку",
"app.statusNotifier.raisedHandsTitle": "Підняті руки",
"app.statusNotifier.raisedHandDesc": "{0} підняли руки",
"app.statusNotifier.raisedHandDescOneUser": "{0} підняв руку",
@ -473,6 +506,9 @@
"app.breakoutJoinConfirmation.freeJoinMessage": "Виберіть кімнату для перерви , до якої бажаєте під’єднатися",
"app.breakoutTimeRemainingMessage": "Час перерви у кімнаті : {0}",
"app.breakoutWillCloseMessage": "Час вичерпано. Конференцію невдовзі буде закрито",
"app.breakout.dropdown.manageDuration": "Змінити протяжність",
"app.breakout.dropdown.destroyAll": "Закінчити кімнати перерв",
"app.breakout.dropdown.options": "Опції перерви",
"app.calculatingBreakoutTimeRemaining": "Підрахунок часу, що залишився...",
"app.audioModal.ariaTitle": "Вікно підключення до голосової конференції",
"app.audioModal.microphoneLabel": "Мікрофон",
@ -528,6 +564,7 @@
"app.audio.audioSettings.descriptionLabel": "У вашому браузері з'явиться вікно із запитом на доступ до мікрофона. Вам потрібно його підтвердити.",
"app.audio.audioSettings.microphoneSourceLabel": "Джерело мікрофона",
"app.audio.audioSettings.speakerSourceLabel": "Пристрій відтворення",
"app.audio.audioSettings.testSpeakerLabel": "Перевірте динаміки",
"app.audio.audioSettings.microphoneStreamLabel": "Гучність вашого звукового потоку",
"app.audio.audioSettings.retryLabel": "Повторити",
"app.audio.listenOnly.backLabel": "Назад",
@ -566,6 +603,7 @@
"app.error.500": "Ой, щось пішло не так",
"app.error.userLoggedOut": "Користувач має невірний sessionToken через те що вилогінився",
"app.error.ejectedUser": "Користувач має невірний sessionToken через викид",
"app.error.joinedAnotherWindow": "Схоже що ця сесія відкрита у іншому вікні браузера",
"app.error.userBanned": "Користувача забанено",
"app.error.leaveLabel": "Увійдіть знову",
"app.error.fallback.presentation.title": "Виникла помилка",
@ -584,6 +622,10 @@
"app.guest.guestDeny": "Гостю відмовлено у приєднанні до зустрічі.",
"app.guest.seatWait": "Гість очікує на вільне місце у зустрічі.",
"app.guest.allow": "Гостя підтверджено та скеровано до зустрічі",
"app.guest.firstPositionInWaitingQueue": "Ви перші у черзі",
"app.guest.positionInWaitingQueue": "Ваша черга",
"app.guest.guestInvalid": "Невірний запрошений гість",
"app.guest.meetingForciblyEnded": "Зустріч зупинено",
"app.userList.guest.waitingUsers": "Учасники у очікуванні",
"app.userList.guest.waitingUsersTitle": "Керування учасниками",
"app.userList.guest.optionTitle": "Переглянути учасників, які очікують",
@ -592,11 +634,14 @@
"app.userList.guest.allowEveryone": "Дозволити всім",
"app.userList.guest.denyEveryone": "Заборонити всім",
"app.userList.guest.pendingUsers": "{0} учасників у очікуванні",
"app.userList.guest.noPendingUsers": "Немає користувачів у очікуванні...",
"app.userList.guest.pendingGuestUsers": "{0} гостей в очікуванні",
"app.userList.guest.pendingGuestAlert": "Приєднався до сеансу та очікує вашого схвалення",
"app.userList.guest.rememberChoice": "Запам'ятати вибір",
"app.userList.guest.emptyMessage": "Немає повідомлень",
"app.userList.guest.inputPlaceholder": "Повідомлення для гостьової кімнати очікування",
"app.userList.guest.privateInputPlaceholder": "Повідомлення до {0}",
"app.userList.guest.privateMessageLabel": "Повідомлення",
"app.userList.guest.acceptLabel": "Прийняти",
"app.userList.guest.denyLabel": "Відмовити",
"app.user-info.title": "Пошук у каталозі",
@ -609,6 +654,9 @@
"app.toast.meetingMuteOn.label": "Всім учасникам вимкнено мікрофони",
"app.toast.meetingMuteOff.label": "Блокування мікрофону вимкнено",
"app.toast.setEmoji.raiseHand": "Ви підняли руку",
"app.toast.setEmoji.lowerHand": "Руку опущено",
"app.toast.promotedLabel": "Вас підвищено до статусу Модератор",
"app.toast.demotedLabel": "Вас понижено до статусу Переглядач",
"app.notification.recordingStart": "Цей сеанс записується",
"app.notification.recordingStop": "Цей сеанс не записується",
"app.notification.recordingPaused": "Цей сеанс більше не записується",
@ -636,6 +684,10 @@
"app.shortcut-help.toggleFullscreen": "Переключити - На весь екран (ведучий)",
"app.shortcut-help.nextSlideDesc": "Наступний слайд (ведучий)",
"app.shortcut-help.previousSlideDesc": "Попередній слайд (ведучий)",
"app.shortcut-help.togglePanKey": "Пробіл",
"app.shortcut-help.toggleFullscreenKey": "Enter",
"app.shortcut-help.nextSlideKey": "Права стрілка",
"app.shortcut-help.previousSlideKey": "Ліва стрілка",
"app.lock-viewers.title": "Обмежити глядачів",
"app.lock-viewers.description": "Ці налаштування дозволяють обмежити учасників у доступі до певних функцій",
"app.lock-viewers.featuresLable": "Властивість",
@ -651,6 +703,7 @@
"app.lock-viewers.button.apply": "Застосувати",
"app.lock-viewers.button.cancel": "Скасувати",
"app.lock-viewers.locked": "Заблокований",
"app.lock-viewers.hideViewersCursor": "Показати курсори інших користувачів",
"app.guest-policy.ariaTitle": "Вікно налаштування політики для гостей",
"app.guest-policy.title": "Гостьова політика",
"app.guest-policy.description": "Змінити налаштування гостьової політики зустрічі",
@ -663,15 +716,26 @@
"app.connection-status.description": "Перегляд стану з'єднання користувачів",
"app.connection-status.empty": "Немає проблем із зв'язком",
"app.connection-status.more": "докладно",
"app.connection-status.copy": "Копіювати статистику",
"app.connection-status.copied": "Скопійовано!",
"app.connection-status.jitter": "Тремтіння",
"app.connection-status.label": "Стан з'єднання",
"app.connection-status.settings": "Налаштування ваших параметрів",
"app.connection-status.no": "Ні",
"app.connection-status.notification": "Ваше під'єднання має втрати",
"app.connection-status.offline": "не в мережі",
"app.connection-status.audioUploadRate": "Якість відправки аудіо",
"app.connection-status.audioDownloadRate": "Якість завантаження аудіо",
"app.connection-status.videoUploadRate": "Якість відправки відео",
"app.connection-status.videoDownloadRate": "Якість завантаження відео",
"app.connection-status.lostPackets": "Втрачені пакети",
"app.connection-status.usingTurn": "Використання TURN",
"app.connection-status.yes": "Так",
"app.connection-status.connectionStats": "Статистика конектів",
"app.connection-status.myLogs": "Мої логи",
"app.connection-status.sessionLogs": "Логи сесії",
"app.connection-status.next": "Наступна сторінка",
"app.connection-status.prev": "Попередня сторінка",
"app.learning-dashboard.label": "Дошка аналітики навчання",
"app.learning-dashboard.description": "Відкрити панель обліку активності учасників",
"app.learning-dashboard.clickHereToOpen": "Відкрити дошку аналітики навчання",
@ -738,10 +802,12 @@
"app.video.virtualBackground.blur": "Розмивання",
"app.video.virtualBackground.home": "Будинок",
"app.video.virtualBackground.board": "Дошка",
"app.video.virtualBackground.coffeeshop": "Кав`ярня",
"app.video.virtualBackground.coffeeshop": "Кав'ярня",
"app.video.virtualBackground.background": "Задній фон",
"app.video.virtualBackground.genericError": "Не вдалось застосувати ефект для камери. Спробуйте знову.",
"app.video.virtualBackground.camBgAriaDesc": "Встановлює віртуальне тло для вебкамери: {0}",
"app.video.camCapReached": "Ви не можете поширити більше камер",
"app.video.meetingCamCapReached": "Досягнуто ліміт камер у зустрічі",
"app.video.dropZoneLabel": "Перетягніть сюди",
"app.fullscreenButton.label": "Вивести {0} на весь екран",
"app.fullscreenUndoButton.label": "Вимкнути {0} режим на весь екран",
@ -831,7 +897,11 @@
"app.createBreakoutRoom.durationInMinutes": "Тривалість (хвилини)",
"app.createBreakoutRoom.randomlyAssign": "Випадково призначити",
"app.createBreakoutRoom.randomlyAssignDesc": "Випадково розподілити користувачів по кімнатах",
"app.createBreakoutRoom.resetAssignments": "Суинути призначення",
"app.createBreakoutRoom.resetAssignmentsDesc": "Скинути усі призначення кімнати",
"app.createBreakoutRoom.endAllBreakouts": "Закрити усі перервні кімнати ",
"app.createBreakoutRoom.chatTitleMsgAllRooms": "усі кімнати",
"app.createBreakoutRoom.msgToBreakoutsSent": "Повідомлення надіслано до {0} кімнат перерв",
"app.createBreakoutRoom.roomName": "{0} (Кімната - {1})",
"app.createBreakoutRoom.doneLabel": "Готово",
"app.createBreakoutRoom.nextLabel": "Далі",
@ -846,6 +916,10 @@
"app.createBreakoutRoom.numberOfRoomsError": "Кількість кімнат є неправильною.",
"app.createBreakoutRoom.duplicatedRoomNameError": "Назва кімнати не може повторюватись",
"app.createBreakoutRoom.emptyRoomNameError": "Назва кімнати не може бути порожньою",
"app.createBreakoutRoom.setTimeInMinutes": "Протяжність перерви (minutes)",
"app.createBreakoutRoom.setTimeLabel": "Застосувати",
"app.createBreakoutRoom.setTimeCancel": "Відмінити",
"app.createBreakoutRoom.setTimeHigherThanMeetingTimeError": "Чвс перерви не може перевищувати час зустрічі",
"app.createBreakoutRoom.roomNameInputDesc": "Оновлює назву кімнати для учасників",
"app.externalVideo.start": "Поділитися новим відео",
"app.externalVideo.title": "Поділитися зовнішнім відео",
@ -857,6 +931,8 @@
"app.externalVideo.refreshLabel": "Оновити програвач відео",
"app.externalVideo.fullscreenLabel": "Переглядач відео",
"app.externalVideo.noteLabel": "Примітка: Зовнішні відеозаписи не з'являються у записі сеансу. Підтримуються: YouTube, Vimeo, Instructure Media, Twitch, Dailymotion та посилання на відео ( наприклад : https://example.com/xy.mp4 )",
"app.externalVideo.subtitlesOn": "Вимкнути",
"app.externalVideo.subtitlesOff": "Вмикнути (якщо доступно)",
"app.actionsBar.actionsDropdown.shareExternalVideo": "Демонстрація зовнішнього відео",
"app.actionsBar.actionsDropdown.stopShareExternalVideo": "Припинити показ зовнішнього відео",
"app.iOSWarning.label": "Будь ласка, оновіть пристрій з iOS до версії 12.2 або новішої версії",
@ -885,9 +961,21 @@
"playback.button.search.aria": "Пошук",
"playback.button.section.aria": "Бічна частина",
"playback.button.swap.aria": "Перемкнути вміст",
"playback.button.theme.aria": "Перемкнути тему",
"playback.error.wrapper.aria": "Область помилок",
"playback.loader.wrapper.aria": "Область завантажувача",
"playback.player.wrapper.aria": "Область програвача",
"playback.player.about.modal.shortcuts.title": "Гарячі клавіші",
"playback.player.about.modal.shortcuts.alt": "Alt",
"playback.player.about.modal.shortcuts.shift": "Shift",
"playback.player.about.modal.shortcuts.fullscreen": "Перемикання на весь екран",
"playback.player.about.modal.shortcuts.play": "Відтворення/Пауза",
"playback.player.about.modal.shortcuts.section": "Перемкнути бокову панель",
"playback.player.about.modal.shortcuts.seek.backward": "Шукати назад",
"playback.player.about.modal.shortcuts.seek.forward": "Шукати далі",
"playback.player.about.modal.shortcuts.skip.next": "Наступний слайд",
"playback.player.about.modal.shortcuts.skip.previous": "Попередній слайд",
"playback.player.about.modal.shortcuts.swap": "Обміняти контент",
"playback.player.chat.message.poll.name": "Результати опитування",
"playback.player.chat.message.poll.question": "Питання",
"playback.player.chat.message.poll.options": "Варіанти",
@ -906,14 +994,30 @@
"playback.player.thumbnails.wrapper.aria": "Область мініатюр",
"playback.player.webcams.wrapper.aria": "Вебкамери",
"app.learningDashboard.dashboardTitle": "Дошка аналітики навчання",
"app.learningDashboard.downloadSessionDataLabel": "Завантажити дані сесії",
"app.learningDashboard.lastUpdatedLabel": "Оновлено ",
"app.learningDashboard.sessionDataDownloadedLabel": "Завантажено!",
"app.learningDashboard.shareButton": "Поширити з іншими",
"app.learningDashboard.shareLinkCopied": "Ліну скопійовано!",
"app.learningDashboard.user": "Користувач",
"app.learningDashboard.indicators.meetingStatusEnded": "Закінчилась",
"app.learningDashboard.indicators.meetingStatusActive": "Активна",
"app.learningDashboard.indicators.usersOnline": "Користувачі онлайн",
"app.learningDashboard.indicators.usersTotal": "Загальна кількість користувачів",
"app.learningDashboard.indicators.polls": "Опитування",
"app.learningDashboard.indicators.timeline": "Таймлайн",
"app.learningDashboard.indicators.activityScore": "Рахунок активності",
"app.learningDashboard.indicators.duration": "Тривалість",
"app.learningDashboard.userDetails.startTime": "Початковий час",
"app.learningDashboard.userDetails.endTime": "Кінцевий час",
"app.learningDashboard.userDetails.joined": "Приєднався",
"app.learningDashboard.userDetails.category": "Категорія",
"app.learningDashboard.userDetails.average": "Усередньому",
"app.learningDashboard.userDetails.activityPoints": "Оцінка активності",
"app.learningDashboard.userDetails.poll": "Опитування",
"app.learningDashboard.userDetails.response": "Відповідь",
"app.learningDashboard.userDetails.mostCommonAnswer": "Найчастіша відповідь",
"app.learningDashboard.userDetails.anonymousAnswer": "Анонімне опитування",
"app.learningDashboard.usersTable.title": "Огляд",
"app.learningDashboard.usersTable.colOnline": "Час онлайн",
"app.learningDashboard.usersTable.colTalk": "Час виступів",
@ -926,10 +1030,32 @@
"app.learningDashboard.usersTable.userStatusOnline": "Онлайн",
"app.learningDashboard.usersTable.userStatusOffline": "Відсутній",
"app.learningDashboard.usersTable.noUsers": "Поки ще нікого немає",
"app.learningDashboard.usersTable.name": "Ім'я",
"app.learningDashboard.usersTable.moderator": "Модератор",
"app.learningDashboard.usersTable.pollVotes": "Результати опитування",
"app.learningDashboard.usersTable.join": "Приєднатися",
"app.learningDashboard.usersTable.left": "Зліва",
"app.learningDashboard.usersTable.notAvailable": "Н/Д",
"app.learningDashboard.pollsTable.title": "Опитування",
"app.learningDashboard.pollsTable.anonymousAnswer": "Анонімні опитування (відповіді в останньому рядку)",
"app.learningDashboard.pollsTable.anonymousRowName": "Анонім",
"app.learningDashboard.pollsTable.noPollsCreatedHeading": "Ще не створено жлдного опитування",
"app.learningDashboard.pollsTable.noPollsCreatedMessage": "Як опитування буде завершено, результати відобразяться у цьому списку",
"app.learningDashboard.statusTimelineTable.title": "Таймлайн",
"app.learningDashboard.statusTimelineTable.thumbnail": "Іконка презентації",
"app.learningDashboard.errors.invalidToken": "Недійсна лексема сеансу",
"app.learningDashboard.errors.dataUnavailable": "Дані більше недоступні"
"app.learningDashboard.errors.dataUnavailable": "Дані більше недоступні",
"mobileApp.portals.list.empty.addFirstPortal.label": "Додайте свій перший портал кнопками вище",
"mobileApp.portals.list.empty.orUseOurDemoServer.label": "чи скористайтеся демонстраційним порталом",
"mobileApp.portals.list.add.button.label": "Додати портал",
"mobileApp.portals.fields.name.label": "Ім'я порталу",
"mobileApp.portals.fields.name.placeholder": "Демонстрація BigBlueButton ",
"mobileApp.portals.fields.url.label": "Адреса сервера",
"mobileApp.portals.addPortalPopup.confirm.button.label": "Зберегти",
"mobileApp.portals.drawerNavigation.button.label": "Портали",
"mobileApp.portals.addPortalPopup.validation.emptyFields": "Необхідні поля",
"mobileApp.portals.addPortalPopup.validation.portalNameAlreadyExists": "Ім'я вже використовується",
"mobileApp.portals.addPortalPopup.validation.urlInvalid": "Помилка завантаження сторінки - перевірте посилання та з'єднання з мережею"
}

View File

@ -4,6 +4,7 @@ DEBNAME_TO_SOURCEDIR[bbb-apps-akka]="akka-bbb-apps bbb-common-message"
DEBNAME_TO_SOURCEDIR[bbb-config]="bigbluebutton-config"
DEBNAME_TO_SOURCEDIR[bbb-demo]="bbb-api-demo"
DEBNAME_TO_SOURCEDIR[bbb-etherpad]="bbb-etherpad"
DEBNAME_TO_SOURCEDIR[bbb-export-annotations]="bbb-export-annotations"
DEBNAME_TO_SOURCEDIR[bbb-freeswitch-core]="freeswitch bbb-voice-conference"
DEBNAME_TO_SOURCEDIR[bbb-freeswitch-sounds]="do_not_copy_anything"
DEBNAME_TO_SOURCEDIR[bbb-fsesl-akka]="akka-bbb-fsesl bbb-common-message bbb-fsesl-client"

View File

@ -0,0 +1,5 @@
[Unit]
PartOf=bigbluebutton.target
[Install]
WantedBy=bigbluebutton.target

View File

@ -0,0 +1,7 @@
[Unit]
Description=BigBlueButton System
Requires=
After=syslog.target network.target
[Install]
WantedBy=multi-user.target

View File

@ -2,6 +2,12 @@
TARGET=`basename $(pwd)`
# inject dependency to bigbluebutton.target
for unit in freeswitch nginx redis-server; do
mkdir -p "staging/lib/systemd/system/${unit}.service.d"
cp bigbluebutton.conf "staging/lib/systemd/system/${unit}.service.d/"
done
PACKAGE=$(echo $TARGET | cut -d'_' -f1)
VERSION=$(echo $TARGET | cut -d'_' -f2)
@ -14,6 +20,7 @@ rm -rf staging
#
# Create build directories for markign by fpm
DIRS="/etc/bigbluebutton \
/lib/systemd/system \
/var/bigbluebutton/blank \
/usr/share/bigbluebutton/blank \
/var/www/bigbluebutton-default"
@ -44,30 +51,7 @@ cp cron.daily/* staging/etc/cron.daily
mkdir -p staging/etc/cron.hourly
cp cron.hourly/bbb-resync-freeswitch staging/etc/cron.hourly
# Overrides
mkdir -p staging/etc/systemd/system/bbb-apps-akka.service.d
cat > staging/etc/systemd/system/bbb-apps-akka.service.d/override.conf <<HERE
[Unit]
Wants=redis-server.service
After=redis-server.service
HERE
mkdir -p staging/etc/systemd/system/bbb-fsesl-akka.service.d
cat > staging/etc/systemd/system/bbb-fsesl-akka.service.d/override.conf <<HERE
[Unit]
Wants=redis-server.service
After=redis-server.service
HERE
mkdir -p staging/etc/systemd/system/bbb-transcode-akka.service.d
cat > staging/etc/systemd/system/bbb-transcode-akka.service.d/override.conf <<HERE
[Unit]
Wants=redis-server.service
After=redis-server.service
HERE
cp bigbluebutton.target staging/lib/systemd/system/
. ./opts-$DISTRO.sh

View File

@ -2,6 +2,7 @@
Description=Etherpad Server
Wants=redis-server.service
After=syslog.target network.target
PartOf=bigbluebutton.target
[Service]
Type=simple
@ -14,5 +15,5 @@ Restart=always
# use mysql plus a complete settings.json to avoid Service hold-off time over, scheduling restart.
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target bigbluebutton.target

View File

@ -0,0 +1,6 @@
after-install.sh
bbb-export-annotations.service
before-remove.sh
build.sh
opts-focal.sh
opts-global.sh

View File

@ -0,0 +1,14 @@
#!/bin/bash -e
case "$1" in
configure|upgrade|1|2)
;;
abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "postinst called with unknown argument \`$1'" >&2
exit 1
;;
esac

View File

@ -0,0 +1,17 @@
[Unit]
Description=BigBlueButton Export Annotations
Wants=redis.service
After=syslog.target network.target
PartOf=bigbluebutton.target
[Service]
WorkingDirectory=/usr/local/bigbluebutton/bbb-export-annotations
ExecStart=/usr/bin/node master.js
Restart=always
SyslogIdentifier=bbb-export-annotations
User=bigbluebutton
Group=bigbluebutton
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target bigbluebutton.target

View File

@ -0,0 +1,3 @@
#!/bin/bash -e
stopService bbb-export-annotations || echo "bbb-export-annotations could not be unregistered or stopped"

View File

@ -0,0 +1,42 @@
#!/bin/bash -ex
TARGET=`basename $(pwd)`
PACKAGE=$(echo $TARGET | cut -d'_' -f1)
VERSION=$(echo $TARGET | cut -d'_' -f2)
DISTRO=$(echo $TARGET | cut -d'_' -f3)
TAG=$(echo $TARGET | cut -d'_' -f4)
#
# Clean up directories
rm -rf staging
#
# package
mkdir -p staging/usr/local/bigbluebutton/bbb-export-annotations
find -maxdepth 1 ! -path . ! -name staging $(printf "! -name %s " $(cat .build-files)) -exec cp -r {} staging/usr/local/bigbluebutton/bbb-export-annotations/ \;
pushd .
cd staging/usr/local/bigbluebutton/bbb-export-annotations/
npm install --production
popd
mkdir -p staging/usr/lib/systemd/system
cp bbb-export-annotations.service staging/usr/lib/systemd/system
##
. ./opts-$DISTRO.sh
#
# Build package
fpm -s dir -C ./staging -n $PACKAGE \
--version $VERSION --epoch $EPOCH \
--after-install after-install.sh \
--before-remove before-remove.sh \
--description "BigBlueButton Export Annotations" \
$DIRECTORIES \
$OPTS

View File

@ -0,0 +1,3 @@
. ./opts-global.sh
OPTS="$OPTS -t deb -d nodejs,npm,bbb-apps-akka,bbb-web"

View File

@ -3,6 +3,7 @@
[Unit]
Description=freeswitch
After=syslog.target network.target local-fs.target
PartOf=bigbluebutton.target
[Service]
; service
@ -54,5 +55,5 @@ CPUSchedulingPriority=89
; execute "systemctl daemon-reload" after editing the unit files.
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target bigbluebutton.target

View File

@ -59,7 +59,7 @@ source /etc/lsb-release
# Set up specific version of node
if [ "$DISTRIB_CODENAME" == "focal" ]; then
node_version="14.19.1"
node_version="14.19.3"
if [[ ! -d /usr/share/node-v${node_version}-linux-x64 ]]; then
cd /usr/share
tar xfz "node-v${node_version}-linux-x64.tar.gz"
@ -76,9 +76,9 @@ if [ -f /opt/freeswitch/etc/freeswitch/sip_profiles/external.xml ]; then
sed -i 's/<!--<param name="enable-3pcc" value="true"\/>-->/<param name="enable-3pcc" value="proxy"\/>/g' /opt/freeswitch/etc/freeswitch/sip_profiles/external.xml
fi
chown root:root /usr/lib/systemd/system
chown root:root /usr/lib/systemd/system/bbb-html5.service
chown root:root /usr/lib/systemd/system/disable-transparent-huge-pages.service
chown root:root /lib/systemd/system
chown root:root /lib/systemd/system/bbb-html5.service
chown root:root /lib/systemd/system/disable-transparent-huge-pages.service
# Ensure settings is readable
chmod go+r /usr/share/meteor/bundle/programs/server/assets/app/config/settings.yml

View File

@ -2,6 +2,7 @@
Description=BigBlueButton HTML5 service
Wants=redis.service mongod.service disable-transparent-huge-pages.service bbb-pads.service
After=redis.service mongod.service disable-transparent-huge-pages.service bbb-pads.service syslog.target network.target
PartOf=bigbluebutton.target
[Service]
Type=oneshot
@ -16,5 +17,5 @@ RemainAfterExit=yes
User=root
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target bigbluebutton.target

View File

@ -92,11 +92,11 @@ cp bbb-html5-frontend@.service staging/usr/lib/systemd/system
mkdir -p staging/usr/share
if [ ! -f node-v14.19.1-linux-x64.tar.gz ]; then
wget https://nodejs.org/dist/v14.19.1/node-v14.19.1-linux-x64.tar.gz
if [ ! -f node-v14.19.3-linux-x64.tar.gz ]; then
wget https://nodejs.org/dist/v14.19.3/node-v14.19.3-linux-x64.tar.gz
fi
cp node-v14.19.1-linux-x64.tar.gz staging/usr/share
cp node-v14.19.3-linux-x64.tar.gz staging/usr/share
if [ -f staging/usr/share/meteor/bundle/programs/web.browser/head.html ]; then
sed -i "s/VERSION/$(($BUILD))/" staging/usr/share/meteor/bundle/programs/web.browser/head.html

View File

@ -1,5 +1,6 @@
[Unit]
Description=Disable Transparent Huge Pages
PartOf=bigbluebutton.target
ConditionPathIsDirectory=/sys/kernel/mm/transparent_hugepage
[Service]
@ -9,4 +10,4 @@ ExecStart=/bin/sh -c "/bin/echo "never" | tee /sys/kernel/mm/transparent_hugepag
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target bigbluebutton.target

View File

@ -5,6 +5,7 @@
Description=High-performance, schema-free document-oriented database
After=network.target
Documentation=https://docs.mongodb.org/manual
PartOf=bigbluebutton.target
[Service]
User=mongodb
@ -34,5 +35,5 @@ TasksAccounting=false
# http://docs.mongodb.org/manual/reference/ulimit/#recommended-settings
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target bigbluebutton.target

View File

@ -1,3 +1,3 @@
. ./opts-global.sh
OPTS="$OPTS -d bc,bbb-pads,bbb-webrtc-sfu,bbb-web,mongodb-org -t deb"
OPTS="$OPTS -d bc,bbb-pads,bbb-webrtc-sfu,bbb-export-annotations,bbb-web,mongodb-org -t deb"

View File

@ -49,7 +49,7 @@ fi
export MONGO_OPLOG_URL=mongodb://127.0.1.1/local
export MONGO_URL=mongodb://127.0.1.1/meteor
export NODE_ENV=production
export NODE_VERSION=node-v14.19.1-linux-x64
export NODE_VERSION=node-v14.19.3-linux-x64
export SERVER_WEBSOCKET_COMPRESSION=0
export BIND_IP=127.0.0.1
PORT=$PORT /usr/share/$NODE_VERSION/bin/node --max-old-space-size=2048 --max_semi_space_size=128 main.js NODEJS_BACKEND_INSTANCE_ID=$INSTANCE_ID

View File

@ -49,7 +49,7 @@ fi
export MONGO_OPLOG_URL=mongodb://127.0.1.1/local
export MONGO_URL=mongodb://127.0.1.1/meteor
export NODE_ENV=production
export NODE_VERSION=node-v14.19.1-linux-x64
export NODE_VERSION=node-v14.19.3-linux-x64
export SERVER_WEBSOCKET_COMPRESSION=0
export BIND_IP=127.0.0.1
PORT=$PORT /usr/share/$NODE_VERSION/bin/node --max-old-space-size=2048 --max_semi_space_size=128 main.js

View File

@ -1,6 +1,7 @@
[Unit]
Description=LibreOffice
After=network.target
PartOf=bigbluebutton.target
[Service]
Type=simple
@ -12,5 +13,4 @@ MemoryLimit=1G
CPUQuota=20%
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target bigbluebutton.target

View File

@ -1,6 +1,7 @@
[Unit]
Description=BigBlueButton LTI Application
Requires=network.target
PartOf=bigbluebutton.target
[Service]
Type=simple
@ -19,5 +20,5 @@ PermissionsStartOnly=true
LimitNOFILE=1024
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target bigbluebutton.target

View File

@ -2,6 +2,7 @@
Description=BigBlueButton Pads
Wants=redis.service etherpad.service
After=syslog.target network.target
PartOf=bigbluebutton.target
[Service]
WorkingDirectory=/usr/local/bigbluebutton/bbb-pads
@ -13,4 +14,4 @@ Group=bigbluebutton
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target bigbluebutton.target

View File

@ -2,6 +2,7 @@
Description=BigBlueButton Web Application
Requires=network.target
After=redis.service
PartOf=bigbluebutton.target
[Service]
Type=simple
@ -20,5 +21,5 @@ PermissionsStartOnly=true
LimitNOFILE=1024
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target bigbluebutton.target

View File

@ -2,6 +2,7 @@
Description=BigBlueButton Webhooks
Wants=redis-server.service
After=syslog.target network.target
PartOf=bigbluebutton.target
[Service]
WorkingDirectory=/usr/local/bigbluebutton/bbb-webhooks
@ -13,4 +14,4 @@ Group=bigbluebutton
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target bigbluebutton.target

View File

@ -2,6 +2,7 @@
Description=BigBlueButton WebRTC SFU
Wants=redis-server.service
After=syslog.target network.target freeswitch.service kurento-media-server.service
PartOf=bigbluebutton.target
[Service]
WorkingDirectory=/usr/local/bigbluebutton/bbb-webrtc-sfu
@ -14,4 +15,4 @@ Environment=NODE_ENV=production
Environment=NODE_CONFIG_DIR=/etc/bigbluebutton/bbb-webrtc-sfu/:/usr/local/bigbluebutton/bbb-webrtc-sfu/config/
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target bigbluebutton.target

View File

@ -1,6 +1,7 @@
[Unit]
Description=Kurento Media Server daemon
After=network.target
PartOf=bigbluebutton.target
[Service]
UMask=0002
@ -15,5 +16,5 @@ PIDFile=/var/run/kurento-media-server.pid
Restart=always
[Install]
WantedBy=default.target
WantedBy=multi-user.target bigbluebutton.target

View File

@ -16,6 +16,7 @@ fi
PKGS="bbb-apps-akka
bbb-config
bbb-etherpad
bbb-export-annotations
bbb-freeswitch-core
bbb-freeswitch-sounds
bbb-fsesl-akka

View File

@ -26,16 +26,9 @@ for dir in .gradle .grails .ivy2 .m2; do
ln -s "${SOURCE}/cache/${dir}" "/root/${dir}"
done
if [ "$LOCAL_BUILD" != 1 ] ; then
GIT_REV="${CI_COMMIT_SHA:0:10}"
else
GIT_REV=$(git rev-parse HEAD)
GIT_REV="local-build-${GIT_REV:0:10}"
fi
VERSION_NUMBER="$(cat "$SOURCE/bigbluebutton-config/bigbluebutton-release" | cut -d '=' -f2 | cut -d "-" -f1)"
# this contains stuff like alpha4 etc
VERSION_ADDON="$(cat "$SOURCE/bigbluebutton-config/bigbluebutton-release" | cut -d '=' -f2 | cut -d "-" -f2)"
COMMIT_DATE="$(git log -n1 --pretty='format:%cd' --date=format:'%Y%m%dT%H%M%S')"
BUILD_NUMBER=${BUILD_NUMBER:=1}
EPOCH=${EPOCH:=2}

View File

@ -15,9 +15,17 @@ mkdir -p artifacts
DOCKER_IMAGE=$(python3 -c 'import yaml; print(yaml.load(open("./.gitlab-ci.yml"), Loader=yaml.SafeLoader)["default"]["image"])')
if [ "$LOCAL_BUILD" != 1 ] ; then
GIT_REV="${CI_COMMIT_SHA:0:10}"
else
GIT_REV=$(git rev-parse HEAD)
GIT_REV="local-build-${GIT_REV:0:10}"
fi
COMMIT_DATE="$(git log -n1 --pretty='format:%cd' --date=format:'%Y%m%dT%H%M%S')"
# -v "$CACHE_DIR/dev":/root/dev
sudo docker run --rm \
-e "LOCAL_BUILD=1" \
--env GIT_REV=$GIT_REV --env COMMIT_DATE=$COMMIT_DATE --env "LOCAL_BUILD=1" \
--mount type=bind,src="$PWD",dst=/mnt \
--mount type=bind,src="${PWD}/artifacts,dst=/artifacts" \
-t "$DOCKER_IMAGE" /mnt/build/setup-inside-docker.sh "$PACKAGE_TO_BUILD"
@ -28,4 +36,4 @@ sudo docker run --rm \
# -v "$CACHE_DIR/$DISTRO/.m2:/root/.m2" \
# -v "$TMP/$TARGET:$TMP/$TARGET" \
find artifacts
find artifacts

View File

@ -960,7 +960,7 @@ module BigBlueButton
# The following events are considered to indicate that the presentation
# area was actively used during the session.
when 'AddShapeEvent', 'ModifyTextEvent', 'UndoShapeEvent',
'ClearPageEvent'
'ClearPageEvent', 'AddTldrawShapeEvent', 'DeleteTldrawShapeEvent'
BigBlueButton.logger.debug("Seen a #{event['eventname']} event, presentation area used.")
return true
# We ignore the first SharePresentationEvent, since it's the default
@ -1093,5 +1093,11 @@ module BigBlueButton
return false
end
# Check if doc has tldraw events
def self.check_for_tldraw_events(events)
return !(events.xpath("recording/event[@eventname='TldrawCameraChangedEvent']").empty? &&
events.xpath("recording/event[@eventname='AddTldrawShapeEvent']").empty?)
end
end
end
end

View File

@ -1,5 +1,6 @@
[Unit]
Description=BigBlueButton recording caption upload handler
PartOf=bigbluebutton.target
[Service]
Type=simple
@ -10,4 +11,4 @@ Slice=bbb_record_core.slice
Restart=on-failure
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target bigbluebutton.target

View File

@ -2,6 +2,7 @@
Description=BigBlueButton resque worker for recordings
Wants=redis.service
After=redis.service
PartOf=bigbluebutton.target
[Service]
Type=simple
@ -15,4 +16,4 @@ Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target bigbluebutton.target

View File

@ -1,5 +1,6 @@
[Unit]
Description=BigBlueButton recording processing starter
PartOf=bigbluebutton.target
[Service]
Type=simple
@ -10,4 +11,4 @@ Slice=bbb_record_core.slice
Restart=on-failure
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target bigbluebutton.target

View File

@ -192,6 +192,10 @@ unless FileTest.directory?(target_dir)
# Copy thumbnails from raw files
FileUtils.cp_r("#{pres_dir}/thumbnails", "#{target_pres_dir}/thumbnails") if File.exist?("#{pres_dir}/thumbnails")
tldraw = BigBlueButton::Events.check_for_tldraw_events(@doc);
if (tldraw)
FileUtils.cp_r("#{pres_dir}/svgs", "#{target_pres_dir}/svgs") if File.exist?("#{pres_dir}/svgs")
end
end
BigBlueButton.logger.info('Generating closed captions')

View File

@ -382,6 +382,30 @@ def svg_render_shape_poll(g, slide, shape)
width: width, height: height, x: x, y: y)
end
def build_tldraw_shape(image_shapes, slide, shape)
shape_in = shape[:in]
shape_out = shape[:out]
if shape_in == shape_out
BigBlueButton.logger.info("Draw #{shape[:shape_id]} Shape #{shape[:shape_unique_id]} is never shown (duration rounds to 0)")
return
end
if (shape_in >= slide[:out]) || (!shape[:out].nil? && shape[:out] <= slide[:in])
BigBlueButton.logger.info("Draw #{shape[:shape_id]} Shape #{shape[:shape_unique_id]} is not visible during image time span")
return
end
tldraw_shape = {
id: shape[:shape_id],
timestamp: shape_in,
undo: (shape[:undo].nil? ? -1 : shape[:undo]),
shape_data: shape[:shape_data]
}
image_shapes.push(tldraw_shape)
end
def svg_render_shape(canvas, slide, shape, image_id)
shape_in = shape[:in]
shape_out = shape[:out]
@ -426,7 +450,7 @@ def svg_render_shape(canvas, slide, shape, image_id)
end
@svg_image_id = 1
def svg_render_image(svg, slide, shapes)
def svg_render_image(svg, slide, shapes, tldraw, tldraw_shapes)
slide_number = slide[:slide]
presentation = slide[:presentation]
slide_in = slide[:in]
@ -458,15 +482,25 @@ def svg_render_image(svg, slide, shapes)
shapes = shapes[presentation][slide_number]
canvas = doc.create_element('g',
class: 'canvas', id: "canvas#{image_id}",
image: "image#{image_id}", display: 'none')
if !tldraw
canvas = doc.create_element('g',
class: 'canvas', id: "canvas#{image_id}",
image: "image#{image_id}", display: 'none')
shapes.each do |shape|
svg_render_shape(canvas, slide, shape, image_id)
shapes.each do |shape|
svg_render_shape(canvas, slide, shape, image_id)
end
svg << canvas unless canvas.element_children.empty?
else
image_shapes = []
shapes.each do |shape|
build_tldraw_shape(image_shapes, slide, shape)
end
tldraw_shapes[image_id] = { :shapes=>image_shapes, :timestamp=> slide_in}
end
svg << canvas unless canvas.element_children.empty?
end
def panzoom_viewbox(panzoom)
@ -483,13 +517,19 @@ def panzoom_viewbox(panzoom)
[x, y, w, h]
end
def panzooms_emit_event(rec, panzoom)
def panzooms_emit_event(rec, panzoom, tldraw)
panzoom_in = panzoom[:in]
return if panzoom_in == panzoom[:out]
rec.event(timestamp: panzoom_in) do
x, y, w, h = panzoom_viewbox(panzoom)
rec.viewBox("#{x} #{y} #{w} #{h}")
if !tldraw
rec.event(timestamp: panzoom_in) do
x, y, w, h = panzoom_viewbox(panzoom)
rec.viewBox("#{x} #{y} #{w} #{h}")
end
else
rec.event(timestamp: panzoom_in) do
rec.cameraAndZoom("#{panzoom[:x_camera]} #{panzoom[:y_camera]} #{panzoom[:zoom]}")
end
end
end
@ -497,14 +537,14 @@ def convert_cursor_coordinate(cursor_coord, panzoom_offset, panzoom_ratio)
(((cursor_coord / 100.0) + (panzoom_offset * MAGIC_MYSTERY_NUMBER / 100.0)) / (panzoom_ratio / 100.0)).round(5)
end
def cursors_emit_event(rec, cursor)
def cursors_emit_event(rec, cursor, tldraw)
cursor_in = cursor[:in]
return if cursor_in == cursor[:out]
rec.event(timestamp: cursor_in) do
panzoom = cursor[:panzoom]
if cursor[:visible]
if @version_atleast_2_0_0
if @version_atleast_2_0_0 && !tldraw
# In BBB 2.0, the cursor now uses the same coordinate system as annotations
# Use the panzoom information to convert it to be relative to viewbox
x = convert_cursor_coordinate(cursor[:x], panzoom[:x_offset], panzoom[:width_ratio])
@ -535,6 +575,53 @@ def determine_slide_number(slide, current_slide)
slide
end
def events_parse_tldraw_shape(shapes, event, current_presentation, current_slide, timestamp)
presentation = event.at_xpath('presentation')
slide = event.at_xpath('pageNumber')
presentation = determine_presentation(presentation, current_presentation)
slide = determine_slide_number(slide, current_slide)
# Set up the shapes data structures if needed
shapes[presentation] ||= {}
shapes[presentation][slide] ||= []
# We only need to deal with shapes for this slide
shapes = shapes[presentation][slide]
# Set up the structure for this shape
shape = {}
# Common properties
shape[:in] = timestamp
shape_data = shape[:shape_data] = JSON.parse(event.at_xpath('shapeData'))
user_id = event.at_xpath('userId')&.text
shape[:user_id] = user_id if user_id
shape_id = event.at_xpath('shapeId')&.text
shape[:id] = shape_id if shape_id
draw_id = shape[:shape_id] = @svg_shape_id
@svg_shape_id += 1
# Find the previous shape, for updates
prev_shape = nil
if shape_id
# If we have a shape ID, look up the previous shape by ID
prev_shape_pos = shapes.rindex { |s| s[:shade_id] == shape_id }
prev_shape = prev_shape_pos ? shapes[prev_shape_pos] : nil
end
if prev_shape
prev_shape[:out] = timestamp
shape[:shape_unique_id] = prev_shape[:shape_unique_id]
else
shape[:shape_unique_id] = @svg_shape_unique_id
@svg_shape_unique_id += 1
end
shapes << shape
end
def events_parse_shape(shapes, event, current_presentation, current_slide, timestamp)
# Figure out what presentation+slide this shape is for, with fallbacks
# for old BBB where this info isn't in the shape messages
@ -734,7 +821,7 @@ def events_parse_clear(shapes, event, current_presentation, current_slide, times
end
end
def events_get_image_info(slide)
def events_get_image_info(slide, tldraw)
slide_deskshare = slide[:deskshare]
slide_presentation = slide[:presentation]
@ -744,7 +831,8 @@ def events_get_image_info(slide)
slide[:src] = 'presentation/logo.png'
else
slide_nr = slide[:slide] + 1
slide[:src] = "presentation/#{slide_presentation}/slide-#{slide_nr}.png"
tldraw ? slide[:src] = "presentation/#{slide_presentation}/svgs/slide#{slide_nr}.svg"
: slide[:src] = "presentation/#{slide_presentation}/slide-#{slide_nr}.png"
slide[:text] = "presentation/#{slide_presentation}/textfiles/slide-#{slide_nr}.txt"
end
image_path = "#{@process_dir}/#{slide[:src]}"
@ -792,6 +880,7 @@ def process_presentation(package_dir)
# Current pan/zoom state
current_x_offset = current_y_offset = 0.0
current_width_ratio = current_height_ratio = 100.0
current_x_camera = current_y_camera = current_zoom = 0.0
# Current cursor status
cursor_x = cursor_y = -1.0
cursor_visible = false
@ -802,6 +891,8 @@ def process_presentation(package_dir)
panzooms = []
cursors = []
shapes = {}
tldraw = BigBlueButton::Events.check_for_tldraw_events(@doc)
tldraw_shapes = {}
# Iterate through the events.xml and store the events, building the
# xml files as we go
@ -836,6 +927,12 @@ def process_presentation(package_dir)
current_height_ratio = event.at_xpath('heightRatio').text.to_f
panzoom_changed = true
when 'TldrawCameraChangedEvent'
current_x_camera = event.at_xpath('xCamera').text.to_f
current_y_camera = event.at_xpath('yCamera').text.to_f
current_zoom = event.at_xpath('zoom').text.to_f
panzoom_changed = true
when 'DeskshareStartedEvent', 'StartWebRTCDesktopShareEvent'
deskshare = slide_changed = true if @presentation_props['include_deskshare']
@ -848,7 +945,10 @@ def process_presentation(package_dir)
when 'AddShapeEvent', 'ModifyTextEvent'
events_parse_shape(shapes, event, current_presentation, current_slide, timestamp)
when 'UndoShapeEvent', 'UndoAnnotationEvent'
when 'AddTldrawShapeEvent'
events_parse_tldraw_shape(shapes, event, current_presentation, current_slide, timestamp)
when 'UndoShapeEvent', 'UndoAnnotationEvent', 'DeleteTldrawShapeEvent'
events_parse_undo(shapes, event, current_presentation, current_slide, timestamp)
when 'ClearPageEvent', 'ClearWhiteboardEvent'
@ -887,7 +987,7 @@ def process_presentation(package_dir)
else
if slide
slide[:out] = timestamp
svg_render_image(svg, slide, shapes)
svg_render_image(svg, slide, shapes, tldraw, tldraw_shapes)
end
BigBlueButton.logger.info("Presentation #{current_presentation} Slide #{current_slide} Deskshare #{deskshare}")
@ -897,7 +997,7 @@ def process_presentation(package_dir)
in: timestamp,
deskshare: deskshare,
}
events_get_image_info(slide)
events_get_image_info(slide, tldraw)
slides << slide
end
end
@ -909,40 +1009,59 @@ def process_presentation(package_dir)
slide_width = slide[:width]
slide_height = slide[:height]
if panzoom &&
(panzoom[:deskshare] == deskshare) &&
((!tldraw &&
(panzoom[:x_offset] == current_x_offset) &&
(panzoom[:y_offset] == current_y_offset) &&
(panzoom[:width_ratio] == current_width_ratio) &&
(panzoom[:height_ratio] == current_height_ratio) &&
(panzoom[:width] == slide_width) &&
(panzoom[:height] == slide_height) &&
(panzoom[:deskshare] == deskshare)
(panzoom[:height] == slide_height)) ||
(tldraw &&
(panzoom[:x_camera] == current_x_camera) &&
(panzoom[:y_camera] == current_y_camera) &&
(panzoom[:zoom] == current_zoom))
)
BigBlueButton.logger.info('Panzoom: skipping, no changes')
panzoom_changed = false
else
if panzoom
panzoom[:out] = timestamp
panzooms_emit_event(panzooms_rec, panzoom)
panzooms_emit_event(panzooms_rec, panzoom, tldraw)
end
if !tldraw
BigBlueButton.logger.info("Panzoom: #{current_x_offset} #{current_y_offset} #{current_width_ratio} #{current_height_ratio} (#{slide_width}x#{slide_height})")
panzoom = {
x_offset: current_x_offset,
y_offset: current_y_offset,
width_ratio: current_width_ratio,
height_ratio: current_height_ratio,
width: slide[:width],
height: slide[:height],
in: timestamp,
deskshare: deskshare,
}
else
BigBlueButton.logger.info("Panzoom: #{current_x_camera} #{current_y_camera} #{current_zoom} (#{slide_width}x#{slide_height})")
panzoom = {
x_camera: current_x_camera,
y_camera: current_y_camera,
zoom: current_zoom,
in: timestamp,
deskshare: deskshare,
}
end
BigBlueButton.logger.info("Panzoom: #{current_x_offset} #{current_y_offset} #{current_width_ratio} #{current_height_ratio} (#{slide_width}x#{slide_height})")
panzoom = {
x_offset: current_x_offset,
y_offset: current_y_offset,
width_ratio: current_width_ratio,
height_ratio: current_height_ratio,
width: slide[:width],
height: slide[:height],
in: timestamp,
deskshare: deskshare,
}
panzooms << panzoom
end
end
# Perform cursor finalization
if cursor_changed || panzoom_changed
unless cursor_x >= 0 && cursor_x <= 100 &&
cursor_y >= 0 && cursor_y <= 100
cursor_visible = false
if !tldraw
unless cursor_x >= 0 && cursor_x <= 100 &&
cursor_y >= 0 && cursor_y <= 100
cursor_visible = false
end
end
panzoom = panzooms.last
@ -955,7 +1074,7 @@ def process_presentation(package_dir)
else
if cursor
cursor[:out] = timestamp
cursors_emit_event(cursors_rec, cursor)
cursors_emit_event(cursors_rec, cursor, tldraw)
end
cursor = {
visible: cursor_visible,
@ -972,26 +1091,27 @@ def process_presentation(package_dir)
# Add the last slide, panzoom, and cursor
slide = slides.last
slide[:out] = last_timestamp
svg_render_image(svg, slide, shapes)
svg_render_image(svg, slide, shapes, tldraw, tldraw_shapes)
panzoom = panzooms.last
panzoom[:out] = last_timestamp
panzooms_emit_event(panzooms_rec, panzoom)
panzooms_emit_event(panzooms_rec, panzoom, tldraw)
cursor = cursors.last
cursor[:out] = last_timestamp
cursors_emit_event(cursors_rec, cursor)
cursors_emit_event(cursors_rec, cursor, tldraw)
cursors_doc = Builder::XmlMarkup.new(indent: 2)
cursors_doc.instruct!
cursors_doc.recording(id: 'cursor_events') { |xml| xml << cursors_rec.target! }
cursors_doc.recording(id: 'cursor_events', tldraw: tldraw) { |xml| xml << cursors_rec.target! }
panzooms_doc = Builder::XmlMarkup.new(indent: 2)
panzooms_doc.instruct!
panzooms_doc.recording(id: 'panzoom_events') { |xml| xml << panzooms_rec.target! }
panzooms_doc.recording(id: 'panzoom_events', tldraw: tldraw) { |xml| xml << panzooms_rec.target! }
# And save the result
File.write("#{package_dir}/#{@shapes_svg_filename}", shapes_doc.to_xml)
File.write("#{package_dir}/#{@panzooms_xml_filename}", panzooms_doc.target!)
File.write("#{package_dir}/#{@cursor_xml_filename}", cursors_doc.target!)
generate_json_file(package_dir, @tldraw_shapes_filename, tldraw_shapes) if tldraw
end
def process_chat_messages(events, bbb_props)
@ -1170,6 +1290,7 @@ end
@panzooms_xml_filename = 'panzooms.xml'
@cursor_xml_filename = 'cursor.xml'
@deskshare_xml_filename = 'deskshare.xml'
@tldraw_shapes_filename = 'tldraw.json'
@svg_shape_id = 1
@svg_shape_unique_id = 1