diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/avatar-profile/avatar-profile.component.scss b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/avatar-profile/avatar-profile.component.scss deleted file mode 100644 index 9ec7407ba..000000000 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/avatar-profile/avatar-profile.component.scss +++ /dev/null @@ -1,31 +0,0 @@ -.poster { - height: 100%; - width: 100%; - background-color: var(--ov-video-background, var(--ov-primary-action-color)); - position: absolute; - z-index: 888; - border-radius: var(--ov-video-radius); -} - -.initial { - position: absolute; - display: inline-grid; - z-index: 1; - margin: auto; - bottom: 0; - right: 0; - left: 0; - top: 0; - height: 70px; - width: 70px; - border-radius: var(--ov-video-radius); - border: 2px solid var(--ov-text-primary-color); - color: var(--ov-video-background, var(--ov-text-primary-color)); -} - -#poster-text { - padding: 0px !important; - font-weight: bold; - font-size: 40px; - margin: auto; -} \ No newline at end of file diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/avatar-profile/avatar-profile.component.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/avatar-profile/avatar-profile.component.ts deleted file mode 100644 index b6e9b21ed..000000000 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/avatar-profile/avatar-profile.component.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Component, Input } from '@angular/core'; - -/** - * @internal - */ - -@Component({ - selector: 'ov-avatar-profile', - template: ` -
- @if (letter) { -
- {{ letter }} -
- } -
- `, - styleUrls: ['./avatar-profile.component.scss'], - standalone: false -}) -export class AvatarProfileComponent { - letter: string; - - @Input() - set name(name: string) { - if (name) this.letter = name[0]; - } - @Input() color; - - constructor() {} -} diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/media-element/media-element.component.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/media-element/media-element.component.ts index 48095dd2e..10a160854 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/media-element/media-element.component.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/media-element/media-element.component.ts @@ -8,7 +8,13 @@ import { Track } from 'livekit-client'; @Component({ selector: 'ov-media-element', template: ` - + `, @@ -29,10 +35,11 @@ export class MediaElementComponent implements AfterViewInit, OnDestroy { private _muted: boolean = false; private previousTrack: Track | null = null; - @Input() showAvatar: boolean; - @Input() avatarColor: string; - @Input() avatarName: string; - @Input() isLocal: boolean; + @Input() showAvatar: boolean = false; + @Input() avatarColor: string = '#000000'; + @Input() avatarName: string = 'User'; + @Input() isLocal: boolean = false; + @Input() hasEncryptionError: boolean = false; @ViewChild('videoElement', { static: false }) set videoElement(element: ElementRef) { diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/participants-panel/participant-panel-item/participant-panel-item.component.html b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/participants-panel/participant-panel-item/participant-panel-item.component.html index 0c5edd922..90e53d638 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/participants-panel/participant-panel-item/participant-panel-item.component.html +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/participants-panel/participant-panel-item/participant-panel-item.component.html @@ -8,7 +8,7 @@ [style.background-color]="_participant?.colorProfile" [attr.aria-label]="'Avatar for ' + participantDisplayName" > - person + {{_participant.hasEncryptionError ? 'lock_person' : 'person'}} @@ -28,42 +28,48 @@ -
- - {{ _participant | tracksPublishedTypes }} - -
+ @if (!_participant.hasEncryptionError) { +
+ + {{ _participant | tracksPublishedTypes }} + +
+ } -
- - + @if (!_participant.hasEncryptionError) { +
+ + - -
- + +
+ +
-
+ }
diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/session/session.component.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/session/session.component.ts index 9d54ef4ec..ffad39287 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/session/session.component.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/session/session.component.ts @@ -230,7 +230,8 @@ export class SessionComponent implements OnInit, OnDestroy { } // this.subscribeToCaptionLanguage(); - this.subcribeToActiveSpeakersChanged(); + this.subscribeToEncryptionErrors(); + this.subscribeToActiveSpeakersChanged(); this.subscribeToParticipantConnected(); this.subscribeToTrackSubscribed(); this.subscribeToTrackUnsubscribed(); @@ -261,7 +262,17 @@ export class SessionComponent implements OnInit, OnDestroy { this.actionService.openDialog(this.translateService.translate('ERRORS.SESSION'), error?.error || error?.message || error); } } - subcribeToActiveSpeakersChanged() { + + protected subscribeToEncryptionErrors() { + // TODO: LiveKit does not provide the participant who has the encryption error yet. + // Waiting for this issue to be solved: https://github.com/livekit/client-sdk-js/issues/1722 + this.room.on(RoomEvent.EncryptionError, (error: Error, participant?: RemoteParticipant) => { + if (!participant) return; + this.participantService.setEncryptionError(participant?.sid, true); + }); + } + + protected subscribeToActiveSpeakersChanged() { this.room.on(RoomEvent.ActiveSpeakersChanged, (speakers: Participant[]) => { this.participantService.setSpeaking(speakers); }); diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/stream/stream.component.html b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/stream/stream.component.html index 54261d7e1..ad39f2d56 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/stream/stream.component.html +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/stream/stream.component.html @@ -34,47 +34,50 @@ [avatarName]="_track.participant.name" [muted]="_track.isMutedForcibly" [isLocal]="_track.participant.isLocal" + [hasEncryptionError]="_track.participant.hasEncryptionError" > -
- - - -
- -
-
- - - + @if (!_track.participant.hasEncryptionError) { +
+ + +
-
+ +
+
+ + + +
+
+ }
diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/video-poster/video-poster.component.html b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/video-poster/video-poster.component.html new file mode 100644 index 000000000..3954053a1 --- /dev/null +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/video-poster/video-poster.component.html @@ -0,0 +1,31 @@ +@if (hasEncryptionError) { +
+
+ + + + + +

{{ 'ERRORS.E2EE_ERROR_TITLE' | translate }}

+

{{ 'ERRORS.E2EE_ERROR_CONTENT' | translate }}

+
+
+} @else if (showAvatar) { +
+ @if (letter) { +
+ {{ letter }} +
+ } +
+} diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/video-poster/video-poster.component.scss b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/video-poster/video-poster.component.scss new file mode 100644 index 000000000..8041396c5 --- /dev/null +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/video-poster/video-poster.component.scss @@ -0,0 +1,74 @@ +.encryption-error-poster { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: #000000; + backdrop-filter: blur(10px); + border-radius: var(--ov-video-radius); + z-index: 10; +} + +.encryption-error-content { + text-align: center; + padding: 2rem; + max-width: 300px; + color: var(--ov-text-primary-color); + + svg { + color: #dc3545; + margin-bottom: 1rem; + filter: drop-shadow(0 2px 4px rgba(220, 53, 69, 0.3)); + } + + h3 { + font-size: 1.1rem; + font-weight: 600; + margin: 0 0 0.5rem 0; + color: var(--ov-text-primary-color); + } + + p { + font-size: 0.875rem; + margin: 0; + opacity: 0.8; + line-height: 1.4; + color: var(--ov-text-secondary-color); + } +} + +.poster { + height: 100%; + width: 100%; + background-color: var(--ov-video-background, var(--ov-primary-action-color)); + position: absolute; + z-index: 888; + border-radius: var(--ov-video-radius); +} + +.initial { + position: absolute; + display: inline-grid; + z-index: 1; + margin: auto; + bottom: 0; + right: 0; + left: 0; + top: 0; + height: 70px; + width: 70px; + border-radius: var(--ov-video-radius); + border: 2px solid var(--ov-text-primary-color); + color: var(--ov-video-background, var(--ov-text-primary-color)); +} + +#poster-text { + padding: 0px !important; + font-weight: bold; + font-size: 40px; + margin: auto; +} diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/video-poster/video-poster.component.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/video-poster/video-poster.component.ts new file mode 100644 index 000000000..d56c30cf8 --- /dev/null +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/video-poster/video-poster.component.ts @@ -0,0 +1,20 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'ov-video-poster', + templateUrl: './video-poster.component.html', + styleUrl: './video-poster.component.scss', + standalone: false +}) +export class VideoPosterComponent { + letter: string = ''; + + @Input() + set nickname(name: string) { + if (name) this.letter = name[0]; + } + @Input() color: string = '#000000'; + @Input() showAvatar: boolean = true; + + @Input() hasEncryptionError: boolean = false; +} diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/directives/api/api.directive.module.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/directives/api/api.directive.module.ts index 8206f3363..e7552fba9 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/directives/api/api.directive.module.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/directives/api/api.directive.module.ts @@ -18,7 +18,8 @@ import { RecordingActivityViewRecordingsButtonDirective, RecordingActivityShowRecordingsListDirective, ToolbarRoomNameDirective, - ShowThemeSelectorDirective + ShowThemeSelectorDirective, + E2EEKeyDirective } from './internals.directive'; import { ParticipantPanelItemMuteButtonDirective } from './participant-panel-item.directive'; import { @@ -113,7 +114,8 @@ const directives = [ RecordingActivityViewRecordingsButtonDirective, RecordingActivityShowRecordingsListDirective, ToolbarRoomNameDirective, - ShowThemeSelectorDirective + ShowThemeSelectorDirective, + E2EEKeyDirective ]; @NgModule({ diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/directives/api/internals.directive.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/directives/api/internals.directive.ts index 7c47aaa50..69b8fede2 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/directives/api/internals.directive.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/directives/api/internals.directive.ts @@ -570,3 +570,51 @@ export class ShowThemeSelectorDirective implements AfterViewInit, OnDestroy { this.libService.updateGeneralConfig({ showThemeSelector: value }); } } + +/** + * @internal + * + * The **e2eeKey** directive allows to configure end-to-end encryption for the videoconference. + * When provided, the room will be configured with E2EE using an external key provider. + * + * Default: `undefined` + * + * Usage: + * + */ +@Directive({ + selector: 'ov-videoconference[e2eeKey]', + standalone: false +}) +export class E2EEKeyDirective implements AfterViewInit, OnDestroy { + @Input() set e2eeKey(value: string | undefined) { + this._value = value; + this.update(this._value); + } + + private _value: string | undefined; + + constructor( + public elementRef: ElementRef, + private libService: OpenViduComponentsConfigService + ) {} + + ngAfterViewInit() { + this.update(this._value); + } + + ngOnDestroy(): void { + this.clear(); + } + + private clear() { + this._value = undefined; + this.update(this._value); + } + + private update(value: string | undefined) { + // Only update if value is valid (not undefined, not null, not empty string) + const validValue = value && value.trim() !== '' ? value.trim() : undefined; + this.libService.updateGeneralConfig({ e2eeKey: validValue }); + } +} diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/cn.json b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/cn.json index 985a23640..f9d9eeedf 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/cn.json +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/cn.json @@ -175,6 +175,8 @@ "MEDIA_ERR_GENERIC": "加载视频时发生错误。", "MEDIA_ERR_NETWORK": "网络错误导致视频下载中途失败。", "MEDIA_ERR_DECODE": "由于损坏问题或视频使用了您的浏览器不支持的功能,视频播放被中止。", - "MEDIA_ERR_SRC_NOT_SUPPORTED": "流媒体录制错误:无法访问 S3。" + "MEDIA_ERR_SRC_NOT_SUPPORTED": "流媒体录制错误:无法访问 S3。", + "E2EE_ERROR_TITLE": "房间密码错误", + "E2EE_ERROR_CONTENT": "此参与者使用了不同的安全密钥。无法显示视频。" } } diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/de.json b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/de.json index 3d5ff1e7b..fc6955d1a 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/de.json +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/de.json @@ -170,6 +170,8 @@ "MEDIA_ERR_GENERIC": "Beim Laden des Videos ist ein Fehler aufgetreten.", "MEDIA_ERR_NETWORK": "Ein Netzwerkfehler führte dazu, dass der Video-Download teilweise fehlschlug.", "MEDIA_ERR_DECODE": "Die Videowiedergabe wurde aufgrund eines Korruptionsproblems oder weil das Video Funktionen verwendet, die Ihr Browser nicht unterstützt, abgebrochen.", - "MEDIA_ERR_SRC_NOT_SUPPORTED": "Fehler beim Streaming der Aufnahme: S3 konnte nicht erreicht werden." + "MEDIA_ERR_SRC_NOT_SUPPORTED": "Fehler beim Streaming der Aufnahme: S3 konnte nicht erreicht werden.", + "E2EE_ERROR_TITLE": "Raum-Passwortfehler", + "E2EE_ERROR_CONTENT": "Dieser Teilnehmer verwendet einen anderen Sicherheitsschlüssel. Video kann nicht angezeigt werden." } } diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/en.json b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/en.json index 76ed85afd..07c82dec7 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/en.json +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/en.json @@ -179,6 +179,8 @@ "MEDIA_ERR_GENERIC": "An error occurred while loading the video.", "MEDIA_ERR_NETWORK": "A network error caused the video download to fail part-way.", "MEDIA_ERR_DECODE": "The video playback was aborted due to a corruption problem or because the video used features your browser did not support.", - "MEDIA_ERR_SRC_NOT_SUPPORTED": "Error streaming recording: S3 could not be reached." + "MEDIA_ERR_SRC_NOT_SUPPORTED": "Error streaming recording: S3 could not be reached.", + "E2EE_ERROR_TITLE": "Room password error", + "E2EE_ERROR_CONTENT": "This participant is using a different encryption key. Video cannot be displayed." } } diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/es.json b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/es.json index f15c7d533..8e23e2f85 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/es.json +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/es.json @@ -174,6 +174,8 @@ "MEDIA_ERR_GENERIC": "Ocurrió un error al cargar el video.", "MEDIA_ERR_NETWORK": "Un error de red causó que la descarga del video fallara a mitad de camino.", "MEDIA_ERR_DECODE": "La reproducción del video se interrumpió debido a un problema de corrupción o porque el video utiliza características que su navegador no soporta.", - "MEDIA_ERR_SRC_NOT_SUPPORTED": "Error al transmitir la grabación: no se pudo conectar con S3." + "MEDIA_ERR_SRC_NOT_SUPPORTED": "Error al transmitir la grabación: no se pudo conectar con S3.", + "E2EE_ERROR_TITLE": "Error de contraseña de sala", + "E2EE_ERROR_CONTENT": "Este participante está utilizando una clave de cifrado diferente. No se puede mostrar el video." } } diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/fr.json b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/fr.json index d9ef66aa3..cfe895434 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/fr.json +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/fr.json @@ -175,6 +175,8 @@ "MEDIA_ERR_GENERIC": "Une erreur s'est produite lors du chargement de la vidéo.", "MEDIA_ERR_NETWORK": "Une erreur de réseau a causé l'échec du téléchargement de la vidéo en cours de route.", "MEDIA_ERR_DECODE": "La lecture de la vidéo a été interrompue en raison d'un problème de corruption ou parce que la vidéo utilise des fonctionnalités que votre navigateur ne prend pas en charge.", - "MEDIA_ERR_SRC_NOT_SUPPORTED": "Erreur de diffusion de l'enregistrement : S3 n'a pas pu être atteint." + "MEDIA_ERR_SRC_NOT_SUPPORTED": "Erreur de diffusion de l'enregistrement : S3 n'a pas pu être atteint.", + "E2EE_ERROR_TITLE": "Erreur de mot de passe de la salle", + "E2EE_ERROR_CONTENT": "Ce participant utilise une clé de sécurité différente. La vidéo ne peut pas être affichée." } } diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/hi.json b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/hi.json index 4aa2ab1c8..a61ed1cef 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/hi.json +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/hi.json @@ -175,6 +175,8 @@ "MEDIA_ERR_GENERIC": "वीडियो लोड करते समय एक त्रुटि हुई।", "MEDIA_ERR_NETWORK": "नेटवर्क त्रुटि के कारण वीडियो डाउनलोड बीच में ही विफल हो गया।", "MEDIA_ERR_DECODE": "वीडियो प्लेबैक को एक भ्रष्टाचार समस्या या क्योंकि वीडियो ने आपके ब्राउज़र द्वारा समर्थित नहीं की गई सुविधाओं का उपयोग किया था, के कारण रोक दिया गया था।", - "MEDIA_ERR_SRC_NOT_SUPPORTED": "स्ट्रीमिंग रिकॉर्डिंग में त्रुटि: S3 तक पहुंचा नहीं जा सका।" + "MEDIA_ERR_SRC_NOT_SUPPORTED": "स्ट्रीमिंग रिकॉर्डिंग में त्रुटि: S3 तक पहुंचा नहीं जा सका।", + "E2EE_ERROR_TITLE": "कक्ष पासवर्ड त्रुटि", + "E2EE_ERROR_CONTENT": "यह प्रतिभागी एक अलग सुरक्षा कुंजी का उपयोग कर रहा है। वीडियो प्रदर्शित नहीं किया जा सकता।" } } diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/it.json b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/it.json index ce162e072..e9b91213f 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/it.json +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/it.json @@ -175,6 +175,8 @@ "MEDIA_ERR_GENERIC": "Si è verificato un errore durante il caricamento del video.", "MEDIA_ERR_NETWORK": "Un errore di rete ha causato l'interruzione del download del video a metà strada.", "MEDIA_ERR_DECODE": "La riproduzione del video è stata interrotta a causa di un problema di corruzione o perché il video utilizzava funzionalità non supportate dal tuo browser.", - "MEDIA_ERR_SRC_NOT_SUPPORTED": "Errore nello streaming della registrazione: impossibile raggiungere S3." + "MEDIA_ERR_SRC_NOT_SUPPORTED": "Errore nello streaming della registrazione: impossibile raggiungere S3.", + "E2EE_ERROR_TITLE": "Errore password della stanza", + "E2EE_ERROR_CONTENT": "Questo partecipante sta utilizzando una chiave di crittografia diversa. Il video non può essere visualizzato." } } diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/ja.json b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/ja.json index adea633f2..e7cf36bae 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/ja.json +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/ja.json @@ -175,6 +175,8 @@ "MEDIA_ERR_GENERIC": "ビデオの読み込み中にエラーが発生しました。", "MEDIA_ERR_NETWORK": "ネットワークエラーにより、ビデオのダウンロードが途中で失敗しました。", "MEDIA_ERR_DECODE": "破損の問題またはビデオがブラウザでサポートされていない機能を使用したために、ビデオの再生が中止されました。", - "MEDIA_ERR_SRC_NOT_SUPPORTED": "録画のストリーミングエラー:S3にアクセスできませんでした。" + "MEDIA_ERR_SRC_NOT_SUPPORTED": "録画のストリーミングエラー:S3にアクセスできませんでした。", + "E2EE_ERROR_TITLE": "ルームパスワードエラー", + "E2EE_ERROR_CONTENT": "この参加者は異なるセキュリティキーを使用しています。ビデオを表示できません。" } } diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/nl.json b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/nl.json index 68245e921..e37d64bef 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/nl.json +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/nl.json @@ -175,6 +175,8 @@ "MEDIA_ERR_GENERIC": "Er is een fout opgetreden bij het laden van de video.", "MEDIA_ERR_NETWORK": "Een netwerkfout heeft ertoe geleid dat het downloaden van de video halverwege is mislukt.", "MEDIA_ERR_DECODE": "Het afspelen van de video is afgebroken vanwege een corruptieprobleem of omdat de video functies gebruikte die uw browser niet ondersteunde.", - "MEDIA_ERR_SRC_NOT_SUPPORTED": "Fout bij het streamen van de opname: S3 kon niet worden bereikt." + "MEDIA_ERR_SRC_NOT_SUPPORTED": "Fout bij het streamen van de opname: S3 kon niet worden bereikt.", + "E2EE_ERROR_TITLE": "Kamerwachtwoordfout", + "E2EE_ERROR_CONTENT": "Deze deelnemer gebruikt een andere beveiligingssleutel. Video kan niet worden weergegeven." } } diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/pt.json b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/pt.json index d9eaf3951..ac90affb4 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/pt.json +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/lang/pt.json @@ -176,6 +176,8 @@ "MEDIA_ERR_GENERIC": "Ocorreu um erro ao carregar o vídeo.", "MEDIA_ERR_NETWORK": "Um erro de rede fez com que o download do vídeo falhasse parcialmente.", "MEDIA_ERR_DECODE": "A reprodução do vídeo foi interrompida devido a um problema de corrupção ou porque o vídeo usou recursos que o seu navegador não suportava.", - "MEDIA_ERR_SRC_NOT_SUPPORTED": "Erro ao transmitir a gravação: não foi possível acessar o S3." + "MEDIA_ERR_SRC_NOT_SUPPORTED": "Erro ao transmitir a gravação: não foi possível acessar o S3.", + "E2EE_ERROR_TITLE": "Erro de senha da sala", + "E2EE_ERROR_CONTENT": "Este participante está usando uma chave de criptografia diferente. O vídeo não pode ser exibido." } } diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/models/participant.model.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/models/participant.model.ts index c920b0090..025ef7815 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/models/participant.model.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/models/participant.model.ts @@ -128,6 +128,7 @@ export class ParticipantModel { private room: Room | undefined; private speaking: boolean = false; private customVideoTrack: Partial; + private _hasEncryptionError: boolean = false; constructor(props: ParticipantProperties) { this.participant = props.participant; @@ -550,4 +551,22 @@ export class ParticipantModel { setMutedForcibly(muted: boolean) { this.tracks.forEach((track) => (track.isMutedForcibly = muted)); } + + /** + * Gets whether this participant has an encryption error. + * This indicates that the participant cannot decrypt the video stream due to an incorrect encryption key. + * @returns boolean + */ + get hasEncryptionError(): boolean { + return this._hasEncryptionError; + } + + /** + * Sets the encryption error state for this participant. + * @param hasError - Whether the participant has an encryption error + * @internal + */ + setEncryptionError(hasError: boolean) { + this._hasEncryptionError = hasError; + } } diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/openvidu-components-angular-ui.module.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/openvidu-components-angular-ui.module.ts index f3271a29e..fd41024e5 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/openvidu-components-angular-ui.module.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/openvidu-components-angular-ui.module.ts @@ -30,7 +30,6 @@ import { VideoconferenceComponent } from './components/videoconference/videoconf import { AdminDashboardComponent } from './admin/admin-dashboard/admin-dashboard.component'; import { AdminLoginComponent } from './admin/admin-login/admin-login.component'; -import { AvatarProfileComponent } from './components/avatar-profile/avatar-profile.component'; // import { CaptionsComponent } from './components/captions/captions.component'; import { ProFeatureDialogTemplateComponent } from './components/dialogs/pro-feature-dialog.component'; import { ActivitiesPanelComponent } from './components/panel/activities-panel/activities-panel.component'; @@ -48,6 +47,7 @@ import { OpenViduComponentsDirectiveModule } from './directives/template/openvid import { AppMaterialModule } from './openvidu-components-angular.material.module'; import { ThemeSelectorComponent } from './components/settings/theme-selector/theme-selector.component'; import { LandscapeWarningComponent } from './components/landscape-warning/landscape-warning.component'; +import { VideoPosterComponent } from './components/video-poster/video-poster.component'; const publicComponents = [ AdminDashboardComponent, @@ -74,7 +74,7 @@ const privateComponents = [ ProFeatureDialogTemplateComponent, RecordingDialogComponent, DeleteDialogComponent, - AvatarProfileComponent, + VideoPosterComponent, MediaElementComponent, VideoDevicesComponent, AudioDevicesComponent, diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/config/directive-config.service.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/config/directive-config.service.ts index 6985c7625..d5c379dc2 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/config/directive-config.service.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/config/directive-config.service.ts @@ -96,6 +96,7 @@ interface GeneralConfig { showDisconnectionDialog: boolean; showThemeSelector: boolean; recordingStreamBaseUrl: string; + e2eeKey?: string; } /** @@ -302,7 +303,8 @@ export class OpenViduComponentsConfigService { prejoinDisplayParticipantName: true, showDisconnectionDialog: true, showThemeSelector: false, - recordingStreamBaseUrl: 'call/api/recordings' + recordingStreamBaseUrl: 'call/api/recordings', + e2eeKey: undefined }); private toolbarConfig = this.createToolbarConfigItem({ @@ -413,6 +415,11 @@ export class OpenViduComponentsConfigService { distinctUntilChanged(), shareReplay(1) ); + e2eeKey$: Observable = this.generalConfig.observable$.pipe( + map((config) => config.e2eeKey), + distinctUntilChanged(), + shareReplay(1) + ); // Stream observables videoEnabled$: Observable = this.streamConfig.observable$.pipe(map((config) => config.videoEnabled)); @@ -565,6 +572,10 @@ export class OpenViduComponentsConfigService { return baseUrl; } + getE2EEKey(): string | undefined { + return this.generalConfig.subject.getValue().e2eeKey; + } + // Stream configuration methods isVideoEnabled(): boolean { diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/openvidu/openvidu.service.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/openvidu/openvidu.service.ts index 793fd31e7..01d275a1a 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/openvidu/openvidu.service.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/openvidu/openvidu.service.ts @@ -6,6 +6,7 @@ import { AudioCaptureOptions, ConnectionState, CreateLocalTracksOptions, + ExternalE2EEKeyProvider, LocalAudioTrack, LocalTrack, LocalVideoTrack, @@ -17,6 +18,7 @@ import { createLocalTracks } from 'livekit-client'; import { StorageService } from '../storage/storage.service'; +import { OpenViduComponentsConfigService } from '../config/directive-config.service'; @Injectable({ providedIn: 'root' @@ -31,6 +33,7 @@ export class OpenViduService { // private _isSttReady: BehaviorSubject = new BehaviorSubject(true); private room: Room; + private keyProvider: ExternalE2EEKeyProvider | undefined; /** * @internal @@ -53,7 +56,8 @@ export class OpenViduService { constructor( private loggerSrv: LoggerService, private deviceService: DeviceService, - private storageService: StorageService + private storageService: StorageService, + private configService: OpenViduComponentsConfigService ) { this.log = this.loggerSrv.get('OpenViduService'); // this.isSttReadyObs = this._isSttReady.asObservable(); @@ -64,14 +68,25 @@ export class OpenViduService { * @internal */ initRoom(): void { - // If room already exists, don't recreate it - if (this.room) { + // Check if E2EE configuration needs to be applied + const e2eeKey = this.configService.getE2EEKey(); + const needsE2EEConfig = e2eeKey && e2eeKey.trim() !== '' && !this.keyProvider; + + // If room already exists and doesn't need E2EE reconfiguration, don't recreate it + if (this.room && !needsE2EEConfig) { this.log.d('Room already initialized, skipping re-initialization'); return; } + // If room exists but needs E2EE configuration, we need to recreate it + if (this.room && needsE2EEConfig) { + this.log.d('Room needs E2EE configuration, recreating room'); + this.room = null as any; + } + const videoDeviceId = this.deviceService.getCameraSelected()?.device ?? undefined; const audioDeviceId = this.deviceService.getMicrophoneSelected()?.device ?? undefined; + const roomOptions: RoomOptions = { adaptiveStream: true, dynacast: true, @@ -93,15 +108,35 @@ export class OpenViduService { stopLocalTrackOnUnpublish: true, disconnectOnPageLeave: true }; + + // Configure E2EE if key is provided + if (e2eeKey && e2eeKey.trim() !== '') { + this.log.d('Configuring E2EE with provided key'); + this.keyProvider = new ExternalE2EEKeyProvider(); + // Create worker using the copied livekit-client e2ee worker from assets + roomOptions.e2ee = { + keyProvider: this.keyProvider, + worker: new Worker('./assets/livekit/livekit-client.e2ee.worker.mjs', { type: 'module' }) + }; + } + this.room = new Room(roomOptions); this.log.d('Room initialized successfully'); - } - - /** + } /** * Connects local participant to the room */ async connectRoom(): Promise { try { + // Configure E2EE if key provider was initialized + if (this.keyProvider) { + const e2eeKey = this.configService.getE2EEKey(); + if (e2eeKey) { + this.log.d('Setting E2EE key and enabling encryption'); + await this.keyProvider.setKey(e2eeKey); + await this.room.setE2EEEnabled(true); + this.log.d('E2EE successfully enabled'); + } + } await this.room.connect(this.livekitUrl, this.livekitToken); this.log.d(`Successfully connected to room ${this.room.name}`); const participantName = this.storageService.getParticipantName(); diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/participant/participant.service.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/participant/participant.service.ts index 0e3d0106a..214f6ba8a 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/participant/participant.service.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/participant/participant.service.ts @@ -284,6 +284,26 @@ export class ParticipantService { }); } + /** + * Sets the encryption error state for a participant. + * This is called when a participant cannot decrypt video streams due to an incorrect encryption key. + * @param participantSid - The SID of the participant with the encryption error + * @param hasError - Whether the participant has an encryption error + * @internal + */ + setEncryptionError(participantSid: string, hasError: boolean) { + if (this.localParticipant?.sid === participantSid) { + this.localParticipant.setEncryptionError(hasError); + this.updateLocalParticipant(); + } else { + const participant = this.remoteParticipants.find((p) => p.sid === participantSid); + if (participant) { + participant.setEncryptionError(hasError); + this.updateRemoteParticipants(); + } + } + } + /** * Returns the local participant name. */