diff --git a/fluxer_app/src/features/i18n/locales/ar/messages.po b/fluxer_app/src/features/i18n/locales/ar/messages.po index 6c3a74015..4bf05887e 100644 --- a/fluxer_app/src/features/i18n/locales/ar/messages.po +++ b/fluxer_app/src/features/i18n/locales/ar/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "عناصر تحكم مشفر مشاركة الشاشة" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "انتهت مشاركة الشاشة" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "يجب أن يستخدم redirect_uri المقدم بروتوكول HTTP أو HTTPS." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "توقفت عملية مشاركة الشاشة عن إرسال الفيديو، لذلك تم إيقاف مشاركة شاشتك." diff --git a/fluxer_app/src/features/i18n/locales/bg/messages.po b/fluxer_app/src/features/i18n/locales/bg/messages.po index e2bb112e8..862414d30 100644 --- a/fluxer_app/src/features/i18n/locales/bg/messages.po +++ b/fluxer_app/src/features/i18n/locales/bg/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "Контроли за кодиране на споделяне на екрана" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "Споделянето на екрана приключи" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "Предоставеният redirect_uri трябва да използва HTTP или HTTPS." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "Източникът на споделяне на екрана спря да изпраща видео, поради което споделянето на екрана беше прекратено." diff --git a/fluxer_app/src/features/i18n/locales/cs/messages.po b/fluxer_app/src/features/i18n/locales/cs/messages.po index 423dbcc0d..c493cdb2a 100644 --- a/fluxer_app/src/features/i18n/locales/cs/messages.po +++ b/fluxer_app/src/features/i18n/locales/cs/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "Ovládací prvky kodeku pro sdílení obrazovky" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "Sdílení obrazovky ukončeno" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "Zadané redirect_uri musí používat HTTP nebo HTTPS." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "Zdroj sdílení obrazovky přestal odesílat video, takže bylo ukončeno sdílení obrazovky." diff --git a/fluxer_app/src/features/i18n/locales/da/messages.po b/fluxer_app/src/features/i18n/locales/da/messages.po index c0aa97898..8e6760544 100644 --- a/fluxer_app/src/features/i18n/locales/da/messages.po +++ b/fluxer_app/src/features/i18n/locales/da/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "Skærmdelingsindstillinger" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "Skærmdeling afsluttet" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "Den angivne redirect_uri skal bruge HTTP eller HTTPS." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "Skærmdelingskilden er stoppet med at sende video, så din skærmdeling blev stoppet." diff --git a/fluxer_app/src/features/i18n/locales/de/messages.po b/fluxer_app/src/features/i18n/locales/de/messages.po index ddc113be8..98586d57c 100644 --- a/fluxer_app/src/features/i18n/locales/de/messages.po +++ b/fluxer_app/src/features/i18n/locales/de/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "Bildschirmfreigabe-Encoder-Einstellungen" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "Bildschirmfreigabe beendet" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "Die angegebene redirect_uri muss HTTP oder HTTPS verwenden." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "Die Bildschirmfreigabe hat das Senden von Video gestoppt, daher wurde Ihre Bildschirmfreigabe beendet." diff --git a/fluxer_app/src/features/i18n/locales/el/messages.po b/fluxer_app/src/features/i18n/locales/el/messages.po index 0d64a3201..d87451385 100644 --- a/fluxer_app/src/features/i18n/locales/el/messages.po +++ b/fluxer_app/src/features/i18n/locales/el/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "Έλεγχος κωδικοποιητή κοινής χρήσης οθόνης" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "Η κοινή χρήση οθόνης έληξε" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "Το redirect_uri που δόθηκε πρέπει να χρησιμοποιεί HTTP ή HTTPS." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "Η πηγή κοινής χρήσης οθόνης σταμάτησε να στέλνει βίντεο, επομένως η κοινή χρήση της οθόνης σας διακόπηκε." diff --git a/fluxer_app/src/features/i18n/locales/en-GB/messages.po b/fluxer_app/src/features/i18n/locales/en-GB/messages.po index 7c24ab440..11bffaf95 100644 --- a/fluxer_app/src/features/i18n/locales/en-GB/messages.po +++ b/fluxer_app/src/features/i18n/locales/en-GB/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "Screen share encoder controls" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "Screen share ended" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "The redirect_uri provided must use HTTP or HTTPS." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "The screen share source stopped sending video, so your screen share was stopped." diff --git a/fluxer_app/src/features/i18n/locales/en-US/messages.po b/fluxer_app/src/features/i18n/locales/en-US/messages.po index d73946b1c..2b63de8a2 100644 --- a/fluxer_app/src/features/i18n/locales/en-US/messages.po +++ b/fluxer_app/src/features/i18n/locales/en-US/messages.po @@ -27052,7 +27052,7 @@ msgid "Screen share encoder controls" msgstr "Screen share encoder controls" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "Screen share ended" @@ -31639,7 +31639,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "The provided redirect_uri must use HTTP or HTTPS." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "The screen share source stopped sending video, so your screen share was stopped." diff --git a/fluxer_app/src/features/i18n/locales/es-419/messages.po b/fluxer_app/src/features/i18n/locales/es-419/messages.po index b9a88b79e..1fc299132 100644 --- a/fluxer_app/src/features/i18n/locales/es-419/messages.po +++ b/fluxer_app/src/features/i18n/locales/es-419/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "Controles del codificador para compartir pantalla" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "Se detuvo el uso compartido de pantalla" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "La redirect_uri proporcionada debe usar HTTP o HTTPS." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "La fuente de uso compartido de pantalla dejó de enviar video, por lo que se detuvo el uso compartido de pantalla." diff --git a/fluxer_app/src/features/i18n/locales/es-ES/messages.po b/fluxer_app/src/features/i18n/locales/es-ES/messages.po index a4b806d32..639d58ae5 100644 --- a/fluxer_app/src/features/i18n/locales/es-ES/messages.po +++ b/fluxer_app/src/features/i18n/locales/es-ES/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "Controles del codificador para compartir pantalla" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "Finalizó el uso compartido de pantalla" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "La redirect_uri proporcionada debe usar HTTP o HTTPS." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "La fuente de uso compartido de pantalla dejó de enviar vídeo, por lo que se detuvo el uso compartido de pantalla." diff --git a/fluxer_app/src/features/i18n/locales/fi/messages.po b/fluxer_app/src/features/i18n/locales/fi/messages.po index 5c2194955..8eb6a2ff8 100644 --- a/fluxer_app/src/features/i18n/locales/fi/messages.po +++ b/fluxer_app/src/features/i18n/locales/fi/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "Näytönjakamisen kooderin asetukset" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "Näytönjako päättyi" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "redirect_uri-osoitteen on käytettävä HTTP- tai HTTPS-protokollaa." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "Näytönjakolähde lopetti videon lähettämisen, joten näytönjako on lopetettu." diff --git a/fluxer_app/src/features/i18n/locales/fr/messages.po b/fluxer_app/src/features/i18n/locales/fr/messages.po index 154859b28..07869675b 100644 --- a/fluxer_app/src/features/i18n/locales/fr/messages.po +++ b/fluxer_app/src/features/i18n/locales/fr/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "Commandes de l’encodeur de partage d’écran" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "Partage d’écran terminé" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "L'URI de redirection fournie doit utiliser HTTP ou HTTPS." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "La source de partage d’écran a cessé d’envoyer des vidéos, votre partage d’écran a donc été arrêté." diff --git a/fluxer_app/src/features/i18n/locales/he/messages.po b/fluxer_app/src/features/i18n/locales/he/messages.po index 51529ce0e..967a2babc 100644 --- a/fluxer_app/src/features/i18n/locales/he/messages.po +++ b/fluxer_app/src/features/i18n/locales/he/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "בקרות מקודד שיתוף מסך" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "שיתוף מסך הסתיים" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "ה-redirect_uri שסופק חייב להשתמש ב-HTTP או ב-HTTPS." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "מקור שיתוף המסך הפסיק לשלוח וידאו, ולכן שיתוף המסך שלך הופסק." diff --git a/fluxer_app/src/features/i18n/locales/hi/messages.po b/fluxer_app/src/features/i18n/locales/hi/messages.po index 30f200586..29d52bc1e 100644 --- a/fluxer_app/src/features/i18n/locales/hi/messages.po +++ b/fluxer_app/src/features/i18n/locales/hi/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "स्क्रीन शेयर एनकोडर नियंत्रण" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "स्क्रीन शेयर समाप्त हो गया" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "दिया गया redirect_uri HTTP या HTTPS का उपयोग करना चाहिए।" #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "स्क्रीन शेयर स्रोत ने वीडियो भेजना बंद कर दिया है, इसलिए आपका स्क्रीन शेयर रोक दिया गया।" diff --git a/fluxer_app/src/features/i18n/locales/hr/messages.po b/fluxer_app/src/features/i18n/locales/hr/messages.po index 6a6cea8fb..b1e7fdb1d 100644 --- a/fluxer_app/src/features/i18n/locales/hr/messages.po +++ b/fluxer_app/src/features/i18n/locales/hr/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "Kontrole za enkoder dijeljenja zaslona" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "Dijeljenje zaslona završeno" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "Navedeni redirect_uri mora koristiti HTTP ili HTTPS." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "Izvor dijeljenja zaslona prestao je slati video, pa je dijeljenje zaslona zaustavljeno." diff --git a/fluxer_app/src/features/i18n/locales/hu/messages.po b/fluxer_app/src/features/i18n/locales/hu/messages.po index a0c752835..d4c041d14 100644 --- a/fluxer_app/src/features/i18n/locales/hu/messages.po +++ b/fluxer_app/src/features/i18n/locales/hu/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "Képernyőmegosztás-kódoló beállításai" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "A képernyőmegosztás befejeződött" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "A megadott redirect_uri-nak HTTP vagy HTTPS protokollt kell használnia." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "A képernyőmegosztás forrása leállította a videó küldését, ezért a képernyőmegosztása is befejeződött." diff --git a/fluxer_app/src/features/i18n/locales/id/messages.po b/fluxer_app/src/features/i18n/locales/id/messages.po index 191294543..e2f11475d 100644 --- a/fluxer_app/src/features/i18n/locales/id/messages.po +++ b/fluxer_app/src/features/i18n/locales/id/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "Kontrol pengode video layar" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "Berbagi layar berakhir" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "redirect_uri yang diberikan harus menggunakan HTTP atau HTTPS." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "Sumber berbagi layar berhenti mengirim video, sehingga berbagi layar Anda dihentikan." diff --git a/fluxer_app/src/features/i18n/locales/it/messages.po b/fluxer_app/src/features/i18n/locales/it/messages.po index 82058f42a..60d41ce0f 100644 --- a/fluxer_app/src/features/i18n/locales/it/messages.po +++ b/fluxer_app/src/features/i18n/locales/it/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "Controlli dell'encoder per la condivisione dello schermo" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "Screen share terminato" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "L'URI di reindirizzamento fornito deve usare HTTP o HTTPS." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "La sorgente della condivisione dello schermo ha smesso di inviare video, quindi la condivisione dello schermo è stata interrotta." diff --git a/fluxer_app/src/features/i18n/locales/ja/messages.po b/fluxer_app/src/features/i18n/locales/ja/messages.po index 12e53c54a..43cacafb6 100644 --- a/fluxer_app/src/features/i18n/locales/ja/messages.po +++ b/fluxer_app/src/features/i18n/locales/ja/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "画面共有エンコーダー設定" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "画面共有が終了しました" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "指定されたredirect_uriはHTTPまたはHTTPSを使用する必要があります。" #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "画面共有のソースがビデオの送信を停止したため、画面共有は停止されました。" diff --git a/fluxer_app/src/features/i18n/locales/ko/messages.po b/fluxer_app/src/features/i18n/locales/ko/messages.po index 077fffb43..436fdf706 100644 --- a/fluxer_app/src/features/i18n/locales/ko/messages.po +++ b/fluxer_app/src/features/i18n/locales/ko/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "화면 공유 인코더 컨트롤" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "화면 공유가 종료되었습니다" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "제공된 redirect_uri는 HTTP 또는 HTTPS를 사용해야 합니다." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "화면 공유 소스가 더 이상 비디오를 보내지 않아 화면 공유가 중단되었습니다." diff --git a/fluxer_app/src/features/i18n/locales/lt/messages.po b/fluxer_app/src/features/i18n/locales/lt/messages.po index 58ef54d89..298131bed 100644 --- a/fluxer_app/src/features/i18n/locales/lt/messages.po +++ b/fluxer_app/src/features/i18n/locales/lt/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "Ekrano dalijimosi įrašymo nustatymų valdikliai" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "Ekrano dalijimasis baigėsi" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "Pateiktas \"redirect_uri\" turi naudoti HTTP arba HTTPS." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "Šaltinio, kuriuo dalijamasi ekranu, vaizdo perdavimas buvo sustabdytas, todėl ekrano dalijimasis buvo nutrauktas." diff --git a/fluxer_app/src/features/i18n/locales/nl/messages.po b/fluxer_app/src/features/i18n/locales/nl/messages.po index 94eaac08d..dc296bc9b 100644 --- a/fluxer_app/src/features/i18n/locales/nl/messages.po +++ b/fluxer_app/src/features/i18n/locales/nl/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "Instellingen voor schermdeling" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "Schermdeling beëindigd" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "De opgegeven redirect_uri moet HTTP of HTTPS gebruiken." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "De bron van het scherm delen stuurt geen video meer, dus uw schermdeling is gestopt." diff --git a/fluxer_app/src/features/i18n/locales/no/messages.po b/fluxer_app/src/features/i18n/locales/no/messages.po index 70584da9a..beec9d679 100644 --- a/fluxer_app/src/features/i18n/locales/no/messages.po +++ b/fluxer_app/src/features/i18n/locales/no/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "Kontroller for skjermdelingsenkoder" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "Skjermdelingen ble avsluttet" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "Den oppgitte redirect_uri må bruke HTTP eller HTTPS." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "Kilden for skjermdelingen sluttet å sende video, så skjermdelingen ble stoppet." diff --git a/fluxer_app/src/features/i18n/locales/pl/messages.po b/fluxer_app/src/features/i18n/locales/pl/messages.po index 390588de4..d1f844ac8 100644 --- a/fluxer_app/src/features/i18n/locales/pl/messages.po +++ b/fluxer_app/src/features/i18n/locales/pl/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "Sterowanie ustawieniami kodowania udostępniania ekranu" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "Udostępnianie ekranu zakończone" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "Podany adres redirect_uri musi używać protokołu HTTP lub HTTPS." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "Źródło udostępniania ekranu przestało wysyłać wideo, więc udostępnianie ekranu zostało zatrzymane." diff --git a/fluxer_app/src/features/i18n/locales/pt-BR/messages.po b/fluxer_app/src/features/i18n/locales/pt-BR/messages.po index 1e38d570e..60949cee9 100644 --- a/fluxer_app/src/features/i18n/locales/pt-BR/messages.po +++ b/fluxer_app/src/features/i18n/locales/pt-BR/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "Controles do codificador de compartilhamento de tela" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "Compartilhamento de tela encerrado" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "O redirect_uri fornecido precisa usar HTTP ou HTTPS." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "A fonte de compartilhamento de tela parou de enviar vídeo, então o seu compartilhamento de tela foi interrompido." diff --git a/fluxer_app/src/features/i18n/locales/ro/messages.po b/fluxer_app/src/features/i18n/locales/ro/messages.po index fb50572b9..3dfb62832 100644 --- a/fluxer_app/src/features/i18n/locales/ro/messages.po +++ b/fluxer_app/src/features/i18n/locales/ro/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "Controalele codificatorului pentru partajarea ecranului" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "Partajarea ecranului s-a încheiat" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "redirect_uri furnizat trebuie să utilizeze HTTP sau HTTPS." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "Sursa partajării ecranului a încetat să trimită video, astfel încât partajarea ecranului s-a oprit." diff --git a/fluxer_app/src/features/i18n/locales/ru/messages.po b/fluxer_app/src/features/i18n/locales/ru/messages.po index 74d1a3567..8fc511a13 100644 --- a/fluxer_app/src/features/i18n/locales/ru/messages.po +++ b/fluxer_app/src/features/i18n/locales/ru/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "Настройки кодека для демонстрации экрана" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "Общий доступ к экрану завершен" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "Указанный redirect_uri должен использовать HTTP или HTTPS." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "Источник трансляции экрана прекратил передачу видео, поэтому трансляция экрана была остановлена." diff --git a/fluxer_app/src/features/i18n/locales/sv-SE/messages.po b/fluxer_app/src/features/i18n/locales/sv-SE/messages.po index e9d34ddcd..ab3f53c87 100644 --- a/fluxer_app/src/features/i18n/locales/sv-SE/messages.po +++ b/fluxer_app/src/features/i18n/locales/sv-SE/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "Inställningar för skärmdelningskodare" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "Skärmdelningen avslutades" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "Den angivna redirect_uri måste använda HTTP eller HTTPS." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "Skärmdelningskällan slutade skicka video, så din skärmdelning har stoppats." diff --git a/fluxer_app/src/features/i18n/locales/th/messages.po b/fluxer_app/src/features/i18n/locales/th/messages.po index 65e711cd5..b5f284797 100644 --- a/fluxer_app/src/features/i18n/locales/th/messages.po +++ b/fluxer_app/src/features/i18n/locales/th/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "การควบคุมตัวเข้ารหัสสำหรับการแชร์หน้าจอ" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "การแชร์หน้าจอสิ้นสุดลง" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "redirect_uri ที่ระบุต้องใช้ HTTP หรือ HTTPS" #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "แหล่งที่มาของการแชร์หน้าจอหยุดส่งวิดีโอ ดังนั้นการแชร์หน้าจอก็จะหยุดลง" diff --git a/fluxer_app/src/features/i18n/locales/tr/messages.po b/fluxer_app/src/features/i18n/locales/tr/messages.po index ee09a5f7e..afeb2d166 100644 --- a/fluxer_app/src/features/i18n/locales/tr/messages.po +++ b/fluxer_app/src/features/i18n/locales/tr/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "Ekran paylaşımı kodlayıcı ayarları" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "Ekran paylaşımı sona erdi" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "Sağlanan redirect_uri HTTP veya HTTPS kullanmalıdır." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "Ekran paylaşımı kaynağı video göndermeyi durdurdu, bu nedenle ekran paylaşımınız durduruldu." diff --git a/fluxer_app/src/features/i18n/locales/uk/messages.po b/fluxer_app/src/features/i18n/locales/uk/messages.po index ffd18a172..0a49e9cde 100644 --- a/fluxer_app/src/features/i18n/locales/uk/messages.po +++ b/fluxer_app/src/features/i18n/locales/uk/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "Керування налаштуваннями кодека для демонстрації екрана" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "Трансляцію екрана завершено" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "Наданий redirect_uri має використовувати HTTP або HTTPS." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "Джерело спільного доступу до екрана припинило надсилання відео, тому спільний доступ до вашого екрана було зупинено." diff --git a/fluxer_app/src/features/i18n/locales/vi/messages.po b/fluxer_app/src/features/i18n/locales/vi/messages.po index 580e70afc..5ac3531e9 100644 --- a/fluxer_app/src/features/i18n/locales/vi/messages.po +++ b/fluxer_app/src/features/i18n/locales/vi/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "Điều khiển bộ mã hóa chia sẻ màn hình" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "Chia sẻ màn hình đã kết thúc" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "redirect_uri được cung cấp phải dùng HTTP hoặc HTTPS." #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "Nguồn chia sẻ màn hình đã ngừng gửi video, vì vậy quá trình chia sẻ màn hình của bạn đã dừng." diff --git a/fluxer_app/src/features/i18n/locales/zh-CN/messages.po b/fluxer_app/src/features/i18n/locales/zh-CN/messages.po index d179266b3..cd4cb83ba 100644 --- a/fluxer_app/src/features/i18n/locales/zh-CN/messages.po +++ b/fluxer_app/src/features/i18n/locales/zh-CN/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "屏幕共享编码器设置" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "屏幕共享已结束" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "提供的 redirect_uri 必须使用 HTTP 或 HTTPS。" #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "屏幕共享源已停止发送视频,因此您的屏幕共享已停止。" diff --git a/fluxer_app/src/features/i18n/locales/zh-TW/messages.po b/fluxer_app/src/features/i18n/locales/zh-TW/messages.po index 5fb268ba4..5d937a12e 100644 --- a/fluxer_app/src/features/i18n/locales/zh-TW/messages.po +++ b/fluxer_app/src/features/i18n/locales/zh-TW/messages.po @@ -27051,7 +27051,7 @@ msgid "Screen share encoder controls" msgstr "螢幕分享編碼器控制項" #. Title of a modal shown when an active screen share stops unexpectedly. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:95 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:98 msgctxt "screen-share" msgid "Screen share ended" msgstr "螢幕分享已結束" @@ -31638,7 +31638,7 @@ msgid "The provided redirect_uri must use HTTP or HTTPS." msgstr "提供的 redirect_uri 必須使用 HTTP 或 HTTPS。" #. Body of a modal shown when a browser screen share track ends outside the app, for example from the browser sharing controls. -#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:100 +#: src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx:103 msgctxt "screen-share" msgid "The screen share source stopped sending video, so your screen share was stopped." msgstr "螢幕分享來源已停止傳送視訊,因此您的螢幕分享已停止。" diff --git a/fluxer_app/src/features/voice/engine/MediaEngineFacade.ts b/fluxer_app/src/features/voice/engine/MediaEngineFacade.ts index b57780dc5..717ccc1a9 100644 --- a/fluxer_app/src/features/voice/engine/MediaEngineFacade.ts +++ b/fluxer_app/src/features/voice/engine/MediaEngineFacade.ts @@ -141,6 +141,7 @@ import {VoiceEngineV2AppSourceLifecycleBridge} from '@app/features/voice/engine/ import {VoiceEngineV2AppStatsHostAdapter} from '@app/features/voice/engine/v2/VoiceEngineV2AppStatsHostAdapter'; import VoiceEngineV2AppSubscriptionAdapter from '@app/features/voice/engine/v2/VoiceEngineV2AppSubscriptionAdapter'; import voiceEngineV2AppVoiceStateAdapter from '@app/features/voice/engine/v2/VoiceEngineV2AppVoiceStateAdapter'; +import type {DisplayScreenShareCaptureContext} from '@app/features/voice/engine/voice_screen_share_manager/shared'; import type {VoiceStateAckPayload} from '@app/features/voice/events/VoiceStateAck'; import CallMediaPrefs from '@app/features/voice/state/CallMediaPrefs'; import {type ChannelE2EEStatus, computeChannelE2EEStatus} from '@app/features/voice/state/ChannelE2EEStatus'; @@ -2238,11 +2239,13 @@ class MediaEngineFacade extends Store { async replaceActiveDisplayScreenShare( options?: ScreenShareCaptureOptions, publishOptions?: TrackPublishOptions, + captureContext?: DisplayScreenShareCaptureContext, ): Promise { return voiceEngineV2AppScreenShareExecutionAdapter.replaceActiveDisplayScreenShare( this.room, options, publishOptions, + captureContext, ); } diff --git a/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx b/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx index d4db550f7..b824d45a8 100644 --- a/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx +++ b/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter.tsx @@ -46,6 +46,7 @@ import type {VoiceEngineV2AppSourceLifecycleBridge} from '@app/features/voice/en import { captureScreenSharePublicationCleanup, type DeviceScreenShareCaptureOptions, + type DisplayScreenShareCaptureContext, logger, mergeScreenShareCaptureCleanupSnapshots, releaseScreenShareCaptureCleanup, @@ -73,6 +74,8 @@ import { commitNativeAudioBridgeReplacement, disarmNativeAudio, } from '@app/features/voice/utils/NativeAudioCaptureBridge'; +import {ScreenShareRollbackIncompleteError} from '@app/features/voice/utils/ScreenShareRollbackIncompleteError'; +import {handleScreenShareError} from '@app/features/voice/utils/ScreenShareUtils'; import type {NativeAudioStartOptions} from '@app/types/electron.d'; import type {VoiceEngineV2ScreenOptions} from '@fluxer/voice_engine_v2'; import {msg} from '@lingui/core/macro'; @@ -403,6 +406,10 @@ class VoiceEngineV2AppScreenShareExecutionAdapter extends Store { getScreenShareSimulcastCleanupSnapshotInternal(screenShareTrack: LocalVideoTrack): ScreenShareCaptureCleanupSnapshot { const snapshot: ScreenShareCaptureCleanupSnapshot = {mediaTracks: [], senders: []}; + const primarySender = screenShareTrack.sender; + if (primarySender) { + snapshot.senders.push({sender: primarySender, expectedTrack: primarySender.track}); + } const simulcastCodecs = ( screenShareTrack as LocalVideoTrack & { simulcastCodecs?: Map; @@ -411,7 +418,17 @@ class VoiceEngineV2AppScreenShareExecutionAdapter extends Store { for (const simulcastTrackInfo of simulcastCodecs?.values() ?? []) { snapshot.mediaTracks.push(simulcastTrackInfo.mediaStreamTrack); if (simulcastTrackInfo.sender) { - snapshot.senders.push(simulcastTrackInfo.sender); + const senderTarget = { + sender: simulcastTrackInfo.sender, + expectedTrack: simulcastTrackInfo.sender.track, + }; + if ( + !snapshot.senders.some( + (target) => target.sender === senderTarget.sender && target.expectedTrack === senderTarget.expectedTrack, + ) + ) { + snapshot.senders.push(senderTarget); + } } } return snapshot; @@ -558,14 +575,23 @@ class VoiceEngineV2AppScreenShareExecutionAdapter extends Store { snapshot, captureScreenSharePublicationCleanup(...publications), ); + const cleanupErrors: Array = []; const cleanupResult = await unpublishLocalMediaPublications(participant, publications); for (const failure of cleanupResult.failedPublications) { + cleanupErrors.push(failure.error); logger.warn('Failed to unpublish lingering screen share track', { error: failure.error, source: failure.publication.source, }); } - await this.releaseScreenShareCapture(participant, cleanupSnapshot); + try { + await this.releaseScreenShareCapture(participant, cleanupSnapshot); + } catch (error) { + cleanupErrors.push(error); + } + if (cleanupErrors.length > 0) { + throw new ScreenShareRollbackIncompleteError(cleanupErrors); + } } handleLocalScreenShareTrackUnpublished(room: Room, playSound: boolean, publication?: LocalTrackPublication): void { @@ -583,6 +609,7 @@ class VoiceEngineV2AppScreenShareExecutionAdapter extends Store { this.applyScreenShareStateInternal(false, {reason: 'user', sendUpdate: true}); } void this.cleanupLingeringScreenShareTracks(participant, cleanupSnapshot).catch((error) => { + if (error instanceof ScreenShareRollbackIncompleteError) handleScreenShareError(error); logger.warn('Failed to clean up screen-share audio after video unpublish', {error}); }); if (playSound && !this.isScreenSharePending) { @@ -746,8 +773,9 @@ class VoiceEngineV2AppScreenShareExecutionAdapter extends Store { room: Room | null, options?: ScreenShareCaptureOptions, publishOptions?: TrackPublishOptions, + captureContext?: DisplayScreenShareCaptureContext, ): Promise { - return this.liveKitFlows.replaceActiveDisplayShare(room, options, publishOptions); + return this.liveKitFlows.replaceActiveDisplayShare(room, options, publishOptions, captureContext); } async replaceActiveDeviceScreenShare( diff --git a/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppScreenShareLiveKitFlows.ts b/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppScreenShareLiveKitFlows.ts index 5c0d2cbd4..2e5d74c2d 100644 --- a/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppScreenShareLiveKitFlows.ts +++ b/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppScreenShareLiveKitFlows.ts @@ -6,7 +6,10 @@ import AdaptiveScreenShareEngine from '@app/features/voice/engine/AdaptiveScreen import {updateLocalParticipantFromRoom} from '@app/features/voice/engine/VoiceMediaEngineBridge'; import { enforceLocalMediaPublicationCap, + getLocalPublicationMediaStreamTrack, + getLocalScreenShareAudioPublications, getLocalScreenSharePublications, + getLocalScreenShareVideoPublications, } from '@app/features/voice/engine/VoiceTrackPublicationUtils'; import {VoiceTrackSource} from '@app/features/voice/engine/VoiceTrackSource'; import type { @@ -34,26 +37,34 @@ import { import { type CapturedScreenShareTracks, type DeviceScreenShareCaptureOptions, + type DisplayScreenShareCaptureContext, getReplacementScreenShareSettingsOptions, logger, type ScreenShareCaptureCleanupSnapshot, type SimulcastTrackInfoLike, stopMediaTrack, } from '@app/features/voice/engine/voice_screen_share_manager/shared'; +import ActiveScreenShareSource, { + type PublishedScreenShareSource, +} from '@app/features/voice/state/ActiveScreenShareSource'; import type LocalVoiceState from '@app/features/voice/state/LocalVoiceState'; import SoftwareEncoderWarning from '@app/features/voice/state/SoftwareEncoderWarning'; +import VoiceSettings from '@app/features/voice/state/VoiceSettings'; import { prepareHighFidelityScreenShareAudioTrack, SCREEN_SHARE_AUDIO_PUBLISH_OPTIONS, } from '@app/features/voice/utils/AudioPublishOptions'; import type {ScreenShareContentSource} from '@app/features/voice/utils/CodecCapabilityDetector'; import {commitNativeAudioBridgeReplacement} from '@app/features/voice/utils/NativeAudioCaptureBridge'; +import {ScreenShareAudioCaptureError} from '@app/features/voice/utils/ScreenShareAudioCaptureError'; +import {ScreenShareRollbackIncompleteError} from '@app/features/voice/utils/ScreenShareRollbackIncompleteError'; import {applyCameraMirrorProcessor} from '@app/features/voice/utils/VideoBackgroundProcessor'; import { createLocalAudioTrack, createLocalVideoTrack, - type LocalAudioTrack, + LocalAudioTrack, type LocalParticipant, + type LocalTrackPublication, type LocalVideoTrack, type Room, type ScreenShareCaptureOptions, @@ -76,6 +87,62 @@ function isUserCancelledOrPermissionDeniedError(error: unknown): boolean { return false; } +const COMMITTED_PUBLICATION_INVARIANT_ATTEMPTS = 2; + +interface ScreenShareReplacementSnapshot { + videoTrack: MediaStreamTrack; + videoHadProcessor: boolean; + audioPublication?: LocalTrackPublication; + audioTrack?: LocalAudioTrack; + audioMediaStreamTrack?: MediaStreamTrack; + audioMuted: boolean; + contentSource: ScreenShareContentSource; + publishedSource: PublishedScreenShareSource | null; + sourceId: string | null; + isOwnWindow: boolean; + publishOptions: TrackPublishOptions; +} + +type ScreenShareAudioReplacementStage = + | {kind: 'none'} + | { + kind: 'candidate'; + publication: LocalTrackPublication; + track: LocalAudioTrack; + mediaStreamTrack: MediaStreamTrack; + }; + +interface ScreenShareSimulcastReplacementStageEntry { + info: SimulcastTrackInfoLike; + previousTrack: MediaStreamTrack; + nextTrack: MediaStreamTrack; + sender?: RTCRtpSender; +} + +type ScreenShareSimulcastReplacementStage = Array; + +function isCurrentScreenShareAudioReplacementStage( + participant: LocalParticipant, + stage: Extract, +): boolean { + const publication = Array.from(participant.audioTrackPublications.values()).find( + (candidate) => candidate === stage.publication, + ); + const localTrack = publication?.audioTrack ?? publication?.track; + const mediaStreamTrack = localTrack?.mediaStream?.getAudioTracks()[0] ?? localTrack?.mediaStreamTrack; + return ( + publication === stage.publication && + localTrack === stage.track && + mediaStreamTrack === stage.mediaStreamTrack && + stage.mediaStreamTrack.readyState === 'live' + ); +} + +interface ScreenShareReconciliationStep { + name: string; + run: () => void; +} + export class VoiceEngineV2AppScreenShareLiveKitFlows { private readonly adapter: VoiceEngineV2AppScreenShareExecutionAdapter; @@ -506,7 +573,7 @@ export class VoiceEngineV2AppScreenShareLiveKitFlows { participant: LocalParticipant, failureContext: {kind: 'display'} | {kind: 'device'; options?: DeviceScreenShareCaptureOptions}, error: unknown, - ): void { + ): boolean { assert.ok(participant); const cancelled = isUserCancelledOrPermissionDeniedError(error); if (cancelled) { @@ -526,12 +593,14 @@ export class VoiceEngineV2AppScreenShareLiveKitFlows { this.adapter.transitionScreenShareLifecycleInternal( buildScreenShareFailureTransition({cancelled, active, sourceType}), ); + return cancelled; } async replaceActiveDisplayShare( room: Room | null, options?: ScreenShareCaptureOptions, publishOptions?: TrackPublishOptions, + captureContext?: DisplayScreenShareCaptureContext, ): Promise { const platformVerdict = guardScreenShareEntry({ platformUnsupportedWarning: SCREEN_SHARE_SOURCE_SWITCH_UNSUPPORTED_PLATFORM_WARNING, @@ -560,13 +629,15 @@ export class VoiceEngineV2AppScreenShareLiveKitFlows { codecRepublishInFlight: true, }); try { - const tracks = await createDisplayScreenShareTracks(options); + const tracks = await createDisplayScreenShareTracks(options, captureContext); didReplace = await this.replaceActiveTracks(room, participant, tracks, options, publishOptions); this.emitReplaceShareResult(participant, 'display', didReplace); } catch (error) { - this.handleReplaceShareFailure(participant, {kind: 'display'}, error); + const cancelled = this.handleReplaceShareFailure(participant, {kind: 'display'}, error); + if (!cancelled) throw error; + } finally { + await this.adapter.applyPendingScreenShareRequestsInternal(room, participant); } - await this.adapter.applyPendingScreenShareRequestsInternal(room, participant); return didReplace; } @@ -612,49 +683,349 @@ export class VoiceEngineV2AppScreenShareLiveKitFlows { return didReplace; } - private async swapScreenShareVideoTrack( + private captureScreenShareReplacementSnapshot( + participant: LocalParticipant, screenShareTrack: LocalVideoTrack, - tracks: CapturedScreenShareTracks, - nextContentSource: ScreenShareContentSource, - ): Promise { - assert.ok(screenShareTrack); - assert.ok(tracks); - const previousVideoMediaTrack = screenShareTrack.mediaStreamTrack; - try { - await screenShareTrack.replaceTrack(tracks.videoTrack, false); - if (nextContentSource === 'device') { - await applyCameraMirrorProcessor(screenShareTrack); - } - await this.refreshSimulcastTracks(screenShareTrack); - } catch (error) { - stopMediaTrack(tracks.videoTrack); - stopMediaTrack(tracks.audioTrack); - logger.error('Failed to replace active screen share video track', {error}); - return false; + publishOptions: TrackPublishOptions, + ): ScreenShareReplacementSnapshot { + const videoHadProcessor = screenShareTrack.getProcessor() != null; + const sourceVideoTrack = + screenShareTrack.mediaStream?.getVideoTracks()[0] ?? + (videoHadProcessor ? undefined : screenShareTrack.mediaStreamTrack); + if (!sourceVideoTrack || sourceVideoTrack.readyState !== 'live') { + throw new Error('Active screen share has no live source video track to preserve'); } - if (previousVideoMediaTrack && previousVideoMediaTrack !== screenShareTrack.mediaStreamTrack) { - this.adapter.cleanupActiveScreenShareEndListenerInternal(); - stopMediaTrack(previousVideoMediaTrack); + const audioPublications = getLocalScreenShareAudioPublications(participant); + if (audioPublications.length > 1) { + throw new Error('Active screen share has multiple audio publications before source replacement'); } - return true; + const audioPublication = audioPublications[0]; + const localAudioTrack = audioPublication?.audioTrack ?? (audioPublication?.track as LocalAudioTrack | undefined); + const sourceAudioTrack = localAudioTrack?.mediaStream?.getAudioTracks()[0] ?? localAudioTrack?.mediaStreamTrack; + if (audioPublication && (!localAudioTrack || !sourceAudioTrack || sourceAudioTrack.readyState !== 'live')) { + throw new Error('Active screen share audio publication has no live source track to preserve'); + } + return { + videoTrack: sourceVideoTrack, + videoHadProcessor, + ...(audioPublication ? {audioPublication} : {}), + ...(localAudioTrack && sourceAudioTrack + ? {audioTrack: localAudioTrack, audioMediaStreamTrack: sourceAudioTrack} + : {}), + audioMuted: audioPublication?.isMuted ?? false, + contentSource: this.adapter.getActiveScreenShareContentSourceInternal(), + publishedSource: ActiveScreenShareSource.getPublishedSource(), + sourceId: ActiveScreenShareSource.getSourceId(), + isOwnWindow: ActiveScreenShareSource.isOwnWindow(), + publishOptions, + }; } - private async swapScreenShareAudioTrack( + private restorePublishedScreenShareSource(snapshot: ScreenShareReplacementSnapshot): void { + if (snapshot.publishedSource === null) { + ActiveScreenShareSource.clear(); + return; + } + ActiveScreenShareSource.setPublishedSource(snapshot.publishedSource, snapshot.sourceId, { + isOwnWindow: snapshot.isOwnWindow, + }); + } + + private async restoreScreenShareReplacement( + room: Room, + participant: LocalParticipant, + screenShareTrack: LocalVideoTrack, + snapshot: ScreenShareReplacementSnapshot, + audioStage: ScreenShareAudioReplacementStage, + ): Promise { + this.adapter.cleanupActiveScreenShareEndListenerInternal(); + if (snapshot.videoTrack.readyState !== 'live') { + throw new Error('Screen share replacement rollback source ended before it could be restored'); + } + if (audioStage.kind === 'candidate') { + const stagedPublication = Array.from(participant.audioTrackPublications.values()).find( + (publication) => publication === audioStage.publication, + ); + const stagedTrack = stagedPublication?.audioTrack ?? stagedPublication?.track; + if (stagedPublication === audioStage.publication && stagedTrack === audioStage.track) { + await participant.unpublishTrack(audioStage.track, false).catch((error) => { + logger.warn('Failed to unpublish screen share audio candidate during rollback', {error}); + }); + } + stopMediaTrack(audioStage.mediaStreamTrack); + } + const activeVideoTrack = screenShareTrack.mediaStream?.getVideoTracks()[0] ?? screenShareTrack.mediaStreamTrack; + const sourceNeedsReplacement = activeVideoTrack !== snapshot.videoTrack; + if (sourceNeedsReplacement) { + await screenShareTrack.stageTrackReplacement(snapshot.videoTrack); + } + if (snapshot.videoHadProcessor) { + const restoredProcessor = await applyCameraMirrorProcessor(screenShareTrack, true); + if (!restoredProcessor) { + throw new Error('Screen share rollback could not restore the previous video processor'); + } + } else if (screenShareTrack.getProcessor()) { + await screenShareTrack.stopProcessor(false); + } + if (snapshot.audioPublication && snapshot.audioTrack && snapshot.audioMediaStreamTrack) { + const restoredAudioPublication = Array.from(participant.audioTrackPublications.values()).find( + (publication) => publication === snapshot.audioPublication, + ); + const restoredAudioTrack = restoredAudioPublication?.audioTrack ?? restoredAudioPublication?.track; + const restoredAudioMediaStreamTrack = + restoredAudioTrack?.mediaStream?.getAudioTracks()[0] ?? restoredAudioTrack?.mediaStreamTrack; + if ( + !restoredAudioPublication || + restoredAudioTrack !== snapshot.audioTrack || + restoredAudioMediaStreamTrack !== snapshot.audioMediaStreamTrack || + restoredAudioMediaStreamTrack.readyState !== 'live' + ) { + throw new Error('Screen share audio rollback did not restore its publication'); + } + if (snapshot.audioMuted) { + await restoredAudioPublication.mute(); + } else { + await restoredAudioPublication.unmute(); + } + } + this.restorePublishedScreenShareSource(snapshot); + await runScreenShareActivationRitual({ + adapter: this.adapter, + room, + participant, + active: true, + steps: { + acquireStreamingPriority: false, + enforcePublicationCap: true, + applyState: () => applyScreenShareState(this.adapter, true, true, true), + applyStatePosition: 'after-pipeline', + publishPipeline: { + contentSource: snapshot.contentSource, + effectivePublishOptions: snapshot.publishOptions, + }, + deactivateCleanup: null, + updateLocalParticipant: true, + audioSync: {kind: 'participant-after-watch'}, + syncPersistedAudioPreferenceWhenActive: true, + playSound: false, + buildResolveTransition: null, + }, + }); + if (snapshot.videoTrack.readyState !== 'live') { + throw new Error('Screen share replacement rollback source ended while runtime state was restored'); + } + if (sourceNeedsReplacement) { + await screenShareTrack.commitStagedTrackReplacement(snapshot.videoTrack, false); + } + } + + private async failClosedScreenShareReplacement( + participant: LocalParticipant, + snapshot: ScreenShareReplacementSnapshot, + cleanupSnapshot?: ScreenShareCaptureCleanupSnapshot, + ): Promise { + let cleanupError: unknown; + try { + await this.adapter.cleanupLingeringScreenShareTracks(participant, cleanupSnapshot); + } catch (error) { + cleanupError = error; + logger.error('Failed to clean up screen share after replacement rollback failed', {error}); + } + stopMediaTrack(snapshot.videoTrack); + stopMediaTrack(snapshot.audioMediaStreamTrack); + ActiveScreenShareSource.clear(); + applyScreenShareState(this.adapter, false, true, true); + this.adapter.syncLocalStreamWatchStateInternal(false); + this.adapter.syncLocalScreenShareAudioStateInternal(participant, false); + AdaptiveScreenShareEngine.stop(); + if (cleanupError !== undefined) { + throw new ScreenShareRollbackIncompleteError([cleanupError]); + } + } + + private async stageScreenShareAudioReplacement( participant: LocalParticipant, tracks: CapturedScreenShareTracks, - ): Promise { + ): Promise { assert.ok(participant); assert.ok(tracks); - let audioTrackAdopted = false; - try { - audioTrackAdopted = await this.adapter.replaceActiveScreenShareAudioTrackInternal(participant, tracks.audioTrack); - if (audioTrackAdopted && tracks.audioTrack) { - commitNativeAudioBridgeReplacement(); - } - } catch (error) { - if (!audioTrackAdopted) stopMediaTrack(tracks.audioTrack); - logger.warn('Failed to replace active screen share audio track', {error}); + if (!tracks.audioTrack) { + return {kind: 'none'}; } + prepareHighFidelityScreenShareAudioTrack(tracks.audioTrack); + let publication: LocalTrackPublication | null = null; + const candidateTrack = new LocalAudioTrack(tracks.audioTrack); + try { + await candidateTrack.mute(); + publication = await participant.publishTrack(candidateTrack, SCREEN_SHARE_AUDIO_PUBLISH_OPTIONS); + const track = publication.audioTrack ?? (publication.track as LocalAudioTrack | undefined); + const mediaStreamTrack = track?.mediaStream?.getAudioTracks()[0] ?? track?.mediaStreamTrack; + if (!track || mediaStreamTrack !== tracks.audioTrack || mediaStreamTrack.readyState !== 'live') { + throw new Error('Replacement screen share audio publication has no live local track'); + } + if (!publication.isMuted) { + await publication.mute(); + } + const stage: Extract = { + kind: 'candidate', + publication, + track, + mediaStreamTrack, + }; + if (!isCurrentScreenShareAudioReplacementStage(participant, stage)) { + throw new Error('Replacement screen share audio publication changed during staging'); + } + return stage; + } catch (error) { + let cleanupError: unknown; + const track = publication?.audioTrack ?? (publication?.track as LocalAudioTrack | undefined); + const currentPublication = Array.from(participant.audioTrackPublications.values()).find( + (candidate) => candidate === publication, + ); + if ( + publication && + track && + currentPublication === publication && + (publication.audioTrack ?? publication.track) === track + ) { + try { + await participant.unpublishTrack(track, false); + } catch (caughtCleanupError) { + cleanupError = caughtCleanupError; + } + } + stopMediaTrack(tracks.audioTrack); + if (cleanupError !== undefined) { + throw new ScreenShareRollbackIncompleteError([error, cleanupError]); + } + throw error; + } + } + + private async commitScreenShareAudioReplacement( + participant: LocalParticipant, + videoPublication: LocalTrackPublication, + videoTrack: LocalVideoTrack, + videoMediaStreamTrack: MediaStreamTrack, + snapshot: ScreenShareReplacementSnapshot, + audioStage: ScreenShareAudioReplacementStage, + ): Promise { + const cleanupErrors: Array = []; + if (audioStage.kind === 'candidate' && !isCurrentScreenShareAudioReplacementStage(participant, audioStage)) { + throw new Error('Replacement screen share audio source changed before replacement commit'); + } + const previousAudioTrack = snapshot.audioTrack; + if (previousAudioTrack) { + const previousAudioPublicationPresent = Array.from(participant.audioTrackPublications.values()).some( + (publication) => publication === snapshot.audioPublication, + ); + const previousAudioMediaStreamTrack = + previousAudioTrack.mediaStream?.getAudioTracks()[0] ?? previousAudioTrack.mediaStreamTrack; + if ( + !previousAudioPublicationPresent || + previousAudioMediaStreamTrack !== snapshot.audioMediaStreamTrack || + previousAudioMediaStreamTrack.readyState !== 'live' + ) { + throw new Error('Previous screen share audio source changed before replacement commit'); + } + try { + await previousAudioTrack.mute(); + } catch (error) { + cleanupErrors.push(error); + stopMediaTrack(snapshot.audioMediaStreamTrack); + } + try { + const currentPreviousAudioPublication = Array.from(participant.audioTrackPublications.values()).find( + (publication) => publication === snapshot.audioPublication, + ); + const currentPreviousAudioTrack = + currentPreviousAudioPublication?.audioTrack ?? currentPreviousAudioPublication?.track; + const currentPreviousAudioMediaStreamTrack = + currentPreviousAudioTrack?.mediaStream?.getAudioTracks()[0] ?? currentPreviousAudioTrack?.mediaStreamTrack; + if ( + currentPreviousAudioPublication !== snapshot.audioPublication || + currentPreviousAudioTrack !== previousAudioTrack || + currentPreviousAudioMediaStreamTrack !== snapshot.audioMediaStreamTrack + ) { + throw new Error('Previous screen share audio source changed while replacement commit was in progress'); + } + if (audioStage.kind === 'candidate') { + await participant.unpublishTrack(previousAudioTrack, false); + stopMediaTrack(snapshot.audioMediaStreamTrack); + } else { + await this.adapter.replaceActiveScreenShareAudioTrackInternal(participant, undefined); + } + } catch (error) { + cleanupErrors.push(error); + stopMediaTrack(snapshot.audioMediaStreamTrack); + } + } + if (audioStage.kind === 'candidate') { + try { + await audioStage.publication.unmute(); + } catch (error) { + cleanupErrors.push(error); + stopMediaTrack(audioStage.mediaStreamTrack); + } + } + const hasCommittedPublicationInvariant = (): boolean => { + const videoPublications = getLocalScreenShareVideoPublications(participant); + const currentVideoTrack = videoPublication.videoTrack ?? videoPublication.track; + const currentVideoMediaStreamTrack = + currentVideoTrack?.mediaStream?.getVideoTracks()[0] ?? currentVideoTrack?.mediaStreamTrack; + if ( + videoPublications.length !== 1 || + videoPublications[0] !== videoPublication || + currentVideoTrack !== videoTrack || + currentVideoMediaStreamTrack !== videoMediaStreamTrack || + videoMediaStreamTrack.readyState !== 'live' + ) { + return false; + } + const audioPublications = getLocalScreenShareAudioPublications(participant); + if (audioStage.kind === 'none') { + return audioPublications.length === 0; + } + if (audioPublications.length !== 1 || audioPublications[0] !== audioStage.publication) { + return false; + } + const publishedAudioTrack = audioStage.publication.audioTrack ?? audioStage.publication.track; + return ( + publishedAudioTrack === audioStage.track && isCurrentScreenShareAudioReplacementStage(participant, audioStage) + ); + }; + let publicationInvariantSatisfied = false; + for (let attempt = 0; attempt < COMMITTED_PUBLICATION_INVARIANT_ATTEMPTS; attempt++) { + const capResult = await enforceLocalMediaPublicationCap(participant, VoiceTrackSource.ScreenShare, { + preferredPublication: audioStage.kind === 'candidate' ? audioStage.publication : undefined, + stopOnUnpublish: true, + }); + for (const failure of capResult.failedPublications) { + cleanupErrors.push(failure.error); + stopMediaTrack(getLocalPublicationMediaStreamTrack(failure.publication) ?? undefined); + } + publicationInvariantSatisfied = hasCommittedPublicationInvariant(); + if (publicationInvariantSatisfied) break; + } + if (!publicationInvariantSatisfied) { + throw new AggregateError( + [...cleanupErrors, new Error('Committed screen share publications do not match the replacement')], + 'Failed to establish committed screen share publication state', + ); + } + if (cleanupErrors.length > 0) { + logger.warn('Recovered from screen share publication cleanup failures after source replacement', { + errors: cleanupErrors, + }); + } + if (audioStage.kind === 'candidate') { + commitNativeAudioBridgeReplacement(); + } + if (!hasCommittedPublicationInvariant()) { + throw new Error('Committed screen share publication ownership changed during native audio finalization'); + } + this.adapter.syncLocalScreenShareAudioStateInternal(participant, true); + this.adapter.syncPersistedScreenShareAudioPreferenceInternal(participant); } private async finalizeReplaceActiveTracks( @@ -663,15 +1034,14 @@ export class VoiceEngineV2AppScreenShareLiveKitFlows { tracks: CapturedScreenShareTracks, nextContentSource: ScreenShareContentSource, options: ScreenShareCaptureOptions | undefined, - publishOptions: TrackPublishOptions | undefined, + effectivePublishOptions: TrackPublishOptions | undefined, ): Promise { assert.ok(participant); assert.ok(tracks); - await enforceLocalMediaPublicationCap(participant, VoiceTrackSource.ScreenShare); - const effectivePublishOptions = await this.adapter.getEffectivePublishOptionsInternal(true, publishOptions); + const replacementSettings = getReplacementScreenShareSettingsOptions(options, tracks.audioTrack != null); await this.adapter.updateActiveScreenShareSettings( room, - getReplacementScreenShareSettingsOptions(options, tracks.audioTrack != null), + replacementSettings ? {...replacementSettings, audio: undefined} : undefined, effectivePublishOptions, ); await runScreenShareActivationRitual({ @@ -693,7 +1063,48 @@ export class VoiceEngineV2AppScreenShareLiveKitFlows { buildResolveTransition: null, }, }); - logger.info('Replaced active screen share source', {audioIncluded: tracks.audioTrack != null}); + } + + private reconcileCommittedScreenShareReplacement( + room: Room, + participant: LocalParticipant, + nextContentSource: ScreenShareContentSource, + effectivePublishOptions: TrackPublishOptions | undefined, + ): void { + const steps: Array = [ + { + name: 'content hint', + run: () => this.adapter.applyScreenShareContentHintInternal(participant, nextContentSource), + }, + {name: 'keep-alive sink', run: () => this.adapter.ensureScreenShareKeepAliveSinkInternal(participant)}, + {name: 'audio content hint', run: () => this.adapter.applyScreenShareAudioContentHintInternal(participant)}, + {name: 'end monitor', run: () => this.adapter.monitorActiveScreenShareEndInternal(room, participant)}, + { + name: 'encoder verification', + run: () => + this.adapter.startEncoderVerificationInternal(room, participant, effectivePublishOptions?.videoCodec), + }, + {name: 'adaptive engine', run: () => AdaptiveScreenShareEngine.start(room)}, + {name: 'local stream state', run: () => applyScreenShareState(this.adapter, true, true, true)}, + {name: 'participant snapshot', run: () => updateLocalParticipantFromRoom(room)}, + {name: 'watch state', run: () => this.adapter.syncLocalStreamWatchStateInternal(true)}, + {name: 'audio state', run: () => this.adapter.syncLocalScreenShareAudioStateInternal(participant, true)}, + { + name: 'persisted audio preference', + run: () => this.adapter.syncPersistedScreenShareAudioPreferenceInternal(participant), + }, + ]; + for (const step of steps) { + try { + step.run(); + } catch (error) { + logger.error('Failed to reconcile committed screen share runtime state', { + error, + nextContentSource, + step: step.name, + }); + } + } } private async replaceActiveTracks( @@ -712,47 +1123,219 @@ export class VoiceEngineV2AppScreenShareLiveKitFlows { logger.warn('No active screen share video track to replace'); return false; } - const nextContentSource = contentSource ?? this.adapter.getActiveScreenShareContentSourceInternal(); - if (nextContentSource !== 'device' && screenShareTrack.getProcessor()) { - await screenShareTrack.stopProcessor(false); + const nextContentSource = contentSource ?? 'display'; + const previousPublishOptions = { + ...(((screenSharePublication as {options?: TrackPublishOptions}).options ?? {}) as TrackPublishOptions), + }; + let snapshot: ScreenShareReplacementSnapshot | null = null; + let effectivePublishOptions: TrackPublishOptions | undefined; + try { + snapshot = this.captureScreenShareReplacementSnapshot(participant, screenShareTrack, previousPublishOptions); + effectivePublishOptions = await this.adapter.getEffectivePublishOptionsInternal(true, publishOptions); + await enforceLocalMediaPublicationCap(participant, VoiceTrackSource.ScreenShare); + } catch (error) { + stopMediaTrack(tracks.videoTrack); + stopMediaTrack(tracks.audioTrack); + throw error; } - const videoSwapped = await this.swapScreenShareVideoTrack(screenShareTrack, tracks, nextContentSource); - if (!videoSwapped) return false; - await this.swapScreenShareAudioTrack(participant, tracks); - await this.finalizeReplaceActiveTracks(room, participant, tracks, nextContentSource, options, publishOptions); + assert.ok(snapshot, 'screen share replacement snapshot must exist after preflight'); + if (tracks.videoTrack === snapshot.videoTrack) { + stopMediaTrack(tracks.audioTrack); + throw new Error('Screen share replacement returned the active source track as its candidate'); + } + let audioStage: ScreenShareAudioReplacementStage = {kind: 'none'}; + let simulcastStage: ScreenShareSimulcastReplacementStage = []; + let requestedAudioStageInProgress = false; + this.adapter.cleanupActiveScreenShareEndListenerInternal(); + try { + await screenShareTrack.stageTrackReplacement(tracks.videoTrack); + if (nextContentSource === 'device') { + const mirrorCamera = VoiceSettings.getMirrorCamera(); + const processor = await applyCameraMirrorProcessor(screenShareTrack, mirrorCamera); + if (mirrorCamera && !processor) { + throw new Error('Replacement device share could not apply its required mirror processor'); + } + if (!mirrorCamera && screenShareTrack.getProcessor()) { + throw new Error('Replacement device share could not clear its previous video processor'); + } + } else if (screenShareTrack.getProcessor()) { + await screenShareTrack.stopProcessor(false); + } + const activeVideoTrack = screenShareTrack.mediaStream?.getVideoTracks()[0] ?? screenShareTrack.mediaStreamTrack; + if (activeVideoTrack !== tracks.videoTrack) { + throw new Error('Replacement screen share video track did not take ownership of its publication'); + } + if (activeVideoTrack.readyState !== 'live') { + throw new Error('Replacement screen share video track ended before commit'); + } + requestedAudioStageInProgress = tracks.displayCapture?.requireAudio === true; + audioStage = await this.stageScreenShareAudioReplacement(participant, tracks); + requestedAudioStageInProgress = false; + if (audioStage.kind === 'candidate' && !isCurrentScreenShareAudioReplacementStage(participant, audioStage)) { + throw new Error('Replacement screen share audio track ended before commit'); + } + simulcastStage = await this.stageScreenShareSimulcastReplacement(screenShareTrack, tracks.videoTrack); + await this.finalizeReplaceActiveTracks( + room, + participant, + tracks, + nextContentSource, + options, + effectivePublishOptions, + ); + await screenShareTrack.commitStagedTrackReplacement(tracks.videoTrack, false); + } catch (error) { + try { + await this.rollbackScreenShareSimulcastReplacement(simulcastStage); + await this.restoreScreenShareReplacement(room, participant, screenShareTrack, snapshot, audioStage); + } catch (rollbackError) { + try { + await this.failClosedScreenShareReplacement(participant, snapshot); + } catch (cleanupError) { + stopMediaTrack(tracks.videoTrack); + stopMediaTrack(tracks.audioTrack); + throw new ScreenShareRollbackIncompleteError([error, rollbackError, cleanupError]); + } + stopMediaTrack(tracks.videoTrack); + stopMediaTrack(tracks.audioTrack); + throw new AggregateError( + [error, rollbackError], + 'Screen share replacement and rollback both failed; the share was stopped', + ); + } + stopMediaTrack(tracks.videoTrack); + stopMediaTrack(tracks.audioTrack); + if ( + requestedAudioStageInProgress && + !(error instanceof ScreenShareAudioCaptureError) && + !(error instanceof ScreenShareRollbackIncompleteError) + ) { + throw new ScreenShareAudioCaptureError({ + sourceId: tracks.displayCapture?.sourceId, + sourceKind: tracks.displayCapture?.displayShareEnvironment, + reason: 'requested-audio-publication-failed', + detail: error instanceof Error ? `${error.name}: ${error.message}` : 'Unknown audio replacement failure', + }); + } + throw error; + } + const committedCleanupSnapshot = this.adapter.getScreenShareCaptureCleanupSnapshotInternal(participant); + let requestedAudioCommitInProgress = false; + try { + requestedAudioCommitInProgress = tracks.displayCapture?.requireAudio === true; + await this.commitScreenShareAudioReplacement( + participant, + screenSharePublication, + screenShareTrack, + tracks.videoTrack, + snapshot, + audioStage, + ); + requestedAudioCommitInProgress = false; + this.commitScreenShareSimulcastReplacement(simulcastStage); + } catch (error) { + try { + await this.failClosedScreenShareReplacement(participant, snapshot, committedCleanupSnapshot); + } catch (cleanupError) { + for (const entry of simulcastStage) stopMediaTrack(entry.nextTrack); + stopMediaTrack(tracks.videoTrack); + stopMediaTrack(tracks.audioTrack); + throw new ScreenShareRollbackIncompleteError([error, cleanupError]); + } + for (const entry of simulcastStage) stopMediaTrack(entry.nextTrack); + stopMediaTrack(tracks.videoTrack); + stopMediaTrack(tracks.audioTrack); + if ( + requestedAudioCommitInProgress && + !(error instanceof ScreenShareAudioCaptureError) && + !(error instanceof ScreenShareRollbackIncompleteError) + ) { + throw new ScreenShareAudioCaptureError({ + sourceId: tracks.displayCapture?.sourceId, + sourceKind: tracks.displayCapture?.displayShareEnvironment, + reason: 'requested-audio-publication-failed', + detail: error instanceof Error ? `${error.name}: ${error.message}` : 'Unknown audio replacement failure', + }); + } + if (error instanceof ScreenShareRollbackIncompleteError) throw error; + throw new AggregateError([error], 'Committed screen share audio replacement failed; the share was stopped'); + } + stopMediaTrack(snapshot.videoTrack); + this.reconcileCommittedScreenShareReplacement(room, participant, nextContentSource, effectivePublishOptions); + logger.info('Replaced active screen share source', {audioIncluded: tracks.audioTrack != null}); return true; } - private async refreshSimulcastTracks(screenShareTrack: LocalVideoTrack): Promise { + private async stageScreenShareSimulcastReplacement( + screenShareTrack: LocalVideoTrack, + candidateTrack: MediaStreamTrack, + ): Promise { const simulcastCodecs = ( screenShareTrack as LocalVideoTrack & { simulcastCodecs?: Map; } ).simulcastCodecs; if (!simulcastCodecs?.size) { - return; + return []; } + const stage: ScreenShareSimulcastReplacementStage = []; for (const simulcastTrackInfo of simulcastCodecs.values()) { const previousTrack = simulcastTrackInfo.mediaStreamTrack; - let nextTrack: MediaStreamTrack | undefined; + const sender = simulcastTrackInfo.sender; + const nextTrack = candidateTrack.clone(); + const entry = {info: simulcastTrackInfo, previousTrack, nextTrack, sender}; + stage.push(entry); try { - nextTrack = screenShareTrack.mediaStreamTrack.clone(); - await simulcastTrackInfo.sender?.replaceTrack(nextTrack); - simulcastTrackInfo.mediaStreamTrack = nextTrack; - } catch (error) { - stopMediaTrack(nextTrack); - try { - await simulcastTrackInfo.sender?.replaceTrack(null); - } catch (replaceError) { - logger.warn('Failed to stop stale screen share simulcast track after replacement failed', { - error: replaceError, - }); + if (sender && sender.track !== previousTrack) { + throw new Error('Screen share simulcast sender does not own its recorded source track'); } - logger.warn('Failed to replace active screen share simulcast track', {error}); - } finally { - stopMediaTrack(previousTrack); + await sender?.replaceTrack(nextTrack); + } catch (error) { + try { + await this.rollbackScreenShareSimulcastReplacement(stage); + } catch (rollbackError) { + throw new AggregateError( + [error, rollbackError], + 'Screen share simulcast replacement and rollback both failed', + ); + } + throw error; } } + return stage; + } + + private async rollbackScreenShareSimulcastReplacement(stage: ScreenShareSimulcastReplacementStage): Promise { + const rollbackErrors: Array = []; + for (let index = stage.length - 1; index >= 0; index -= 1) { + const entry = stage[index]; + if (!entry) continue; + try { + if (entry.sender?.track === entry.nextTrack) { + await entry.sender.replaceTrack(entry.previousTrack); + } + } catch (error) { + rollbackErrors.push(error); + } + stopMediaTrack(entry.nextTrack); + } + stage.length = 0; + if (rollbackErrors.length > 0) { + throw new AggregateError(rollbackErrors, 'Screen share simulcast rollback was incomplete'); + } + } + + private commitScreenShareSimulcastReplacement(stage: ScreenShareSimulcastReplacementStage): void { + for (const entry of stage) { + if (entry.sender && entry.sender.track !== entry.nextTrack) { + throw new Error('Screen share simulcast sender changed before replacement commit'); + } + } + for (const entry of stage) { + entry.info.mediaStreamTrack = entry.nextTrack; + stopMediaTrack(entry.previousTrack); + } + stage.length = 0; } private finalizeReconnectAlreadyEnabled(room: Room | null, participant: LocalParticipant): boolean { diff --git a/fluxer_app/src/features/voice/engine/voice_screen_share_manager/DisplayMediaCapture.ts b/fluxer_app/src/features/voice/engine/voice_screen_share_manager/DisplayMediaCapture.ts index 171c2218a..b6350d679 100644 --- a/fluxer_app/src/features/voice/engine/voice_screen_share_manager/DisplayMediaCapture.ts +++ b/fluxer_app/src/features/voice/engine/voice_screen_share_manager/DisplayMediaCapture.ts @@ -2,9 +2,11 @@ import { type CapturedScreenShareTracks, + type DisplayScreenShareCaptureContext, stopMediaTrack, stopUnselectedStreamTracks, } from '@app/features/voice/engine/voice_screen_share_manager/shared'; +import {ScreenShareAudioCaptureError} from '@app/features/voice/utils/ScreenShareAudioCaptureError'; import type {ScreenShareCaptureOptions} from 'livekit-client'; type DisplayMediaVideoConstraints = MediaTrackConstraints & { @@ -92,28 +94,42 @@ export function getDisplayMediaOptions(options?: ScreenShareCaptureOptions): Dis export async function createDisplayScreenShareTracks( options?: ScreenShareCaptureOptions, + captureContext?: DisplayScreenShareCaptureContext, ): Promise { if (!navigator.mediaDevices.getDisplayMedia) { throw new Error('getDisplayMedia not supported'); } const stream = await navigator.mediaDevices.getDisplayMedia(getDisplayMediaOptions(options)); - const videoTrack = stream.getVideoTracks()[0]; - if (!videoTrack) { + try { + const videoTrack = stream.getVideoTracks()[0]; + if (!videoTrack) { + throw new Error('No video track found in screen share capture'); + } + if (options?.contentHint) { + videoTrack.contentHint = options.contentHint; + } + await videoTrack.applyConstraints({colorSpace: 'rec709'} as MediaTrackConstraints).catch(() => undefined); + const cursor = resolveCapturedDisplayMediaCursorCapture(videoTrack, options); + if ((videoTrack.getSettings() as DisplayMediaTrackSettings).cursor !== cursor) { + await videoTrack.applyConstraints({cursor} as MediaTrackConstraints).catch(() => undefined); + } + const capturedAudioTrack = stream.getAudioTracks()[0]; + const audioTrack = capturedAudioTrack?.readyState === 'live' ? capturedAudioTrack : undefined; + if (captureContext?.requireAudio && !audioTrack) { + throw new ScreenShareAudioCaptureError({ + sourceId: captureContext.sourceId, + reason: 'required-audio-track-missing', + detail: 'display capture completed without the requested native audio track', + }); + } + stopUnselectedStreamTracks(stream, [videoTrack, audioTrack]); + return { + videoTrack, + audioTrack, + displayCapture: captureContext, + }; + } catch (error) { stream.getTracks().forEach(stopMediaTrack); - throw new Error('No video track found in screen share capture'); + throw error; } - if (options?.contentHint) { - videoTrack.contentHint = options.contentHint; - } - await videoTrack.applyConstraints({colorSpace: 'rec709'} as MediaTrackConstraints).catch(() => undefined); - const cursor = resolveCapturedDisplayMediaCursorCapture(videoTrack, options); - if ((videoTrack.getSettings() as DisplayMediaTrackSettings).cursor !== cursor) { - await videoTrack.applyConstraints({cursor} as MediaTrackConstraints).catch(() => undefined); - } - const audioTrack = stream.getAudioTracks()[0]; - stopUnselectedStreamTracks(stream, [videoTrack, audioTrack]); - return { - videoTrack, - audioTrack, - }; } diff --git a/fluxer_app/src/features/voice/engine/voice_screen_share_manager/shared.ts b/fluxer_app/src/features/voice/engine/voice_screen_share_manager/shared.ts index 5f0c3321d..4a9f469f7 100644 --- a/fluxer_app/src/features/voice/engine/voice_screen_share_manager/shared.ts +++ b/fluxer_app/src/features/voice/engine/voice_screen_share_manager/shared.ts @@ -17,6 +17,7 @@ import { SCREEN_SHARE_DEGRADATION_PREFERENCE, SCREEN_SHARE_FORCED_VIDEO_BITRATE_BPS, } from '@app/features/voice/utils/ScreenShareOptions'; +import {ScreenShareRollbackIncompleteError} from '@app/features/voice/utils/ScreenShareRollbackIncompleteError'; import {classifyVideoEncoderAcceleration} from '@app/features/voice/utils/VideoAccelerationClassification'; import { BackupCodecPolicy, @@ -42,6 +43,13 @@ export interface DeviceScreenShareCaptureOptions { export interface CapturedScreenShareTracks { videoTrack: MediaStreamTrack; audioTrack?: MediaStreamTrack; + displayCapture?: DisplayScreenShareCaptureContext; +} + +export interface DisplayScreenShareCaptureContext { + sourceId: string | null; + displayShareEnvironment: string | null; + requireAudio: boolean; } export interface SimulcastTrackInfoLike { @@ -60,12 +68,18 @@ export interface ScreenSharePublishOptionsResolutionOptions { onCodecReadiness?: (status: ScreenShareCodecReadinessStatus) => void; } +export interface ScreenShareSenderCleanupTarget { + sender: RTCRtpSender; + expectedTrack: MediaStreamTrack | null; +} + export interface ScreenShareCaptureCleanupSnapshot { mediaTracks: Array; - senders: Array; + senders: Array; } interface ScreenShareTrackLike { + mediaStream?: MediaStream; mediaStreamTrack?: MediaStreamTrack; sender?: RTCRtpSender; simulcastCodecs?: Map; @@ -80,15 +94,25 @@ function pushUnique(items: Array, item: T | undefined | null): void { items.push(item); } +function pushUniqueSender(snapshot: ScreenShareCaptureCleanupSnapshot, sender: RTCRtpSender | undefined): void { + if (!sender) return; + const expectedTrack = sender.track; + if (snapshot.senders.some((target) => target.sender === sender && target.expectedTrack === expectedTrack)) return; + snapshot.senders.push({sender, expectedTrack}); +} + function captureScreenShareTrackCleanup( snapshot: ScreenShareCaptureCleanupSnapshot, track: ScreenShareTrackLike | undefined | null, ): void { + for (const mediaStreamTrack of track?.mediaStream?.getTracks() ?? []) { + pushUnique(snapshot.mediaTracks, mediaStreamTrack); + } pushUnique(snapshot.mediaTracks, track?.mediaStreamTrack); - pushUnique(snapshot.senders, track?.sender); + pushUniqueSender(snapshot, track?.sender); for (const simulcastTrackInfo of track?.simulcastCodecs?.values() ?? []) { pushUnique(snapshot.mediaTracks, simulcastTrackInfo.mediaStreamTrack); - pushUnique(snapshot.senders, simulcastTrackInfo.sender); + pushUniqueSender(snapshot, simulcastTrackInfo.sender); } } @@ -112,26 +136,43 @@ export function mergeScreenShareCaptureCleanupSnapshots( for (const mediaTrack of snapshot?.mediaTracks ?? []) { pushUnique(merged.mediaTracks, mediaTrack); } - for (const sender of snapshot?.senders ?? []) { - pushUnique(merged.senders, sender); + for (const senderTarget of snapshot?.senders ?? []) { + if ( + !merged.senders.some( + (target) => target.sender === senderTarget.sender && target.expectedTrack === senderTarget.expectedTrack, + ) + ) { + merged.senders.push(senderTarget); + } } } return merged; } -async function detachScreenShareSender(sender: RTCRtpSender): Promise { - try { - if (sender.transport?.state === 'closed') return; - await sender.replaceTrack(null); - } catch (error) { - logger.warn('Failed to detach screen share sender during cleanup', {error}); - } +async function detachScreenShareSender(target: ScreenShareSenderCleanupTarget): Promise { + if (target.sender.track !== target.expectedTrack) return; + if (target.sender.transport?.state === 'closed') return; + await target.sender.replaceTrack(null); } export async function releaseScreenShareCaptureCleanup(snapshot: ScreenShareCaptureCleanupSnapshot): Promise { - await Promise.all(snapshot.senders.map(detachScreenShareSender)); + const cleanupErrors: Array = []; + const senderResults = await Promise.allSettled(snapshot.senders.map(detachScreenShareSender)); + for (const result of senderResults) { + if (result.status === 'rejected') cleanupErrors.push(result.reason); + } for (const mediaTrack of snapshot.mediaTracks) { - stopMediaTrack(mediaTrack); + try { + mediaTrack.stop(); + } catch (error) { + cleanupErrors.push(error); + } + if (mediaTrack.readyState === 'live') { + cleanupErrors.push(new Error('Screen share capture track remained live after cleanup')); + } + } + if (cleanupErrors.length > 0) { + throw new ScreenShareRollbackIncompleteError(cleanupErrors); } } diff --git a/fluxer_app/src/features/voice/state/ActiveScreenShareSource.test.ts b/fluxer_app/src/features/voice/state/ActiveScreenShareSource.test.ts index f9fce4a9c..db655a264 100644 --- a/fluxer_app/src/features/voice/state/ActiveScreenShareSource.test.ts +++ b/fluxer_app/src/features/voice/state/ActiveScreenShareSource.test.ts @@ -9,16 +9,19 @@ describe('ActiveScreenShareSource', () => { }); it('tracks and clears whether the selected source is a Fluxer-owned window', () => { - ActiveScreenShareSource.setSourceId('window:42:0', {isOwnWindow: true}); + ActiveScreenShareSource.setPublishedSource('app', 'window:42:0', {isOwnWindow: true}); expect(ActiveScreenShareSource.getSourceId()).toBe('window:42:0'); expect(ActiveScreenShareSource.isOwnWindow()).toBe(true); + expect(ActiveScreenShareSource.getShareContext()).toBe('app'); - ActiveScreenShareSource.setSourceId('screen:1:0'); + ActiveScreenShareSource.setPublishedSource('display', 'screen:1:0'); expect(ActiveScreenShareSource.getSourceId()).toBe('screen:1:0'); expect(ActiveScreenShareSource.isOwnWindow()).toBe(false); + expect(ActiveScreenShareSource.getShareContext()).toBe('display'); ActiveScreenShareSource.clear(); expect(ActiveScreenShareSource.getSourceId()).toBeNull(); expect(ActiveScreenShareSource.isOwnWindow()).toBe(false); + expect(ActiveScreenShareSource.getShareContext()).toBeNull(); }); }); diff --git a/fluxer_app/src/features/voice/state/ActiveScreenShareSource.ts b/fluxer_app/src/features/voice/state/ActiveScreenShareSource.ts index 5f9cf8a94..21ea0f72e 100644 --- a/fluxer_app/src/features/voice/state/ActiveScreenShareSource.ts +++ b/fluxer_app/src/features/voice/state/ActiveScreenShareSource.ts @@ -1,20 +1,29 @@ // SPDX-License-Identifier: AGPL-3.0-or-later +import type {StreamSettingsShareContext} from '@app/features/voice/utils/StreamSettingsUpdatePolicy'; import {makeAutoObservable} from 'mobx'; export interface ActiveScreenShareSourceOptions { readonly isOwnWindow?: boolean; } +export type PublishedScreenShareSource = 'web' | 'wayland' | 'device' | 'app' | 'display'; + class ActiveScreenShareSource { sourceId: string | null = null; ownWindow = false; + publishedSource: PublishedScreenShareSource | null = null; constructor() { makeAutoObservable(this, {}, {autoBind: true}); } - setSourceId(sourceId: string | null, options: ActiveScreenShareSourceOptions = {}): void { + setPublishedSource( + publishedSource: PublishedScreenShareSource, + sourceId: string | null, + options: ActiveScreenShareSourceOptions = {}, + ): void { + this.publishedSource = publishedSource; this.sourceId = sourceId; this.ownWindow = sourceId !== null && options.isOwnWindow === true; } @@ -27,9 +36,21 @@ class ActiveScreenShareSource { return this.ownWindow; } + getPublishedSource(): PublishedScreenShareSource | null { + return this.publishedSource; + } + + getShareContext(): StreamSettingsShareContext | null { + if (this.publishedSource === 'app') return 'app'; + if (this.publishedSource === 'device') return 'device'; + if (this.publishedSource === null) return null; + return 'display'; + } + clear(): void { this.sourceId = null; this.ownWindow = false; + this.publishedSource = null; } } diff --git a/fluxer_app/src/features/voice/utils/ScreenShareStartFlow.test.ts b/fluxer_app/src/features/voice/utils/ScreenShareStartFlow.test.ts new file mode 100644 index 000000000..3af4a68fa --- /dev/null +++ b/fluxer_app/src/features/voice/utils/ScreenShareStartFlow.test.ts @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +import {beforeEach, describe, expect, test, vi} from 'vitest'; + +const platform = {current: 'darwin' as NodeJS.Platform | string}; + +const setScreenShareEnabled = vi.fn(async () => {}); +const replaceActiveDisplayScreenShare = vi.fn(async () => true); +const armNativeAudioForNextCapture = vi.fn(async () => true); +const armNativeSystemAudioForNextCapture = vi.fn(async () => true); +const armNativeAudioForLinuxRouting = vi.fn(async () => true); + +vi.mock('@app/features/ui/utils/NativeUtils', () => ({ + getElectronAPI: () => ({platform: platform.current}), + supportsDesktopScreenShareAudioCapture: () => true, +})); + +vi.mock('@app/features/voice/engine/MediaEngineFacade', () => ({ + default: { + room: {localParticipant: {isScreenShareEnabled: true}}, + setScreenShareEnabled, + replaceActiveDisplayScreenShare, + }, +})); + +vi.mock('@app/features/voice/utils/ScreenShareEnvironment', () => ({ + getDisplayShareEnvironment: async () => 'desktop-custom', + usesNativeDisplayShareAudioSelection: (environment: string) => environment !== 'desktop-custom', +})); + +vi.mock('@app/features/voice/utils/NativeAudioCaptureBridge', () => ({ + armNativeAudioForLinuxRouting, + armNativeAudioForNextCapture, + armNativeSystemAudioForNextCapture, + disarmNativeAudio: vi.fn(), + disarmPendingNativeAudio: vi.fn(), + getLastNativeAudioArmFailure: () => null, +})); + +vi.mock('@app/features/voice/utils/LinuxScreenShareAudio', () => ({ + disarmVirtmic: vi.fn(), +})); + +vi.mock('@app/features/voice/state/ActiveScreenShareSource', () => ({ + default: {setPublishedSource: vi.fn(), clear: vi.fn(), getSourceId: () => null}, +})); + +vi.mock('@app/features/voice/state/DesktopSourceIntent', () => ({ + setDesktopSourceIntent: vi.fn(), + clearDesktopSourceIntent: vi.fn(), +})); + +vi.mock('@app/features/voice/state/LocalVoiceState', () => ({ + default: {getSelfStream: () => null}, +})); + +vi.mock('@app/features/voice/utils/ScreenShareUtils', () => ({ + executeScreenShareOperation: async (operation: () => Promise) => { + await operation(); + }, +})); + +vi.mock('@app/features/voice/engine/ScreenShareCodecNegotiation', () => ({ + default: {selectScreenShareCodec: () => 'vp8'}, +})); + +vi.mock('@app/features/voice/utils/CodecCapabilityDetector', () => ({ + resolveScreenShareContentHintForContext: () => 'motion', +})); + +vi.mock('@app/features/app/utils/LimitResolverAdapter', () => ({ + LimitResolver: {resolve: () => 0}, +})); + +vi.mock('@app/features/app/utils/LimitUtils', () => ({ + isLimitToggleEnabled: () => false, +})); + +vi.mock('@app/features/voice/commands/VoiceSettingsCommands', () => ({ + update: vi.fn(), +})); + +vi.mock('@app/features/voice/state/VoiceSettings', () => ({ + default: { + getShareAppAudio: () => true, + getShareDesktopAudio: () => true, + getShareDeviceAudio: () => true, + getScreenshareResolution: () => 'medium', + getStreamingMode: () => 'screenshare', + getVideoFrameRate: () => 30, + getPreferredScreenShareCodec: () => 'auto', + getScreenShareContentHintOverride: () => 'auto', + getScreenShareMaxBitrateBpsOverride: () => null, + getEffectiveScreenShareAudioDeviceId: () => '', + getScreenShareAudioSourceMode: () => 'system', + getScreenShareAudioIncludeSources: () => [], + getScreenShareAudioExcludeSources: () => [], + }, +})); + +const {startConfiguredDisplayScreenShare, switchConfiguredDisplayScreenShare} = await import( + '@app/features/voice/utils/ScreenShareStartFlow' +); +const ActiveScreenShareSource = (await import('@app/features/voice/state/ActiveScreenShareSource')).default; + +beforeEach(() => { + platform.current = 'darwin'; + vi.clearAllMocks(); + setScreenShareEnabled.mockResolvedValue(undefined); + armNativeAudioForNextCapture.mockResolvedValue(true); +}); + +describe('sharing a Fluxer-owned window while app audio is enabled', () => { + test('starts the share video-only instead of failing', async () => { + const started = await startConfiguredDisplayScreenShare('window:99:0', { + isOwnWindow: true, + preferredDisplaySurface: 'window', + }); + + expect(started).toBe(true); + expect(setScreenShareEnabled).toHaveBeenCalledTimes(1); + const call = setScreenShareEnabled.mock.calls[0] as unknown as [ + boolean, + {audio: boolean; systemAudio: string; windowAudio: string}, + unknown, + ]; + expect(call[0]).toBe(true); + expect(call[1].audio).toBe(false); + expect(call[1].systemAudio).toBe('exclude'); + expect(call[1].windowAudio).toBe('exclude'); + expect(armNativeAudioForNextCapture).not.toHaveBeenCalled(); + }); +}); + +describe('switching the display source', () => { + test('preserves the running share state when the switch fails', async () => { + replaceActiveDisplayScreenShare.mockResolvedValue(false); + + const switched = await switchConfiguredDisplayScreenShare('window:7:0', {preferredDisplaySurface: 'window'}); + + expect(switched).toBe(false); + expect(ActiveScreenShareSource.clear).not.toHaveBeenCalled(); + expect(ActiveScreenShareSource.setPublishedSource).not.toHaveBeenCalled(); + }); + + test('records the new published source when the switch succeeds', async () => { + replaceActiveDisplayScreenShare.mockResolvedValue(true); + + const switched = await switchConfiguredDisplayScreenShare('window:7:0', {preferredDisplaySurface: 'window'}); + + expect(switched).toBe(true); + expect(ActiveScreenShareSource.setPublishedSource).toHaveBeenCalledWith('app', 'window:7:0', {isOwnWindow: false}); + expect(ActiveScreenShareSource.clear).not.toHaveBeenCalled(); + }); +}); diff --git a/fluxer_app/src/features/voice/utils/ScreenShareStartFlow.ts b/fluxer_app/src/features/voice/utils/ScreenShareStartFlow.ts index d3b42113d..3519978e2 100644 --- a/fluxer_app/src/features/voice/utils/ScreenShareStartFlow.ts +++ b/fluxer_app/src/features/voice/utils/ScreenShareStartFlow.ts @@ -268,6 +268,7 @@ function getConfiguredScreenShareOptions( sourceId?: string | null, preferredDisplaySurface?: 'window' | 'monitor', videoCodec?: VideoCodec, + includeAudioOverride?: boolean, ) { const currentResolution = VoiceSettings.getScreenshareResolution(); const currentStreamingMode = VoiceSettings.getStreamingMode(); @@ -288,12 +289,9 @@ function getConfiguredScreenShareOptions( VoiceSettings.getVideoFrameRate(), higherQuality, ); - const includeAudio = shouldIncludeAudioForShare( - shareContext, - displayShareEnvironment, - sourceId, - preferredDisplaySurface, - ); + const includeAudio = + includeAudioOverride ?? + shouldIncludeAudioForShare(shareContext, displayShareEnvironment, sourceId, preferredDisplaySurface); if (!includeAudio && shareContext !== 'device' && supportsDesktopScreenShareAudioCapture()) { logger.info('Screen share audio not requested for this surface', { sourceId, @@ -328,6 +326,7 @@ export interface ConfiguredDisplayScreenShareOptions { }; preferredDisplaySurface?: 'window' | 'monitor'; isOwnWindow?: boolean; + includeAudio?: boolean; } async function runConfiguredDisplayScreenShare( @@ -348,6 +347,8 @@ async function runConfiguredDisplayScreenShare( options?.sourceDimensions, sourceId, options?.preferredDisplaySurface, + undefined, + options?.includeAudio, ); if (electronApi) { const restartWaylandPortalForSwitch = useWaylandPortal && mode === 'switch'; @@ -373,14 +374,7 @@ async function runConfiguredDisplayScreenShare( sourceId, platform: electronApi.platform, }); - degradeAudioToVideoOnly( - captureOptions, - buildAudioCaptureFailureDebug({ - sourceId, - platform: electronApi.platform, - reason: 'self-window-audio-route-unavailable', - }), - ); + removeAudioFromCaptureOptions(captureOptions); } const requestedAppAudioOnLinux = requestedAudio && !isOwnWindowShare && electronApi.platform === 'linux' && sourceId?.startsWith('window:'); @@ -513,11 +507,14 @@ async function runConfiguredDisplayScreenShare( } if (!useWaylandPortal && sourceId) { setDesktopSourceIntent({sourceId, includeAudio: false}); - ActiveScreenShareSource.setSourceId(sourceId, {isOwnWindow: isOwnWindowShare}); } let operationSucceeded = false; if (mode === 'switch' && !restartWaylandPortalForSwitch) { - operationSucceeded = await MediaEngine.replaceActiveDisplayScreenShare(captureOptions, publishOptions); + operationSucceeded = await MediaEngine.replaceActiveDisplayScreenShare(captureOptions, publishOptions, { + sourceId: sourceId ?? null, + displayShareEnvironment, + requireAudio: requestedAudio && nativeAudioArmed, + }); } else { await MediaEngine.setScreenShareEnabled( true, @@ -530,7 +527,15 @@ async function runConfiguredDisplayScreenShare( if (nativeAudioArmed && !captured) { cleanupNativeAudioAfterCaptureDidNotStart(mode); } - if (!captured) { + if (captured && !useWaylandPortal && sourceId) { + ActiveScreenShareSource.setPublishedSource(sourceId.startsWith('window:') ? 'app' : 'display', sourceId, { + isOwnWindow: isOwnWindowShare, + }); + } + if (captured && useWaylandPortal) { + ActiveScreenShareSource.setPublishedSource('wayland', null); + } + if (!captured && mode === 'start' && !didScreenShareStart()) { ActiveScreenShareSource.clear(); } if (!captured && useWaylandPortal && mode !== 'switch') { @@ -570,7 +575,7 @@ async function runConfiguredDisplayScreenShare( if (nativeAudioArmed && !capturedAfterError) { cleanupNativeAudioAfterCaptureDidNotStart(mode); } - if (!capturedAfterError) { + if (!capturedAfterError && mode === 'start' && !didScreenShareStart()) { ActiveScreenShareSource.clear(); } if (useWaylandPortal && !capturedAfterError && mode !== 'switch') { @@ -589,80 +594,153 @@ async function runConfiguredDisplayScreenShare( } let operationSucceeded = false; if (mode === 'switch') { - operationSucceeded = await MediaEngine.replaceActiveDisplayScreenShare(captureOptions, publishOptions); + operationSucceeded = await MediaEngine.replaceActiveDisplayScreenShare(captureOptions, publishOptions, { + sourceId: null, + displayShareEnvironment, + requireAudio: false, + }); } else { await MediaEngine.setScreenShareEnabled(true, captureOptions, publishOptions); operationSucceeded = didScreenShareStart(); } - return mode === 'switch' ? operationSucceeded : didScreenShareStart(); + const captured = mode === 'switch' ? operationSucceeded : didScreenShareStart(); + if (captured) { + ActiveScreenShareSource.setPublishedSource('web', null); + } + return captured; +} + +interface ConfiguredScreenShareMutationRequest { + execute: () => Promise; + promise: Promise; + resolve: (result: boolean) => void; + reject: (error: unknown) => void; +} + +let configuredScreenShareMutationActive = false; +let pendingConfiguredScreenShareMutation: ConfiguredScreenShareMutationRequest | null = null; + +function createConfiguredScreenShareMutationRequest( + execute: () => Promise, +): ConfiguredScreenShareMutationRequest { + let resolveRequest: ((result: boolean) => void) | undefined; + let rejectRequest: ((error: unknown) => void) | undefined; + const promise = new Promise((resolve, reject) => { + resolveRequest = resolve; + rejectRequest = reject; + }); + if (!resolveRequest || !rejectRequest) { + throw new Error('Configured screen share mutation deferred was not initialized'); + } + return {execute, promise, resolve: resolveRequest, reject: rejectRequest}; +} + +async function drainConfiguredScreenShareMutations( + initialRequest: ConfiguredScreenShareMutationRequest, +): Promise { + let request: ConfiguredScreenShareMutationRequest | null = initialRequest; + while (request) { + try { + request.resolve(await request.execute()); + } catch (error) { + request.reject(error); + } + request = pendingConfiguredScreenShareMutation; + pendingConfiguredScreenShareMutation = null; + } + configuredScreenShareMutationActive = false; +} + +function scheduleConfiguredScreenShareMutation(execute: () => Promise): Promise { + const request = createConfiguredScreenShareMutationRequest(execute); + if (!configuredScreenShareMutationActive) { + configuredScreenShareMutationActive = true; + void drainConfiguredScreenShareMutations(request); + return request.promise; + } + pendingConfiguredScreenShareMutation?.resolve(false); + pendingConfiguredScreenShareMutation = request; + return request.promise; } export async function startConfiguredDisplayScreenShare( sourceId?: string | null, options?: ConfiguredDisplayScreenShareOptions, ): Promise { - let didStart = false; - await executeScreenShareOperation(async () => { - didStart = await runConfiguredDisplayScreenShare(sourceId, options, 'start'); + return scheduleConfiguredScreenShareMutation(async () => { + let didStart = false; + await executeScreenShareOperation(async () => { + didStart = await runConfiguredDisplayScreenShare(sourceId, options, 'start'); + }); + return didStart; }); - return didStart; } export async function switchConfiguredDisplayScreenShare( sourceId?: string | null, options?: ConfiguredDisplayScreenShareOptions, ): Promise { - let didSwitch = false; - await executeScreenShareOperation(async () => { - didSwitch = await runConfiguredDisplayScreenShare(sourceId, options, 'switch'); + return scheduleConfiguredScreenShareMutation(async () => { + let didSwitch = false; + await executeScreenShareOperation(async () => { + didSwitch = await runConfiguredDisplayScreenShare(sourceId, options, 'switch'); + }); + return didSwitch; }); - return didSwitch; } export async function startConfiguredDeviceScreenShare(videoDeviceId: string): Promise { - normaliseDeviceScreenShareSettings(); - const {captureOptions, publishOptions, includeAudio, audioDeviceId} = getConfiguredScreenShareOptions( - 'device', - 'desktop-custom', - ); - try { - await MediaEngine.startDeviceScreenShare( - { - videoDeviceId, - audioDeviceId: includeAudio ? audioDeviceId : undefined, - resolution: captureOptions.resolution, - }, - publishOptions, + return scheduleConfiguredScreenShareMutation(async () => { + normaliseDeviceScreenShareSettings(); + const {captureOptions, publishOptions, includeAudio, audioDeviceId} = getConfiguredScreenShareOptions( + 'device', + 'desktop-custom', ); - } catch (error) { - logger.error('Failed to start device screen share', { - error, - videoDeviceId, - }); - } - return didScreenShareStart(); + try { + await MediaEngine.startDeviceScreenShare( + { + videoDeviceId, + audioDeviceId: includeAudio ? audioDeviceId : undefined, + resolution: captureOptions.resolution, + }, + publishOptions, + ); + } catch (error) { + logger.error('Failed to start device screen share', { + error, + videoDeviceId, + }); + } + const didStart = didScreenShareStart(); + if (didStart) ActiveScreenShareSource.setPublishedSource('device', null); + return didStart; + }); } export async function switchConfiguredDeviceScreenShare(videoDeviceId: string): Promise { - normaliseDeviceScreenShareSettings(); - const {captureOptions, publishOptions, includeAudio, audioDeviceId} = getConfiguredScreenShareOptions( - 'device', - 'desktop-custom', - ); - try { - return await MediaEngine.replaceActiveDeviceScreenShare( - { - videoDeviceId, - audioDeviceId: includeAudio ? audioDeviceId : undefined, - resolution: captureOptions.resolution, - }, - publishOptions, + return scheduleConfiguredScreenShareMutation(async () => { + normaliseDeviceScreenShareSettings(); + const {captureOptions, publishOptions, includeAudio, audioDeviceId} = getConfiguredScreenShareOptions( + 'device', + 'desktop-custom', ); - } catch (error) { - logger.error('Failed to switch device screen share source', { - error, - videoDeviceId, - }); - return false; - } + try { + const didSwitch = await MediaEngine.replaceActiveDeviceScreenShare( + { + videoDeviceId, + audioDeviceId: includeAudio ? audioDeviceId : undefined, + resolution: captureOptions.resolution, + }, + publishOptions, + ); + if (didSwitch) ActiveScreenShareSource.setPublishedSource('device', null); + return didSwitch; + } catch (error) { + logger.error('Failed to switch device screen share source', { + error, + videoDeviceId, + }); + return false; + } + }); }