Compare commits

...

15 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
9bbf525060 fix(local-recordings) back to WebM format, fix duration
After a lot of back and forth, WebM seems to be the only option we
really have. In terms of containers and codecs, here is the rundown:

- WebM, any codec: the resulting file is not seekable
- MKV, any codec: the resulting file is not seekable
- MP4, vp9 + opus: video artifacts and audio clipping, file is seekable
- MP4, av1 + AAC: all good, but not supported on Linux :-/

MP4 looked very promising but there is no combination that leads to
something that works reliably everywhere, oh well. In addition, MP4
files can be opened with QuickTime on macOS, but not with the codec
combination we'd use, so that is somewhat a disadvantage.

So, we are back to where we started: WebM with VP8 and opus. But we need
to fix the duration in a potentially long file... the trick is to _only_
fix the duration. We can do that by inserting the right segment in the
metadata section. Something we cannot do without reading the whole file
is create cue points, but players like VLC seem to work well without
them.
2025-05-19 11:37:03 -05:00
Saúl Ibarra Corretgé
9dcad3e2fb fix(local-recordings) use the Matroska container with VP8 as a codec
In the 1st incarnation of local recordings we used to use VP8 as the
video encoder. Upon switching to MP4 that combiantion is not supported
for some reason, so I used VP9 instead.

Some anecdotal evidence suggests VP9 is behqaving more erratically, with
rendering errors and fixes.

Turns out Chrome also supports the Matroska container! And VP8 inside it
at that! The bonus we get from using it is that QuickTime on macOS won't
try to open it, thus avoiding some confusion with MP4 files, which it
recognizes, but cannot open due to the video codec.
2025-05-08 09:59:29 -05:00
Saúl Ibarra Corretgé
cf01abf72f fix(local-recordings) use constant bitrate for audio 2025-05-08 09:59:23 -05:00
Andrei Gavrilescu
6c6442f97d fix(popover): touch interaction closes overflow drawer without triggering action
* automatic drawer toolbox on mobile browser

* fix touch interaction on Popover
2025-05-06 16:20:58 +03:00
Saúl Ibarra Corretgé
2ea866d04e fix(local-recordings) make sure we have a gDM audio stream 2025-05-06 07:46:40 -05:00
Saúl Ibarra Corretgé
4da3648d6e fix(local-recordings) tweak audio constraints for local recordings 2025-05-06 07:46:27 -05:00
Saúl Ibarra Corretgé
33f61e5c23 feat(recording) add ability to skip consent in-meeting
When turned on, the consent dialog won't be displayed for the users who
are already in the meeting, it will only be displayed to those who join
after the recording was started.
2025-04-30 11:34:59 -04:00
Saúl Ibarra Corretgé
d8e91ad63c fix(local-recordings) fix data loss when MediaRecorder is stopped
Flush the file after the 'stop' event is emitted, which happens _after_
the last 'dataavailable' has been emitted, and thus when the
MediaRecorder is really done.

In addition, lower the time slice as added precaution against crashes.
2025-04-30 11:34:51 -04:00
Saúl Ibarra Corretgé
580a769ee3 fix(local-recordings) more resilient way to get local audio
It's OK if we don't have any local audio track, we'll add it to the
mixer later.

The original bug / limitation that prompted the previous code no longer
applies since we always have a MediaStream (with audio tracks) which
we are recording.
2025-04-30 11:34:41 -04:00
Saúl Ibarra Corretgé
e200ab64b3 fix(local-recordings) remove text mentioning time limit 2025-04-30 11:34:30 -04:00
Saúl Ibarra Corretgé
434f66030a feat(local-recordings) refactor how audio is captured
Capture the tab audio, which will include all participants and sound
effects, YouTube videos, anything playing in the tab.

This requires the `suppressLocalAudioPlayback` constraint since
otherwise the shared tab won't keep playing audio.

Local audio still needs to be injected seprarately, since it's not
played back to the local user.
2025-04-30 11:34:20 -04:00
Saúl Ibarra Corretgé
0908a51650 fix(local-recordings) style, for readability 2025-04-30 11:34:10 -04:00
Saúl Ibarra Corretgé
c6a49c8697 fix(local-recording) require setCaptureHandleConfig 2025-04-30 11:33:59 -04:00
Saúl Ibarra Corretgé
befef4af54 fix(recording) prevent multiple consent requests
A given recording should only trigger a single consent request.

The mechanism to notify about recording status updates may fire multiple
times since it's tied to XMPP presence and may send updates such as when
the live stream view URL is set.

Rather than trying to handle all possible corner cases to make sure we
only show the consent dialog once, keep track of the recording session
IDs for which we _have_ asked for consent and skip the dialog in case we
have done it already.
2025-04-30 11:33:43 -04:00
Jaya Allamsetty
7fb29693a2 fix(test): Fix codec selection test 2025-04-30 11:33:20 -04:00
48 changed files with 628 additions and 125 deletions

View File

