mirror of https://github.com/OpenVidu/openvidu.git
ov-components: Add E2EEKey directive and integrate E2EE configuration in OpenViduService
ov-components: Update E2EE error messages for improved clarity across multiple languagespull/856/head
parent
4bf413cffc
commit
9950a2ba21
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import { Component, Input } from '@angular/core';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
|
||||
@Component({
|
||||
selector: 'ov-avatar-profile',
|
||||
template: `
|
||||
<div class="poster" id="video-poster">
|
||||
@if (letter) {
|
||||
<div class="initial" [ngStyle]="{ 'background-color': color }">
|
||||
<span id="poster-text">{{ letter }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
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() {}
|
||||
}
|
||||
|
|
@ -8,7 +8,13 @@ import { Track } from 'livekit-client';
|
|||
@Component({
|
||||
selector: 'ov-media-element',
|
||||
template: `
|
||||
<ov-avatar-profile @posterAnimation *ngIf="showAvatar" [name]="avatarName" [color]="avatarColor"></ov-avatar-profile>
|
||||
<ov-video-poster
|
||||
@posterAnimation
|
||||
[showAvatar]="showAvatar"
|
||||
[nickname]="avatarName"
|
||||
[color]="avatarColor"
|
||||
[hasEncryptionError]="hasEncryptionError"
|
||||
></ov-video-poster>
|
||||
<video #videoElement *ngIf="_track?.kind === 'video'" class="OV_video-element" [attr.id]="_track?.sid"></video>
|
||||
<audio #audioElement *ngIf="_track?.kind === 'audio'" [attr.id]="_track?.sid"></audio>
|
||||
`,
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
[style.background-color]="_participant?.colorProfile"
|
||||
[attr.aria-label]="'Avatar for ' + participantDisplayName"
|
||||
>
|
||||
<mat-icon>person</mat-icon>
|
||||
<mat-icon>{{_participant.hasEncryptionError ? 'lock_person' : 'person'}}</mat-icon>
|
||||
</div>
|
||||
|
||||
<!-- Content section with name and status -->
|
||||
|
|
@ -28,14 +28,17 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
@if (!_participant.hasEncryptionError) {
|
||||
<div class="participant-subtitle">
|
||||
<span class="status-indicator">
|
||||
{{ _participant | tracksPublishedTypes }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Action buttons section -->
|
||||
@if (!_participant.hasEncryptionError) {
|
||||
<div class="participant-action-buttons">
|
||||
<!-- Mute/Unmute button for remote participants -->
|
||||
<button
|
||||
|
|
@ -52,7 +55,9 @@
|
|||
: ('PANEL.PARTICIPANTS.MUTE' | translate) + ' ' + participantDisplayName
|
||||
"
|
||||
[matTooltip]="
|
||||
_participant?.isMutedForcibly ? ('PANEL.PARTICIPANTS.UNMUTE' | translate) : ('PANEL.PARTICIPANTS.MUTE' | translate)
|
||||
_participant?.isMutedForcibly
|
||||
? ('PANEL.PARTICIPANTS.UNMUTE' | translate)
|
||||
: ('PANEL.PARTICIPANTS.MUTE' | translate)
|
||||
"
|
||||
>
|
||||
<mat-icon *ngIf="!_participant?.isMutedForcibly">volume_up</mat-icon>
|
||||
|
|
@ -64,6 +69,7 @@
|
|||
<ng-container *ngTemplateOutlet="participantPanelItemElementsTemplate"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -34,8 +34,10 @@
|
|||
[avatarName]="_track.participant.name"
|
||||
[muted]="_track.isMutedForcibly"
|
||||
[isLocal]="_track.participant.isLocal"
|
||||
[hasEncryptionError]="_track.participant.hasEncryptionError"
|
||||
></ov-media-element>
|
||||
|
||||
@if (!_track.participant.hasEncryptionError) {
|
||||
<div class="status-icons">
|
||||
<mat-icon id="status-mic" fontIcon="mic_off" *ngIf="!_track.participant?.isMicrophoneEnabled"></mat-icon>
|
||||
<mat-icon id="status-muted-forcibly" fontIcon="volume_off" *ngIf="_track.isMutedForcibly"></mat-icon>
|
||||
|
|
@ -77,4 +79,5 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
@if (hasEncryptionError) {
|
||||
<div class="encryption-error-poster">
|
||||
<div class="encryption-error-content">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||
<path d="M7 11V7a5 5 0 0 1 9.9-1"></path>
|
||||
<line x1="12" y1="16" x2="12" y2="18"></line>
|
||||
</svg>
|
||||
<h3>{{ 'ERRORS.E2EE_ERROR_TITLE' | translate }}</h3>
|
||||
<p>{{ 'ERRORS.E2EE_ERROR_CONTENT' | translate }}</p>
|
||||
</div>
|
||||
</div>
|
||||
} @else if (showAvatar) {
|
||||
<div class="poster" id="video-poster">
|
||||
@if (letter) {
|
||||
<div class="initial" [ngStyle]="{ 'background-color': color }">
|
||||
<span id="poster-text">{{ letter }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
* <ov-videoconference [e2eeKey]="yourEncryptionKey"></ov-videoconference>
|
||||
*/
|
||||
@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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": "此参与者使用了不同的安全密钥。无法显示视频。"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": "यह प्रतिभागी एक अलग सुरक्षा कुंजी का उपयोग कर रहा है। वीडियो प्रदर्शित नहीं किया जा सकता।"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": "この参加者は異なるセキュリティキーを使用しています。ビデオを表示できません。"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ export class ParticipantModel {
|
|||
private room: Room | undefined;
|
||||
private speaking: boolean = false;
|
||||
private customVideoTrack: Partial<ParticipantTrackPublication>;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<string | undefined> = this.generalConfig.observable$.pipe(
|
||||
map((config) => config.e2eeKey),
|
||||
distinctUntilChanged(),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
// Stream observables
|
||||
videoEnabled$: Observable<boolean> = 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 {
|
||||
|
|
|
|||
|
|
@ -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<boolean> = 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
|
||||
};
|
||||
this.room = new Room(roomOptions);
|
||||
this.log.d('Room initialized successfully');
|
||||
|
||||
// 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<void> {
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in New Issue