@@ -401,6 +401,8 @@ var config = {
// // If true, mutes audio and video when a recording begins and displays a dialog
// // explaining the effect of unmuting.
// // requireConsent: true,
// // If true consent will be skipped for users who are already in the meeting.
// // skipConsentInMeeting: true,
// },
// recordingService: {

View File

@@ -916,7 +916,7 @@
"localRecordingStartWarningTitle": "Atura l'enregistrament per a desar-lo",
"localRecordingVideoStop": "Aturar el vídeo també aturarà l'enregistrament local. Segur que voleu continuar?",
"localRecordingVideoWarning": "Per a enregistrar el vostre vídeo, cal que ho feu en començar l'enregistrament",
"localRecordingWarning": "Assegureu-vos de triar la pestanya actual per a poder usar el vídeo i àudios correctes. L'enregistrament actualment està limitat a 1 GB. Això són, aproximadament, 100 minuts.",
"localRecordingWarning": "Assegureu-vos de triar la pestanya actual per a poder usar el vídeo i àudios correctes.",
"loggedIn": "Sessió iniciada com a {{userName}}",
"noStreams": "No s'ha detectat flux d'àudio ni vídeo.",
"off": "S'ha aturat l'enregistrament",

View File

@@ -1047,7 +1047,7 @@
"localRecordingStartWarningTitle": "Zastavte záznam, abyste jej uložili",
"localRecordingVideoStop": "Zastavením videa se zastaví také místní nahrávání. Opravdu chcete pokračovat?",
"localRecordingVideoWarning": "Chcete-li nahrát video, musíte jej mít zapnutý při zahájení nahrávání",
"localRecordingWarning": "Ujistěte se, že jste vybrali aktuální kartu, abyste mohli použít správné video a zvuk. Záznam je aktuálně omezen na 1GB, což je kolem 100 minut.",
"localRecordingWarning": "Ujistěte se, že jste vybrali aktuální kartu, abyste mohli použít správné video a zvuk.",
"loggedIn": "Přihlášen/a jako {{userName}}",
"noMicPermission": "Kanál mikrofonu nelze vytvořit. Udělte prosím povolení k použití mikrofonu.",
"noStreams": "Nebyl zjištěn žádný audio nebo video stream.",

View File

@@ -1055,7 +1055,7 @@
"localRecordingStartWarningTitle": "Aufzeichnung zum Speichern beenden",
"localRecordingVideoStop": "Wenn Sie ihre Kamera abschalten wird auch die Aufnahme beendet. Sind Sie sicher, dass Sie fortfahren möchten?",
"localRecordingVideoWarning": "Um Ihr eigenes Kamerabild aufzuzeichnen, müssen Sie Ihre Kamera beim Start der Aufnahme einschalten",
"localRecordingWarning": "Bitte prüfen Sie, dass das aktuelle Tab auswählen, um Bild und Ton aufzuzeichnen. Die Länge der Aufzeichnung ist aktuell auf 1GB beschränkt, was ungefähr 100 Minuten entspricht.",
"localRecordingWarning": "Bitte prüfen Sie, dass das aktuelle Tab auswählen, um Bild und Ton aufzuzeichnen.",
"loggedIn": "Als {{userName}} angemeldet",
"noMicPermission": "Zugriff auf Mikrofon fehlgeschlagen. Bitte erlauben Sie den Zugriff auf das Mikrofon.",
"noStreams": "Kein Ton oder Video erkannt.",

View File

@@ -916,7 +916,7 @@
"localRecordingStartWarningTitle": "nagrawanje dokóńcowaś a zachowaś",
"localRecordingVideoStop": "Gaž kameru wušaltujośo, ga teke lokalne nagrawanje se dokóńcujo. Sćo-li napšawdu wěste, až to cośo?",
"localRecordingVideoWarning": "Aby bildu ze swójeje samskeje kamery nagrawali, musyśo swóju kameru na zachopjeńku nagrawanja zašaltowaś.",
"localRecordingWarning": "Pśespytujśo, lěc sćo aktuelnu kórtu wuzwólili, aby wideo a zuk nagrawali. Dłujkosć nagrawanja jo tuchylu na 1 GB wobgranicowana, což dosega za jadnab 100 minutow.",
"localRecordingWarning": "Pśespytujśo, lěc sćo aktuelnu kórtu wuzwólili, aby wideo a zuk nagrawali.",
"loggedIn": "ako {{userName}} zalogowany/-a",
"noMicPermission": "",
"noStreams": "Žeden zuk a žeden wideo njejo namakany.",

View File

@@ -930,7 +930,7 @@
"localRecordingStartWarningTitle": "Σταματήστε τη καταγραφή για να την αποθηκεύσετε",
"localRecordingVideoStop": "Σταματώντας το βίντεο σας θα σταματήσει και η τοπική καταγραφή. Θέλετε να συνεχίσετε;",
"localRecordingVideoWarning": "Για να καταγράψετε το βίντεο σας πρέπει να είναι ήδη ενεργό όταν ξεκινήσει η καταγραφή",
"localRecordingWarning": "Βεβαιωθείτε ότι επιλέξατε τη τρέχουσα σελίδα ώστε να χρησιμοποιηθούν το σωστό βίντεο και ήχος. Αυτή τη στιγμή η καταγραφή είναι μέχρι 1GB, περίπου 100 λεπτά.",
"localRecordingWarning": "Βεβαιωθείτε ότι επιλέξατε τη τρέχουσα σελίδα ώστε να χρησιμοποιηθούν το σωστό βίντεο και ήχος.",
"loggedIn": "Συνδέθηκε ως {{userName}}",
"noMicPermission": "Δεν ήταν δυνατή η δημιουργία του κομματιού του μικροφώνου. Παρακαλώ δώστε την άδεια για χρήση του.",
"noStreams": "Δεν εντοπίστηκε ροή ήχου ή βίντεο.",

View File

@@ -1007,7 +1007,7 @@
"localRecordingStartWarningTitle": "Ĉesigu la registradon por konservi ĝin.",
"localRecordingVideoStop": "Ĉesigi vian videaĵon ankaŭ ĉesos la lokan registradon. Ĉu vi certas, ke vi volas daŭrigi?",
"localRecordingVideoWarning": "Por registri vian videon, ŝaltu la kameraon antaŭ vi komencas la registradon.",
"localRecordingWarning": "Certigu, ke vi elektas la nunan langeton por uzi la ĝustajn filmetojn kaj sonojn. La registrado estas nuntempe limigita al 1GB, kio estas proksimume 100 minutoj.",
"localRecordingWarning": "Certigu, ke vi elektas la nunan langeton por uzi la ĝustajn filmetojn kaj sonojn.",
"loggedIn": "Ensalutinta kiel {{userName}}",
"noMicPermission": "Mikrofono ne povis esti uzata. Bonvole donu permeson uzi la mikrofonon.",
"noStreams": "Neniu aŭdio aŭ videofluo detektita.",

View File

@@ -958,7 +958,7 @@
"localRecordingStartWarningTitle": "Detenga la grabación para guardarla",
"localRecordingVideoStop": "Detener su video también detendrá la grabación local. ¿Está seguro de querer continuar?",
"localRecordingVideoWarning": "Para grabar su video debe tenerlo encendido al iniciar la grabación",
"localRecordingWarning": "Asegúrese de seleccionar la pestaña actual para usar el video y audio correctos. La grabación está actualmente limitada a 1GB, que son aproximadamente 100 minutos.",
"localRecordingWarning": "Asegúrese de seleccionar la pestaña actual para usar el video y audio correctos.",
"loggedIn": "Sesión iniciada como {{userName}}",
"noMicPermission": "No se pudo crear la pista de micrófono. Por favor otorgue permiso para usar el micrófono.",
"noStreams": "",

View File

@@ -1047,7 +1047,7 @@
"localRecordingStartWarningTitle": "Arrêter lenregistrement pour le sauvegarder",
"localRecordingVideoStop": "Arrêter votre vidéo va aussi arrêter votre enregistrement local. Êtes-vous sûrs de vouloir continuer ?",
"localRecordingVideoWarning": "Pour enregistrer votre vidéo, vous devez avoir celle-ci active au moment de commencer lenregistrement.",
"localRecordingWarning": "Assurez-vous de sélectionner longlet courant pour utiliser le bon son et la bonne vidéo. Lenregistrement est pour le moment limité à 1Go, soit approximativement 100 minutes.",
"localRecordingWarning": "Assurez-vous de sélectionner longlet courant pour utiliser le bon son et la bonne vidéo.",
"loggedIn": "Connecté en tant que {{userName}}",
"noMicPermission": "La piste microphone ne peut pas être créée. Veuillez autoriser lutilisation du microphone.",
"noStreams": "Aucun flux audio ou vidéo détectés.",

View File

@@ -1026,7 +1026,7 @@
"localRecordingStartWarningTitle": "Arrêter lenregistrement pour le sauvegarder",
"localRecordingVideoStop": "Arrêter votre vidéo va aussi arrêter votre enregistrement local. Êtes-vous sûrs de vouloir continuer ?",
"localRecordingVideoWarning": "Pour enregistrer votre vidéo, vous devez avoir celle-ci active au moment de commencer lenregistrement.",
"localRecordingWarning": "Assurez-vous de sélectionner longlet courant pour utiliser le bon son et la bonne vidéo. Lenregistrement est pour le moment limité à 1Go, soit approximativement 100 minutes.",
"localRecordingWarning": "Assurez-vous de sélectionner longlet courant pour utiliser le bon son et la bonne vidéo.",
"loggedIn": "Connecté en tant que {{userName}}",
"noMicPermission": "La piste microphone ne peut pas être créée. Veuillez autoriser lutilisation du microphone.",
"noStreams": "Aucun flux audio ou vidéo détectés.",

View File

@@ -914,7 +914,7 @@
"localRecordingStartWarningTitle": "Prekini snimanje kako bi se spremilo",
"localRecordingVideoStop": "Prekidanje tvog videa će također prekinuti lokalno snimanje. Stvarno želiš nastaviti?",
"localRecordingVideoWarning": "Za snimanje tvog videa, on mora biti uključen kada započneš snimanje",
"localRecordingWarning": "Odaberi trenutačnu karticu za korištenje ispravnog videa i zvuka. Snimanje je trenutačno ograničeno na 1 GB, što je oko 100 minuta.",
"localRecordingWarning": "Odaberi trenutačnu karticu za korištenje ispravnog videa i zvuka.",
"loggedIn": "Prijava kao {{userName}}",
"noStreams": "Nema otkrivenih audio niti videoprijenosa.",
"off": "Snimanje prekinuto",

View File

@@ -892,7 +892,7 @@
"limitNotificationDescriptionWeb": "Wulkeje potrjeby dla je Waše nahrawanje na {{limit}} mjeń. wobmjezowane. Za njewobmjezowane nahrawanje wužiwajće prošu <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
"linkGenerated": "link na nahrawanje bu kreěrowany",
"live": "LIVE",
"localRecordingWarning": "Kedźbujće na to, aktualny tab za prawe widejo a awdijo wužiwać. Nahrawanje je wokomiknje hač do 1GB - někak 100 mjeńšinow - móžne.",
"localRecordingWarning": "Kedźbujće na to, aktualny tab za prawe widejo a awdijo wužiwać.",
"loggedIn": "přizjewjeny/a jako {{userName}} ",
"off": "nahrawanje stopowane",
"offBy": "{{name}} stopuje nahrawanje",

View File

@@ -726,7 +726,7 @@
"localRecordingNoNotificationWarning": "A felvételt nem közöljük más résztvevőkkel. Értesítenie kell velük, hogy a találkozót rögzítették.",
"localRecordingStartWarning": "A megbeszélésből való kilépés előtt feltétlenül állítsa le a felvételt, hogy elmentse azt.",
"localRecordingStartWarningTitle": "Állítsa le a felvételt a mentéshez",
"localRecordingWarning": "Győződjön meg arról, hogy az aktuális lapot választotta a megfelelő videó és hang használatához. A felvétel jelenleg 1 GB-ra van korlátozva, ami körülbelül 100 perc.",
"localRecordingWarning": "Győződjön meg arról, hogy az aktuális lapot választotta a megfelelő videó és hang használatához.",
"loggedIn": "Belépve mint {{userName}}",
"off": "Felvétel leállítva",
"offBy": "{{name}} leállította a felvételt",

View File

@@ -1026,7 +1026,7 @@
"localRecordingStartWarningTitle": "Hentikan rekaman untuk menyimpannya",
"localRecordingVideoStop": "Menghentikan video Anda juga akan menghentikan rekaman lokal. Apakah Anda yakin ingin melanjutkan?",
"localRecordingVideoWarning": "Untuk merekam video Anda, Anda harus mengaktifkannya saat memulai rekaman",
"localRecordingWarning": "Pastikan Anda memilih tab saat ini untuk menggunakan video dan audio yang tepat. Rekaman saat ini dibatasi hingga 1GB, sekitar 100 menit.",
"localRecordingWarning": "Pastikan Anda memilih tab saat ini untuk menggunakan video dan audio yang tepat.",
"loggedIn": "Masuk sebagai {{userName}}",
"noMicPermission": "Trek mikrofon tidak dapat dibuat. Harap izinkan penggunaan mikrofon.",
"noStreams": "Tidak ada aliran audio atau video yang terdeteksi.",

View File

@@ -1007,7 +1007,7 @@
"localRecordingStartWarningTitle": "Stöðvaðu upptöku til að vista hana",
"localRecordingVideoStop": "Ef þú stöðvar myndmerkið þitt stöðvast einnig upptakan á tölvunni. Ertu viss um að þú viljir halda áfram?",
"localRecordingVideoWarning": "Til að taka upp myndmerkið þitt þarf það að vera í gangi þegar upptakan er sett af stað",
"localRecordingWarning": "Gakktu úr skugga um að þú veljir fyrirliggjandi flipa til að nota rétt mynd- og hljóðmerki. Upptakan er takmörkuð við 1GB, sem gera í kringum 100 mínútur.",
"localRecordingWarning": "Gakktu úr skugga um að þú veljir fyrirliggjandi flipa til að nota rétt mynd- og hljóðmerki.",
"loggedIn": "Skráð inn sem {{userName}}",
"noMicPermission": "Ekki var hægt að útbúa hljóðnemarás. Veittu heimildir til að nota hljóðnemann.",
"noStreams": "Ekkert hljóð eða myndstreymi fannst",

View File

@@ -915,7 +915,7 @@
"localRecordingStartWarningTitle": "Ferma la registrazione per salvarla",
"localRecordingVideoStop": "Fermare il video fermerà anche la registrazione. Sei sicuro di voler terminare?",
"localRecordingVideoWarning": "Per registrare il video, deve essere già avviato prima dell'inizio della registrazione",
"localRecordingWarning": "Assicurati di aver selezionato la scheda corrente, per registrare gli audio e video corretti. La registrazione è limitata ad 1GB, cioè circa 100 minuti.",
"localRecordingWarning": "Assicurati di aver selezionato la scheda corrente, per registrare gli audio e video corretti.",
"loggedIn": "Accesso effettuato come {{userName}}",
"noStreams": "Audio e video non trovati.",
"off": "Registrazione interrotta",

View File

@@ -846,7 +846,7 @@
"localRecordingStartWarningTitle": "録画を停止して保存",
"localRecordingVideoStop": "ビデオを停止すると録画も停止されます。停止してもよろしいですか?",
"localRecordingVideoWarning": "ビデオを録画するためには録画開始前に有効化しておく必要があります",
"localRecordingWarning": "正しく録画をするために必ず現在のタブを選択してください。現在録画は1GBまで、約100分に制限されています。",
"localRecordingWarning": "正しく録画をするために必ず現在のタブを選択してください。",
"loggedIn": "{{userName}} としてログイン",
"noMicPermission": "マイクが認識できませんでした。マイクへのアクセス権を確認してください。",
"noStreams": "音声・映像が検出されませんでした。",

View File

@@ -1046,7 +1046,7 @@
"localRecordingStartWarningTitle": "저장을 위해 녹화를 중지하세요",
"localRecordingVideoStop": "비디오를 중지하면 로컬 녹화도 중지됩니다. 계속하시겠습니까?",
"localRecordingVideoWarning": "비디오를 녹화하려면 녹화를 시작할 때 켜져 있어야 합니다",
"localRecordingWarning": "올바른 비디오 및 오디오를 사용하려면 현재 탭을 선택해야 합니다. 녹화는 현재 1GB로 제한되어 있으며, 이는 약 100분입니다.",
"localRecordingWarning": "올바른 비디오 및 오디오를 사용하려면 현재 탭을 선택해야 합니다.",
"loggedIn": "{{userName}}으로 로그인했습니다.",
"noMicPermission": "마이크 트랙을 생성할 수 없습니다. 마이크 사용 권한을 부여하세요.",
"noStreams": "오디오 또는 비디오 스트림이 감지되지 않았습니다.",

View File

@@ -1053,7 +1053,7 @@
"localRecordingStartWarningTitle": "Apturiet ierakstīšanu, lai to saglabātu",
"localRecordingVideoStop": "Apturot vide, tiks apturēta arī lokālā ierakstīšana. Vai tiešām vēlaties turpināt?",
"localRecordingVideoWarning": "Lai ierakstītu video, tas ir jāieslēdz, uzsākot ierakstīšanu",
"localRecordingWarning": "Noteikti izvēlieties pašreizējo cilni, lai izmantotu pareizo video un audio. Ieraksts pašlaik ir ierobežots līdz 1 GB, kas ir aptuveni 100 minūtes.",
"localRecordingWarning": "Noteikti izvēlieties pašreizējo cilni, lai izmantotu pareizo video un audio.",
"loggedIn": "Pierakstījies kā {{userName}}",
"noMicPermission": "Mikrofona ierakstu nevarēja izveidot. Lūdzu, piešķiriet atļauju lietot mikrofonu.",
"noStreams": "Nav konstatēta audio vai video straume.",

View File

@@ -943,7 +943,7 @@
"localRecordingStartWarningTitle": "Хадгалахын тулд бичлэгээ зогсооно уу",
"localRecordingVideoStop": "Видео бичлэгээ зогсоовол өөр дээрээ файлаар хадгалах нь хамтдаа зогсоно. Та үргэлжлүүлэхдээ итгэлтэй байна уу?",
"localRecordingVideoWarning": "To record your video you must have it on when starting the recording",
"localRecordingWarning": "Дуу болон видео дамжуулалтын зөвшөөрлийг өгөхийн тулд энэ табыг сонгоно уу. Бичлэгийн дээд хэмжээ нь 1ГБ буюу ойролцоогоор The recording is currently limited to 1GB, which is around 100 minutes.",
"localRecordingWarning": "Дуу болон видео дамжуулалтын зөвшөөрлийг өгөхийн тулд энэ табыг сонгоно уу.",
"loggedIn": "{userName} нэвтэрнэ үү",
"noMicPermission": "Микрофон олдсонгүй. Микрофон ашиглах зөвшөөрлөө өгнө үү.",
"noStreams": "Дуу эсвэл видео дамжуулалт олдсонгүй.",

View File

@@ -1047,7 +1047,7 @@
"localRecordingStartWarningTitle": "Stopp opptaket for å lagre det",
"localRecordingVideoStop": "Hvis du stopper videoen, vil også det lokale opptaket stoppe. Er du sikker på at du vil fortsette?",
"localRecordingVideoWarning": "For å ta opp videoen din, må du ha den på når opptaket starter",
"localRecordingWarning": "Pass på å velge den gjeldende fanen for å bruke riktig video og lyd. Opptaket er for øyeblikket begrenset til 1 GB, noe som tilsvarer rundt 100 minutter.",
"localRecordingWarning": "Pass på å velge den gjeldende fanen for å bruke riktig video og lyd.",
"loggedIn": "Logget inn som {{userName}}",
"noMicPermission": "Kunne ikke opprette mikrofonspor. Vennligst gi tillatelse til å bruke mikrofonen.",
"noStreams": "Ingen lyd- eller videostrøm oppdaget.",

View File

@@ -1047,7 +1047,7 @@
"localRecordingStartWarningTitle": "Stopp opptaket for å lagre det",
"localRecordingVideoStop": "Hvis du stopper videoen, vil også det lokale opptaket stoppe. Er du sikker på at du vil fortsette?",
"localRecordingVideoWarning": "For å ta opp videoen din, må du ha den på når opptaket starter",
"localRecordingWarning": "Pass på å velge den gjeldende fanen for å bruke riktig video og lyd. Opptaket er for øyeblikket begrenset til 1 GB, noe som tilsvarer rundt 100 minutter.",
"localRecordingWarning": "Pass på å velge den gjeldende fanen for å bruke riktig video og lyd.",
"loggedIn": "Logget inn som {{userName}}",
"noMicPermission": "Kunne ikke opprette mikrofonspor. Vennligst gi tillatelse til å bruke mikrofonen.",
"noStreams": "Ingen lyd- eller videostrøm oppdaget.",

View File

@@ -1047,7 +1047,7 @@
"localRecordingStartWarningTitle": "Arrestar l'enregistrament per lo salvar",
"localRecordingVideoStop": "Arrestar la vidèo arrestarà tanben l'enregistrament local. Volètz vertadièrament contunhar?",
"localRecordingVideoWarning": "Per enregistrar vòstra vidèo, devètz l'aver activada en començant l'enregistrament",
"localRecordingWarning": "Asseguratz-vos de causir l'onglet actual per utilizar lo son e la vidèo corrècts. L'enregistrament es actualament limitat a 1Go, siá aproximativament 100minutas.",
"localRecordingWarning": "Asseguratz-vos de causir l'onglet actual per utilizar lo son e la vidèo corrècts.",
"loggedIn": "Session a {{userName}}",
"noMicPermission": "Se pòt pas crear la pista del microfòn. Volgatz autorizar l'utilizacion del microfòn.",
"noStreams": "Cap de flux àudio o vidèo pas detectat.",

View File

@@ -946,7 +946,7 @@
"localRecordingStartWarningTitle": "Zatrzymaj nagrywanie, aby je zapisać",
"localRecordingVideoStop": "Zatrzymanie filmu spowoduje również zatrzymanie nagrywania lokalnego. Jesteś pewien, że chcesz kontynuować?",
"localRecordingVideoWarning": "Aby nagrać film, musisz go mieć na początku nagrywania",
"localRecordingWarning": "Upewnij się, że wybrałeś bieżącą kartę, aby użyć odpowiedniego obrazu i dźwięku. Nagranie jest obecnie ograniczone do 1 GB, czyli około 100 minut.",
"localRecordingWarning": "Upewnij się, że wybrałeś bieżącą kartę, aby użyć odpowiedniego obrazu i dźwięku.",
"loggedIn": "Zalogowano jako {{userName}}",
"noMicPermission": "Nie można utworzyć ścieżki mikrofonu. Zezwól na korzystanie z mikrofonu.",
"noStreams": "Nie wykryto strumienia audio lub wideo.",

View File

@@ -1035,7 +1035,7 @@
"localRecordingStartWarningTitle": "Parar a gravação para a salvar",
"localRecordingVideoStop": "A paragem do seu vídeo também irá parar a gravação local. Tem a certeza de que quer continuar?",
"localRecordingVideoWarning": "Para gravar o seu vídeo deve tê-lo ligado quando iniciar a gravação",
"localRecordingWarning": "Certifique-se de selecionar o separador actual a fim de utilizar o vídeo e áudio corretos. A gravação está actualmente limitada a 1 GB, o que é cerca de 100 minutos.",
"localRecordingWarning": "Certifique-se de selecionar o separador actual a fim de utilizar o vídeo e áudio corretos.",
"loggedIn": "Conectado como {{userName}}",
"noMicPermission": "Não foi possível criar a faixa de microfone. Por favor, conceda permissão para utilizar o microfone.",
"noStreams": "Não foi detetado nenhum sinal áudio ou vídeo.",

View File

@@ -1007,7 +1007,7 @@
"localRecordingStartWarningTitle": "Parar a gravação para salvá-la",
"localRecordingVideoStop": "Ao parar o seu vídeo a gravação local também será parada. Tem certeza que deseja continuar?",
"localRecordingVideoWarning": "Para gravar o seu vídeo você precisa ativá-lo antes de inicar a gravação",
"localRecordingWarning": "Tenha certeza de selecionar a aba atual para usar o áudio e vídeo corretos. A gravação está atualmente limitada a 1GB, que corresponde a aproximadamente 100 minutos.",
"localRecordingWarning": "Tenha certeza de selecionar a aba atual para usar o áudio e vídeo corretos.",
"loggedIn": "Conectado como {{userName}}",
"noMicPermission": "Trilha para o microfone não pôde ser criada. Por favor conceda permissão para usar o microfone.",
"noStreams": "Nenhum fluxo de áudio ou vídeo detectado.",

View File

@@ -1021,7 +1021,7 @@
"localRecordingStartWarningTitle": "Остановите запись для сохранения",
"localRecordingVideoStop": "Остановка вашего видео также остановит локальную запись. Вы уверены, что хотите продолжить?",
"localRecordingVideoWarning": "Чтобы записать ваше видео, оно должно быть включено при начале записи",
"localRecordingWarning": "Убедитесь, что вы выбрали текущую вкладку для использования правильного видео и аудио. Запись в настоящее время ограничена 1ГБ, что составляет около 100 минут.",
"localRecordingWarning": "Убедитесь, что вы выбрали текущую вкладку для использования правильного видео и аудио.",
"loggedIn": "Вошел как {{userName}}",
"noMicPermission": "Не удалось создать аудиодорожку микрофона. Пожалуйста, предоставьте разрешение на использование микрофона.",
"noStreams": "Аудио или видеопоток не обнаружен.",

View File

@@ -916,7 +916,7 @@
"localRecordingStartWarningTitle": "Firma sa registratzione pro dda sarvare",
"localRecordingVideoStop": "Istudende su vìdeu as a firmare puru sa registratzione locale. Ses seguru de chèrrere sighire?",
"localRecordingVideoWarning": "Pro registrare unu vìdeu, depet èssere allutu prima de cumintzare",
"localRecordingWarning": "Assegura·ti chi seletzionas s'ischeda atuale in manera de impreare s'àudio e su vìdeu curretos. Sa registratzione est limitada a 1 GB, est a nàrrere, a s'inghìriu de 100 minutos.",
"localRecordingWarning": "Assegura·ti chi seletzionas s'ischeda atuale in manera de impreare s'àudio e su vìdeu curretos.",
"loggedIn": "Autenticatzione: {{userName}}",
"noStreams": "Nissunu flussu de vìdeu o de àudio rilevadu.",
"off": "Registratzione firmada",

View File

@@ -1046,7 +1046,7 @@
"localRecordingStartWarningTitle": "Ndaleni regjistrimin që ta ruani",
"localRecordingVideoStop": "Ndalja e videos tuaj do të ndalë gjithashtu edhe regjistrimin vendor. Jeni i sigurt se doni të vazhdohet?",
"localRecordingVideoWarning": "Që ta regjistroni, videon tuaj duhet ta keni të hapur, kur niset regjistrimi",
"localRecordingWarning": "Sigurohuni se përzgjidhni skedën e tanishme, që të mund të përdoret videoja dhe audioja e saktë. Regjistrimi aktualisht është i kufizuar deri në 1GB, çka është aty afër 100 minutave.",
"localRecordingWarning": "Sigurohuni se përzgjidhni skedën e tanishme, që të mund të përdoret videoja dhe audioja e saktë.",
"loggedIn": "I futur si {{userName}}",
"noMicPermission": "Su krijua dot pistë mikrofoni. Ju lutemi, akordoni leje për përdorim të mikrofonit.",
"noStreams": "Su pikas rrjedhë audio ose video.",

View File

@@ -1047,7 +1047,7 @@
"localRecordingStartWarningTitle": "Stoppa inspelningen för att spara den",
"localRecordingVideoStop": "Om du stoppar din video stoppas även den lokala inspelningen. Är du säker på att du vill fortsätta?",
"localRecordingVideoWarning": "För att spela in din video måste du ha den på när du startar inspelningen",
"localRecordingWarning": "Se till att du väljer den aktuella fliken för att kunna använda rätt video och ljud. Inspelningen är för närvarande begränsad till 1 GB, vilket är cirka 100 minuter.",
"localRecordingWarning": "Se till att du väljer den aktuella fliken för att kunna använda rätt video och ljud.",
"loggedIn": "Inloggad som {{userName}}",
"noMicPermission": "Mikrofonspåret kunde inte skapas. Vänligen ge tillstånd att använda mikrofonen.",
"noStreams": "Ingen ljud- eller videoström upptäcktes.",

View File

@@ -1041,7 +1041,7 @@
"localRecordingStartWarningTitle": "Kaydetmek için kaydı durdurun",
"localRecordingVideoStop": "Videonuzu durdurmak yerel kaydı da durduracaktır. Devam etmek istediğine emin misin?",
"localRecordingVideoWarning": "Videonuzu kaydetmek için kayda başlarken açmış olmanız gerekir",
"localRecordingWarning": "Doğru video ve sesi kullanmak için geçerli sekmeyi seçtiğinizden emin olun. Kayıt şu anda yaklaşık 100 dakika olan 1GB ile sınırlıdır.",
"localRecordingWarning": "Doğru video ve sesi kullanmak için geçerli sekmeyi seçtiğinizden emin olun.",
"loggedIn": "{{userName}} olarak giriş yapıldı",
"noMicPermission": "Mikrofon parçası oluşturulamadı. Lütfen mikrofonu kullanma izni verin.",
"noStreams": "Ses veya video akışı algılanmadı",

View File

@@ -941,7 +941,7 @@
"localRecordingStartWarningTitle": "Зупиніть запис, щоб зберегти його",
"localRecordingVideoStop": "Вимикання камери також припинить локальний запис. Продовжити?",
"localRecordingVideoWarning": "Щоб записати ваше відео, камера має бути увімкнена до початку запису",
"localRecordingWarning": "Виберіть поточну вкладку веб-браузера, щоб використовувати правильні налаштування камери та мікрофона. Запис наразі обмежений 1 ГБ, що становить приблизно 100 хв.",
"localRecordingWarning": "Виберіть поточну вкладку веб-браузера, щоб використовувати правильні налаштування камери та мікрофона.",
"loggedIn": "Ви ввійшли як {{userName}}",
"noMicPermission": "Не вдалося створити мікрофонну доріжку. Надайте дозвіл на доступ до мікрофона.",
"noStreams": "Аудіо чи відео потік не виявлено.",

View File

@@ -1019,7 +1019,7 @@
"localRecordingStartWarningTitle": "Dừng ghi âm để lưu lại",
"localRecordingVideoStop": "Dừng video của bạn cũng sẽ dừng ghi âm cục bộ. Bạn có chắc chắn muốn tiếp tục không?",
"localRecordingVideoWarning": "Để ghi lại video của bạn, bạn phải bật nó khi bắt đầu ghi âm",
"localRecordingWarning": "Đảm bảo bạn chọn tab hiện tại để sử dụng video và âm thanh đúng cách. Hiện tại, ghi âm bị giới hạn chỉ là 1GB, tương đương với khoảng 100 phút.",
"localRecordingWarning": "Đảm bảo bạn chọn tab hiện tại để sử dụng video và âm thanh đúng cách.",
"loggedIn": "Đã đăng nhập dưới tên {{userName}}",
"noMicPermission": "Không thể tạo track microphone. Vui lòng cấp quyền sử dụng microphone.",
"noStreams": "Không phát hiện luồng âm thanh hoặc video nào.",

View File

@@ -989,7 +989,7 @@
"localRecordingStartWarningTitle": "停止录制以保存内容",
"localRecordingVideoStop": "停止视频也会停止本地录制,你确定要继续吗?",
"localRecordingVideoWarning": "要录制视频,你必须在开始录制时保持视频开启",
"localRecordingWarning": "请确保你选择当前的标签页,以便录制正确的视频和音频。录制目前限制在1GB大约100分钟。",
"localRecordingWarning": "请确保你选择当前的标签页,以便录制正确的视频和音频。",
"loggedIn": "以{{userName}}登录",
"noMicPermission": "无法创建麦克风轨道,请允许使用麦克风。",
"noStreams": "未检测到音频或视频。",

View File

@@ -1006,7 +1006,7 @@
"localRecordingStartWarningTitle": "停用錄製以保存",
"localRecordingVideoStop": "關閉您的視訊也將停止本機錄製,確定繼續嗎?",
"localRecordingVideoWarning": "錄製視訊必須在開始時啟用",
"localRecordingWarning": "確保選擇目前的分頁以錄製正確的視訊和音訊。錄製目前限制為1GB約可錄製100分鐘。",
"localRecordingWarning": "確保選擇目前的分頁以錄製正確的視訊和音訊。",
"loggedIn": "以 {{userName}} 登入",
"noMicPermission": "無法建立麥克風軌,請允許使用麥克風的權限。",
"noStreams": "未檢測到音訊或視訊。",

View File

@@ -1064,7 +1064,7 @@
"localRecordingStartWarningTitle": "Stop the recording to save it",
"localRecordingVideoStop": "Stopping your video will also stop the local recording. Are you sure you want to continue?",
"localRecordingVideoWarning": "To record your video you must have it on when starting the recording",
"localRecordingWarning": "Make sure you select the current tab in order to use the right video and audio. The recording is currently limited to 1GB, which is around 100 minutes.",
"localRecordingWarning": "Make sure you select the current tab in order to use the right video and audio.",
"loggedIn": "Logged in as {{userName}}",
"noMicPermission": "Microphone track could not be created. Please grant permission to use the microphone.",
"noStreams": "No audio or video stream detected.",

359
package-lock.json generated
View File

@@ -112,6 +112,7 @@
"seamless-scroll-polyfill": "2.1.8",
"semver": "7.5.4",
"text-encoding": "0.7.0",
"ts-ebml": "^3.0.1",
"tss-react": "4.9.4",
"util": "0.12.1",
"uuid": "8.3.2",
@@ -10061,6 +10062,19 @@
"node": "*"
}
},
"node_modules/bigint-buffer": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz",
"integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"bindings": "^1.3.0"
},
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@@ -10070,6 +10084,15 @@
"node": ">=8"
}
},
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"license": "MIT",
"dependencies": {
"file-uri-to-path": "1.0.0"
}
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
@@ -10281,6 +10304,14 @@
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
},
"node_modules/buffers": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
"integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==",
"engines": {
"node": ">=0.2.0"
}
},
"node_modules/builtin-modules": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-2.0.0.tgz",
@@ -11217,6 +11248,12 @@
"node": ">=4"
}
},
"node_modules/crc": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/crc/-/crc-3.2.1.tgz",
"integrity": "sha512-H21TaZQyic++ilBStWHntVpS2STWO37tzE0w0P5iAY1ntaPVtlZ3E6FcwltyZa6MYrEbKMxjEwXh3fBHlW8Qqw==",
"license": "MIT"
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
@@ -11528,6 +11565,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/dateformat": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.11.tgz",
"integrity": "sha512-5zcI+Qoul0QgiCcjzsobs9G1c+vHCNaYB2NGa57ZbVnP7bZeKJJgoM/K+I/obaHAmyEL0J8hJafbQ+HFSZnzZA==",
"dependencies": {
"get-stdin": "*",
"meow": "*"
},
"bin": {
"dateformat": "bin/cli.js"
},
"engines": {
"node": "*"
}
},
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
@@ -11925,6 +11977,40 @@
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true
},
"node_modules/ebml": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ebml/-/ebml-3.0.0.tgz",
"integrity": "sha512-Q6C1u4/TX1nYipT9HNIopp95YyyyI0zs1GXdNRKO7XL7k+oo+ZtDc1CaJjpCdmlLxWsnlKBOXJCXkYU0K/Anlg==",
"license": "MIT",
"dependencies": {
"buffers": "^0.1.1",
"debug": "~3.1.0"
},
"engines": {
"node": ">= 6.4"
}
},
"node_modules/ebml-block": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/ebml-block/-/ebml-block-1.1.2.tgz",
"integrity": "sha512-HgNlIsRFP6D9VKU5atCeHRJY7XkJP8bOe8yEhd8NB7B3b4++VWTyauz6g650iiPmLfPLGlVpoJmGSgMfXDYusg==",
"license": "MIT"
},
"node_modules/ebml/node_modules/debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/ebml/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -13799,6 +13885,12 @@
"node": "^10.12.0 || >=12.0.0"
}
},
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"license": "MIT"
},
"node_modules/filelist": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
@@ -13997,7 +14089,6 @@
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"dev": true,
"funding": [
{
"type": "individual",
@@ -14311,6 +14402,18 @@
"node": ">= 0.4"
}
},
"node_modules/get-stdin": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz",
"integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@@ -15140,6 +15243,12 @@
"node": ">=18"
}
},
"node_modules/int64-buffer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/int64-buffer/-/int64-buffer-1.1.0.tgz",
"integrity": "sha512-94smTCQOvigN4d/2R/YDjz8YVG0Sufvv2aAh8P5m42gwhCsDAJqnbNOrxJsrADuAFAA69Q/ptGzxvNcNuIJcvw==",
"license": "MIT"
},
"node_modules/internal-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@@ -17579,6 +17688,62 @@
"node": ">= 0.4"
}
},
"node_modules/matroska": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/matroska/-/matroska-2.2.5.tgz",
"integrity": "sha512-6+yf536Aofg8jVWQcLl/z2v8PXZMkjUs080t+Nyz++BZSJ+0U6c2RBudXqKT6DrV89DdNkklyHhHlWerpS06hw==",
"license": "GPL",
"dependencies": {
"async": "1.0.0",
"crc": "3.2.1",
"dateformat": "1.0.11",
"debug": "^2.6.9",
"follow-redirects": "^1.14.9",
"mime": "^1.6.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/matroska-schema": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/matroska-schema/-/matroska-schema-2.1.0.tgz",
"integrity": "sha512-6c1oFmDxf4Vc5J5lA+9wO7TKcw5M1w85HfzFhAFT4OuEUuqp/s/jqqC3OKlaWe1YwN5wTThJyTC7iwhyW7kQdg==",
"license": "MIT"
},
"node_modules/matroska/node_modules/async": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz",
"integrity": "sha512-5mO7DX4CbJzp9zjaFXusQQ4tzKJARjNB1Ih1pVBi8wkbmXy/xzIDgEMXxWePLzt2OdFwaxfneIlT1nCiXubrPQ==",
"license": "MIT"
},
"node_modules/matroska/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/matroska/node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/matroska/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/md5": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
@@ -17628,6 +17793,18 @@
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
},
"node_modules/meow": {
"version": "13.2.0",
"resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz",
"integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
@@ -23777,6 +23954,34 @@
"resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz",
"integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ=="
},
"node_modules/ts-ebml": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ts-ebml/-/ts-ebml-3.0.1.tgz",
"integrity": "sha512-gPpBXaePtnBR/MYRUyyJybD6vikWf9cY57MSWx+1lTmItSl/ESQj1GouBEuTs6f9OZfM+y8ORrY6QyBWWH8CUg==",
"license": "MIT",
"dependencies": {
"bigint-buffer": "^1.1.5",
"commander": "^12.0.0",
"ebml": "^3.0.0",
"ebml-block": "^1.1.2",
"events": "^3.3.0",
"int64-buffer": "^1.0.1",
"matroska": "^2.2.5",
"matroska-schema": "^2.1.0"
},
"bin": {
"ts-ebml": "lib/cli.js"
}
},
"node_modules/ts-ebml/node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/ts-loader": {
"version": "9.4.2",
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.2.tgz",
@@ -32514,12 +32719,28 @@
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
"integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ=="
},
"bigint-buffer": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz",
"integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==",
"requires": {
"bindings": "^1.3.0"
}
},
"binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true
},
"bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"requires": {
"file-uri-to-path": "1.0.0"
}
},
"bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
@@ -32676,6 +32897,11 @@
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
},
"buffers": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
"integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="
},
"builtin-modules": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-2.0.0.tgz",
@@ -33343,6 +33569,11 @@
"parse-json": "^4.0.0"
}
},
"crc": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/crc/-/crc-3.2.1.tgz",
"integrity": "sha512-H21TaZQyic++ilBStWHntVpS2STWO37tzE0w0P5iAY1ntaPVtlZ3E6FcwltyZa6MYrEbKMxjEwXh3fBHlW8Qqw=="
},
"crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
@@ -33567,6 +33798,15 @@
"is-data-view": "^1.0.1"
}
},
"dateformat": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.11.tgz",
"integrity": "sha512-5zcI+Qoul0QgiCcjzsobs9G1c+vHCNaYB2NGa57ZbVnP7bZeKJJgoM/K+I/obaHAmyEL0J8hJafbQ+HFSZnzZA==",
"requires": {
"get-stdin": "*",
"meow": "*"
}
},
"dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
@@ -33840,6 +34080,35 @@
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true
},
"ebml": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ebml/-/ebml-3.0.0.tgz",
"integrity": "sha512-Q6C1u4/TX1nYipT9HNIopp95YyyyI0zs1GXdNRKO7XL7k+oo+ZtDc1CaJjpCdmlLxWsnlKBOXJCXkYU0K/Anlg==",
"requires": {
"buffers": "^0.1.1",
"debug": "~3.1.0"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
}
}
},
"ebml-block": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/ebml-block/-/ebml-block-1.1.2.tgz",
"integrity": "sha512-HgNlIsRFP6D9VKU5atCeHRJY7XkJP8bOe8yEhd8NB7B3b4++VWTyauz6g650iiPmLfPLGlVpoJmGSgMfXDYusg=="
},
"ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -35177,6 +35446,11 @@
"flat-cache": "^3.0.4"
}
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
},
"filelist": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
@@ -35337,8 +35611,7 @@
"follow-redirects": {
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"dev": true
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA=="
},
"for-each": {
"version": "0.3.3",
@@ -35534,6 +35807,11 @@
"es-object-atoms": "^1.0.0"
}
},
"get-stdin": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz",
"integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA=="
},
"get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@@ -36137,6 +36415,11 @@
"rxjs": "^7.8.1"
}
},
"int64-buffer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/int64-buffer/-/int64-buffer-1.1.0.tgz",
"integrity": "sha512-94smTCQOvigN4d/2R/YDjz8YVG0Sufvv2aAh8P5m42gwhCsDAJqnbNOrxJsrADuAFAA69Q/ptGzxvNcNuIJcvw=="
},
"internal-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@@ -37916,6 +38199,49 @@
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
},
"matroska": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/matroska/-/matroska-2.2.5.tgz",
"integrity": "sha512-6+yf536Aofg8jVWQcLl/z2v8PXZMkjUs080t+Nyz++BZSJ+0U6c2RBudXqKT6DrV89DdNkklyHhHlWerpS06hw==",
"requires": {
"async": "1.0.0",
"crc": "3.2.1",
"dateformat": "1.0.11",
"debug": "^2.6.9",
"follow-redirects": "^1.14.9",
"mime": "^1.6.0"
},
"dependencies": {
"async": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz",
"integrity": "sha512-5mO7DX4CbJzp9zjaFXusQQ4tzKJARjNB1Ih1pVBi8wkbmXy/xzIDgEMXxWePLzt2OdFwaxfneIlT1nCiXubrPQ=="
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
}
}
},
"matroska-schema": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/matroska-schema/-/matroska-schema-2.1.0.tgz",
"integrity": "sha512-6c1oFmDxf4Vc5J5lA+9wO7TKcw5M1w85HfzFhAFT4OuEUuqp/s/jqqC3OKlaWe1YwN5wTThJyTC7iwhyW7kQdg=="
},
"md5": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
@@ -37955,6 +38281,11 @@
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
},
"meow": {
"version": "13.2.0",
"resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz",
"integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA=="
},
"merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
@@ -42250,6 +42581,28 @@
"resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz",
"integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ=="
},
"ts-ebml": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ts-ebml/-/ts-ebml-3.0.1.tgz",
"integrity": "sha512-gPpBXaePtnBR/MYRUyyJybD6vikWf9cY57MSWx+1lTmItSl/ESQj1GouBEuTs6f9OZfM+y8ORrY6QyBWWH8CUg==",
"requires": {
"bigint-buffer": "^1.1.5",
"commander": "^12.0.0",
"ebml": "^3.0.0",
"ebml-block": "^1.1.2",
"events": "^3.3.0",
"int64-buffer": "^1.0.1",
"matroska": "^2.2.5",
"matroska-schema": "^2.1.0"
},
"dependencies": {
"commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="
}
}
},
"ts-loader": {
"version": "9.4.2",
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.2.tgz",

View File

@@ -118,6 +118,7 @@
"seamless-scroll-polyfill": "2.1.8",
"semver": "7.5.4",
"text-encoding": "0.7.0",
"ts-ebml": "^3.0.1",
"tss-react": "4.9.4",
"util": "0.12.1",
"uuid": "8.3.2",

View File

@@ -546,6 +546,7 @@ export interface IConfig {
requireConsent?: boolean;
showPrejoinWarning?: boolean;
showRecordingLink?: boolean;
skipConsentInMeeting?: boolean;
suggestRecording?: boolean;
};
remoteVideoMenu?: {

View File

@@ -176,6 +176,7 @@ class Popover extends Component<IProps, IState> {
this._setContextMenuStyle = this._setContextMenuStyle.bind(this);
this._getCustomDialogStyle = this._getCustomDialogStyle.bind(this);
this._onOutsideClick = this._onOutsideClick.bind(this);
this._onOutsideTouchStart = this._onOutsideTouchStart.bind(this);
}
/**
@@ -185,7 +186,7 @@ class Popover extends Component<IProps, IState> {
* @returns {void}
*/
override componentDidMount() {
window.addEventListener('touchstart', this._onTouchStart);
window.addEventListener('touchstart', this._onOutsideTouchStart);
if (this.props.trigger === 'click') {
// @ts-ignore
window.addEventListener('click', this._onOutsideClick);
@@ -199,7 +200,7 @@ class Popover extends Component<IProps, IState> {
* @returns {void}
*/
override componentWillUnmount() {
window.removeEventListener('touchstart', this._onTouchStart);
window.removeEventListener('touchstart', this._onOutsideTouchStart);
if (this.props.trigger === 'click') {
// @ts-ignore
window.removeEventListener('click', this._onOutsideClick);
@@ -261,6 +262,7 @@ class Popover extends Component<IProps, IState> {
id = { id }
onClick = { this._onClick }
onKeyPress = { this._onKeyPress }
onTouchStart = { this._onTouchStart }
{ ...(trigger === 'hover' ? {
onMouseEnter: this._onShowDialog,
onMouseLeave: this._onHideDialog
@@ -337,7 +339,7 @@ class Popover extends Component<IProps, IState> {
* @private
* @returns {void}
*/
_onTouchStart(event: TouchEvent) {
_onOutsideTouchStart(event: TouchEvent) {
if (this.props.visible
&& !this.props.overflowDrawer
&& !this._contextMenuRef?.contains?.(event.target as Node)
@@ -401,6 +403,24 @@ class Popover extends Component<IProps, IState> {
}
}
/**
* Stops propagation of touchstart events originating from the Popover's trigger container.
* This prevents the window's 'touchstart' listener (_onOutsideTouchStart) from
* immediately closing the Popover if the touch begins on the trigger area itself.
* Without this, the subsequent synthesized 'click' event will not execute
* because the Popover would already be closing or removed, breaking interactions
* within the Popover on touch devices.
*
* e.g. On a mobile device overflow buttons don't execute their click actions.
*
* @param {React.TouchEvent} event - The touch start event.
* @private
* @returns {void}
*/
_onTouchStart(event: React.TouchEvent) {
event.stopPropagation();
}
/**
* KeyPress handler for accessibility.
*

View File

@@ -180,6 +180,7 @@ export interface IDynamicBrandingState {
requireRecordingConsent?: boolean;
sharedVideoAllowedURLDomains?: Array<string>;
showGiphyIntegration?: boolean;
skipRecordingConsentInMeeting?: boolean;
supportUrl?: string;
useDynamicBrandingData: boolean;
virtualBackgrounds: Array<Image>;
@@ -206,9 +207,10 @@ ReducerRegistry.register<IDynamicBrandingState>(STORE_NAME, (state = DEFAULT_STA
muiBrandedTheme,
pollCreationRequiresPermission,
premeetingBackground,
requireRecordingConsent,
sharedVideoAllowedURLDomains,
showGiphyIntegration,
requireRecordingConsent,
skipRecordingConsentInMeeting,
supportUrl,
virtualBackgrounds
} = action.value;
@@ -228,9 +230,10 @@ ReducerRegistry.register<IDynamicBrandingState>(STORE_NAME, (state = DEFAULT_STA
muiBrandedTheme,
pollCreationRequiresPermission,
premeetingBackground,
requireRecordingConsent,
sharedVideoAllowedURLDomains,
showGiphyIntegration,
requireRecordingConsent,
skipRecordingConsentInMeeting,
supportUrl,
customizationFailed: false,
customizationReady: true,

View File

@@ -8,6 +8,16 @@
*/
export const CLEAR_RECORDING_SESSIONS = 'CLEAR_RECORDING_SESSIONS';
/**
* The type of Redux action which marks a session ID as consent requested.
*
* {
* type: MARK_CONSENT_REQUESTED,
* sessionId: string
* }
*/
export const MARK_CONSENT_REQUESTED = 'MARK_CONSENT_REQUESTED';
/**
* The type of Redux action which updates the current known state of a recording
* session.

View File

@@ -20,6 +20,7 @@ import { isRecorderTranscriptionsRunning } from '../transcribing/functions';
import {
CLEAR_RECORDING_SESSIONS,
MARK_CONSENT_REQUESTED,
RECORDING_SESSION_UPDATED,
SET_MEETING_HIGHLIGHT_BUTTON_STATE,
SET_PENDING_RECORDING_NOTIFICATION_UID,
@@ -476,3 +477,17 @@ export function showStartRecordingNotificationWithCallback(openRecordingDialog:
}, NOTIFICATION_TIMEOUT_TYPE.EXTRA_LONG));
};
}
/**
* Marks the given session as consent requested. No further consent requests will be
* made for this session.
*
* @param {string} sessionId - The session id.
* @returns {Object}
*/
export function markConsentRequested(sessionId: string) {
return {
type: MARK_CONSENT_REQUESTED,
sessionId
};
}

View File

@@ -1,4 +1,5 @@
import i18next from 'i18next';
// @ts-ignore
import * as ebml from 'ts-ebml/dist/EBML.min.js';
import { v4 as uuidV4 } from 'uuid';
import { IStore } from '../../../app/types';
@@ -20,6 +21,7 @@ interface ILocalRecordingManager {
audioContext: AudioContext | undefined;
audioDestination: MediaStreamAudioDestinationNode | undefined;
fileHandle: FileSystemFileHandle | undefined;
firstChunk: Blob | undefined;
getFilename: () => string;
initializeAudioMixer: () => void;
isRecordingLocally: () => boolean;
@@ -30,20 +32,22 @@ interface ILocalRecordingManager {
roomName: string;
selfRecording: ISelfRecording;
startLocalRecording: (store: IStore, onlySelf: boolean) => Promise<void>;
startTime: number | undefined;
stopLocalRecording: () => void;
stream: MediaStream | undefined;
writableStream: FileSystemWritableFileStream | undefined;
}
/**
* We want to use the MP4 container due to it not suffering from the resulting file
* not being seek-able.
* After a lot of trial and error, this is the preferred media type for
* local recording. It is the only one that works across all platforms, with the
* only caveat being that the resulting file wouldn't be seekable.
*
* The choice of VP9 as the video codec and Opus as the audio codec is for compatibility.
* While Chrome does support avc1 and avc3 (we'd need the latter since the resolution can change)
* it's not supported across the board.
* We solve that by fixing the first Blob in order to reserve the space for the
* corrected metadata, and after the recording is done, we do it again, this time with
* the real duration, and overwrite the first part of the file.
*/
const PREFERRED_MEDIA_TYPE = 'video/mp4;codecs=vp9,opus';
const PREFERRED_MEDIA_TYPE = 'video/webm;codecs=vp8,opus';
const VIDEO_BIT_RATE = 2500000; // 2.5Mbps in bits
@@ -58,7 +62,9 @@ const LocalRecordingManager: ILocalRecordingManager = {
on: false,
withVideo: false
},
firstChunk: undefined,
fileHandle: undefined,
startTime: undefined,
writableStream: undefined,
get mediaType() {
@@ -126,16 +132,7 @@ const LocalRecordingManager: ILocalRecordingManager = {
* @returns {void}
* */
stopLocalRecording() {
if (this.recorder) {
this.recorder.stop();
this.recorder = undefined;
this.audioContext = undefined;
this.audioDestination = undefined;
this.writableStream?.close().then(() => {
this.fileHandle = undefined;
this.writableStream = undefined;
});
}
this.recorder?.stop();
},
/**
@@ -153,15 +150,14 @@ const LocalRecordingManager: ILocalRecordingManager = {
// Get a handle to the file we are going to write.
const options = {
startIn: 'downloads',
suggestedName: `${this.getFilename()}.mp4`,
suggestedName: `${this.getFilename()}.webm`,
};
// @ts-expect-error
this.fileHandle = await window.showSaveFilePicker(options);
this.writableStream = await this.fileHandle?.createWritable();
// @ts-ignore
const supportsCaptureHandle = Boolean(navigator.mediaDevices.setCaptureHandleConfig) && !isEmbedded();
const supportsCaptureHandle = !isEmbedded();
const tabId = uuidV4();
this.selfRecording.on = onlySelf;
@@ -169,25 +165,19 @@ const LocalRecordingManager: ILocalRecordingManager = {
const tracks = getTrackState(getState());
if (onlySelf) {
let audioTrack: MediaStreamTrack | undefined = getLocalTrack(tracks, MEDIA_TYPE.AUDIO)?.jitsiTrack?.track;
const audioTrack: MediaStreamTrack | undefined = getLocalTrack(tracks, MEDIA_TYPE.AUDIO)?.jitsiTrack?.track;
let videoTrack: MediaStreamTrack | undefined = getLocalTrack(tracks, MEDIA_TYPE.VIDEO)?.jitsiTrack?.track;
if (!audioTrack) {
APP.conference.muteAudio(false);
setTimeout(() => APP.conference.muteAudio(true), 100);
await new Promise(resolve => {
setTimeout(resolve, 100);
});
}
if (videoTrack && videoTrack.readyState !== 'live') {
videoTrack = undefined;
}
audioTrack = getLocalTrack(getTrackState(getState()), MEDIA_TYPE.AUDIO)?.jitsiTrack?.track;
if (!audioTrack && !videoTrack) {
throw new Error('NoLocalStreams');
}
this.selfRecording.withVideo = Boolean(videoTrack);
const localTracks = [];
const localTracks: MediaStreamTrack[] = [];
audioTrack && localTracks.push(audioTrack);
videoTrack && localTracks.push(videoTrack);
@@ -200,77 +190,111 @@ const LocalRecordingManager: ILocalRecordingManager = {
permittedOrigins: [ '*' ]
});
}
const localAudioTrack = getLocalTrack(tracks, MEDIA_TYPE.AUDIO)?.jitsiTrack?.track;
// Starting chrome 107, the recorder does not record any data if the audio stream has no tracks
// To fix this we create a track for the local user(muted track)
if (!localAudioTrack) {
APP.conference.muteAudio(false);
setTimeout(() => APP.conference.muteAudio(true), 100);
await new Promise(resolve => {
setTimeout(resolve, 100);
});
}
// handle no mic permission
if (!getLocalTrack(getTrackState(getState()), MEDIA_TYPE.AUDIO)?.jitsiTrack?.track) {
throw new Error('NoMicTrack');
}
const currentTitle = document.title;
document.title = i18next.t('localRecording.selectTabTitle');
// @ts-ignore
gdmStream = await navigator.mediaDevices.getDisplayMedia({
video: { displaySurface: 'browser',
frameRate: 30 },
audio: false, // @ts-ignore
preferCurrentTab: true
video: {
displaySurface: 'browser',
frameRate: 30
},
audio: {
autoGainControl: false,
channelCount: 2,
echoCancellation: false,
noiseSuppression: false,
// @ts-ignore
restrictOwnAudio: false,
// @ts-ignore
suppressLocalAudioPlayback: false,
},
// @ts-ignore
preferCurrentTab: true,
surfaceSwitching: 'exclude'
});
document.title = currentTitle;
const isBrowser = gdmStream.getVideoTracks()[0].getSettings().displaySurface === 'browser';
const gdmVideoTrack = gdmStream.getVideoTracks()[0];
const isBrowser = gdmVideoTrack.getSettings().displaySurface === 'browser';
const matchesHandle = (supportsCaptureHandle // @ts-ignore
&& gdmVideoTrack.getCaptureHandle()?.handle === `JitsiMeet-${tabId}`);
if (!isBrowser || (supportsCaptureHandle // @ts-ignore
&& gdmStream.getVideoTracks()[0].getCaptureHandle()?.handle !== `JitsiMeet-${tabId}`)) {
if (!isBrowser || !matchesHandle) {
gdmStream.getTracks().forEach((track: MediaStreamTrack) => track.stop());
throw new Error('WrongSurfaceSelected');
}
this.initializeAudioMixer();
const allTracks = getTrackState(getState());
const gdmAudioTrack = gdmStream.getAudioTracks()[0];
allTracks.forEach((track: any) => {
if (track.mediaType === MEDIA_TYPE.AUDIO) {
const audioTrack = track?.jitsiTrack?.track;
if (!gdmAudioTrack) {
throw new Error('NoAudioTrackFound');
}
this.addAudioTrackToLocalRecording(gdmAudioTrack);
const localAudioTrack = getLocalTrack(tracks, MEDIA_TYPE.AUDIO)?.jitsiTrack?.track;
if (localAudioTrack) {
this.addAudioTrackToLocalRecording(localAudioTrack);
}
this.addAudioTrackToLocalRecording(audioTrack);
}
});
this.stream = new MediaStream([
...this.audioDestination?.stream.getAudioTracks() || [],
gdmStream.getVideoTracks()[0]
gdmVideoTrack
]);
}
this.recorder = new MediaRecorder(this.stream, {
// @ts-ignore
audioBitrateMode: 'constant',
mimeType: this.mediaType,
videoBitsPerSecond: VIDEO_BIT_RATE
});
this.recorder.addEventListener('dataavailable', async e => {
if (this.recorder && e.data && e.data.size > 0) {
await this.writableStream?.write(e.data);
if (e.data && e.data.size > 0) {
let data = e.data;
if (!this.firstChunk) {
this.firstChunk = data = await fixDuration(data, 864000000); // Reserve 24h.
}
await this.writableStream?.write(data);
}
});
this.recorder.addEventListener('start', () => {
this.startTime = Date.now();
});
this.recorder.addEventListener('stop', async () => {
const duration = Date.now() - this.startTime!;
this.stream?.getTracks().forEach((track: MediaStreamTrack) => track.stop());
gdmStream?.getTracks().forEach((track: MediaStreamTrack) => track.stop());
// The stop event is emitted when the recorder is done, and _after_ the last buffered
// data has been handed over to the dataavailable event.
this.recorder = undefined;
this.audioContext = undefined;
this.audioDestination = undefined;
this.startTime = undefined;
if (this.writableStream) {
try {
await this.writableStream.seek(0);
await this.writableStream.write(await fixDuration(this.firstChunk!, duration));
await this.writableStream.close();
} catch (_) {
// Ignored, not much we can do here.
} finally {
this.firstChunk = undefined;
this.fileHandle = undefined;
this.writableStream = undefined;
}
}
});
if (!onlySelf) {
this.recorder.addEventListener('stop', () => {
this.stream?.getTracks().forEach((track: MediaStreamTrack) => track.stop());
gdmStream?.getTracks().forEach((track: MediaStreamTrack) => track.stop());
});
gdmStream?.addEventListener('inactive', () => {
dispatch(stopLocalVideoRecording());
});
@@ -280,7 +304,7 @@ const LocalRecordingManager: ILocalRecordingManager = {
});
}
this.recorder.start(5000);
this.recorder.start(1000);
},
/**
@@ -294,6 +318,8 @@ const LocalRecordingManager: ILocalRecordingManager = {
&& !browser.isReactNative()
&& !isMobileBrowser()
// @ts-expect-error
&& Boolean(navigator.mediaDevices.setCaptureHandleConfig)
// @ts-expect-error
&& typeof window.showSaveFilePicker !== 'undefined'
&& MediaRecorder.isTypeSupported(PREFERRED_MEDIA_TYPE);
@@ -310,4 +336,39 @@ const LocalRecordingManager: ILocalRecordingManager = {
};
/**
* Fixes the duration in the WebM container metadata.
* Note: cues are omitted.
*
* @param {Blob} data - The first Blob of WebM data.
* @param {number} duration - Actual duration of the video in milliseconds.
* @returns {Promise<Blob>}
*/
async function fixDuration(data: Blob, duration: number): Promise<Blob> {
const decoder = new ebml.Decoder();
const reader = new ebml.Reader();
reader.logging = false;
reader.drop_default_duration = false;
const dataBuf = await data.arrayBuffer();
const elms = decoder.decode(dataBuf);
for (const elm of elms) {
reader.read(elm);
}
reader.stop();
const newMetadataBuf = ebml.tools.makeMetadataSeekable(
reader.metadatas,
duration,
[] // No cues
);
const body = new Uint8Array(dataBuf).subarray(reader.metadataSize);
// @ts-ignore
return new Blob([ newMetadataBuf, body ], { type: data.type });
}
export default LocalRecordingManager;

View File

@@ -439,9 +439,12 @@ export function isLiveStreamingButtonVisible({
* @returns {boolean}
*/
export function shouldRequireRecordingConsent(recorderSession: any, state: IReduxState) {
const { requireRecordingConsent } = state['features/dynamic-branding'] || {};
const { requireConsent } = state['features/base/config'].recordings || {};
const { requireRecordingConsent, skipRecordingConsentInMeeting }
= state['features/dynamic-branding'] || {};
const { conference } = state['features/base/conference'] || {};
const { requireConsent, skipConsentInMeeting } = state['features/base/config'].recordings || {};
const { iAmRecorder } = state['features/base/config'];
const { consentRequested } = state['features/recording'];
if (iAmRecorder) {
return false;
@@ -455,10 +458,22 @@ export function shouldRequireRecordingConsent(recorderSession: any, state: IRedu
return false;
}
if (!recorderSession.getInitiator()
|| recorderSession.getStatus() === JitsiRecordingConstants.status.OFF) {
if (consentRequested.has(recorderSession.getID())) {
return false;
}
return recorderSession.getInitiator() !== getLocalParticipant(state)?.id;
// If we join a meeting that has an ongoing recording `conference` will be undefined since
// we get the recording state through the initial presence which happens in between the
// WILL_JOIN and JOINED events.
if (conference && (skipConsentInMeeting || skipRecordingConsentInMeeting)) {
return false;
}
const initiator = recorderSession.getInitiator();
if (!initiator || recorderSession.getStatus() === JitsiRecordingConstants.status.OFF) {
return false;
}
return initiator !== getLocalParticipant(state)?.id;
}

View File

@@ -36,6 +36,7 @@ import { isRecorderTranscriptionsRunning } from '../transcribing/functions';
import { RECORDING_SESSION_UPDATED, START_LOCAL_RECORDING, STOP_LOCAL_RECORDING } from './actionTypes';
import {
clearRecordingSessions,
markConsentRequested,
hidePendingRecordingNotification,
showPendingRecordingNotification,
showRecordingError,
@@ -307,7 +308,8 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
case TRACK_ADDED: {
const { track } = action;
if (LocalRecordingManager.isRecordingLocally() && track.mediaType === MEDIA_TYPE.AUDIO) {
if (LocalRecordingManager.isRecordingLocally()
&& track.mediaType === MEDIA_TYPE.AUDIO && track.local) {
const audioTrack = track.jitsiTrack.track;
LocalRecordingManager.addAudioTrackToLocalRecording(audioTrack);
@@ -420,6 +422,7 @@ function _showExplicitConsentDialog(recorderSession: any, dispatch: IStore['disp
}
batch(() => {
dispatch(markConsentRequested(recorderSession.getID()));
dispatch(setAudioUnmutePermissions(true, true));
dispatch(setVideoUnmutePermissions(true, true));
dispatch(setAudioMuted(true));

View File

@@ -2,6 +2,7 @@ import ReducerRegistry from '../base/redux/ReducerRegistry';
import {
CLEAR_RECORDING_SESSIONS,
MARK_CONSENT_REQUESTED,
RECORDING_SESSION_UPDATED,
SET_MEETING_HIGHLIGHT_BUTTON_STATE,
SET_PENDING_RECORDING_NOTIFICATION_UID,
@@ -11,6 +12,7 @@ import {
} from './actionTypes';
const DEFAULT_STATE = {
consentRequested: new Set(),
disableHighlightMeetingMoment: false,
pendingNotificationUids: {},
selectedRecordingService: '',
@@ -29,6 +31,7 @@ export interface ISessionData {
}
export interface IRecordingState {
consentRequested: Set<any>;
disableHighlightMeetingMoment: boolean;
pendingNotificationUids: {
[key: string]: string | undefined;
@@ -57,6 +60,15 @@ ReducerRegistry.register<IRecordingState>(STORE_NAME,
sessionDatas: []
};
case MARK_CONSENT_REQUESTED:
return {
...state,
consentRequested: new Set([
...state.consentRequested,
action.sessionId
])
};
case RECORDING_SESSION_UPDATED:
return {
...state,

View File

@@ -52,6 +52,8 @@ describe('Codec selection', () => {
// Check if media is playing on p3.
expect(await p3.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
const majorVersion = parseInt(p1.driver.capabilities.browserVersion || '0', 10);
// Check if p1 is encoding in VP9, p2 in VP8 and p3 in AV1 as per their codec preferences.
// Except on Firefox because it doesn't support VP9 encode.
if (p1.driver.isFirefox) {
@@ -62,7 +64,12 @@ describe('Codec selection', () => {
expect(await p2.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
expect(await p3.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingAv1())).toBe(true);
// If there is a Firefox ep in the call, all other eps will switch to VP9.
if (p1.driver.isFirefox && majorVersion < 136) {
expect(await p3.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
} else {
expect(await p3.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingAv1())).toBe(true);
}
});
it('codec switch over', async () => {