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({
|
@Component({
|
||||||
selector: 'ov-media-element',
|
selector: 'ov-media-element',
|
||||||
template: `
|
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>
|
<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>
|
<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 _muted: boolean = false;
|
||||||
private previousTrack: Track | null = null;
|
private previousTrack: Track | null = null;
|
||||||
|
|
||||||
@Input() showAvatar: boolean;
|
@Input() showAvatar: boolean = false;
|
||||||
@Input() avatarColor: string;
|
@Input() avatarColor: string = '#000000';
|
||||||
@Input() avatarName: string;
|
@Input() avatarName: string = 'User';
|
||||||
@Input() isLocal: boolean;
|
@Input() isLocal: boolean = false;
|
||||||
|
@Input() hasEncryptionError: boolean = false;
|
||||||
|
|
||||||
@ViewChild('videoElement', { static: false })
|
@ViewChild('videoElement', { static: false })
|
||||||
set videoElement(element: ElementRef) {
|
set videoElement(element: ElementRef) {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
[style.background-color]="_participant?.colorProfile"
|
[style.background-color]="_participant?.colorProfile"
|
||||||
[attr.aria-label]="'Avatar for ' + participantDisplayName"
|
[attr.aria-label]="'Avatar for ' + participantDisplayName"
|
||||||
>
|
>
|
||||||
<mat-icon>person</mat-icon>
|
<mat-icon>{{_participant.hasEncryptionError ? 'lock_person' : 'person'}}</mat-icon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content section with name and status -->
|
<!-- Content section with name and status -->
|
||||||
|
|
@ -28,42 +28,48 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="participant-subtitle">
|
@if (!_participant.hasEncryptionError) {
|
||||||
<span class="status-indicator">
|
<div class="participant-subtitle">
|
||||||
{{ _participant | tracksPublishedTypes }}
|
<span class="status-indicator">
|
||||||
</span>
|
{{ _participant | tracksPublishedTypes }}
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Action buttons section -->
|
<!-- Action buttons section -->
|
||||||
<div class="participant-action-buttons">
|
@if (!_participant.hasEncryptionError) {
|
||||||
<!-- Mute/Unmute button for remote participants -->
|
<div class="participant-action-buttons">
|
||||||
<button
|
<!-- Mute/Unmute button for remote participants -->
|
||||||
mat-icon-button
|
<button
|
||||||
id="mute-btn"
|
mat-icon-button
|
||||||
*ngIf="!isLocalParticipant && showMuteButton"
|
id="mute-btn"
|
||||||
[class.warn-btn]="_participant?.isMutedForcibly"
|
*ngIf="!isLocalParticipant && showMuteButton"
|
||||||
(click)="toggleMuteForcibly()"
|
[class.warn-btn]="_participant?.isMutedForcibly"
|
||||||
[disabled]="!_participant"
|
(click)="toggleMuteForcibly()"
|
||||||
[disableRipple]="true"
|
[disabled]="!_participant"
|
||||||
[attr.aria-label]="
|
[disableRipple]="true"
|
||||||
_participant?.isMutedForcibly
|
[attr.aria-label]="
|
||||||
? ('PANEL.PARTICIPANTS.UNMUTE' | translate) + ' ' + participantDisplayName
|
_participant?.isMutedForcibly
|
||||||
: ('PANEL.PARTICIPANTS.MUTE' | translate) + ' ' + participantDisplayName
|
? ('PANEL.PARTICIPANTS.UNMUTE' | translate) + ' ' + participantDisplayName
|
||||||
"
|
: ('PANEL.PARTICIPANTS.MUTE' | translate) + ' ' + participantDisplayName
|
||||||
[matTooltip]="
|
"
|
||||||
_participant?.isMutedForcibly ? ('PANEL.PARTICIPANTS.UNMUTE' | translate) : ('PANEL.PARTICIPANTS.MUTE' | translate)
|
[matTooltip]="
|
||||||
"
|
_participant?.isMutedForcibly
|
||||||
>
|
? ('PANEL.PARTICIPANTS.UNMUTE' | translate)
|
||||||
<mat-icon *ngIf="!_participant?.isMutedForcibly">volume_up</mat-icon>
|
: ('PANEL.PARTICIPANTS.MUTE' | translate)
|
||||||
<mat-icon *ngIf="_participant?.isMutedForcibly">volume_off</mat-icon>
|
"
|
||||||
</button>
|
>
|
||||||
|
<mat-icon *ngIf="!_participant?.isMutedForcibly">volume_up</mat-icon>
|
||||||
|
<mat-icon *ngIf="_participant?.isMutedForcibly">volume_off</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- External item elements with improved structure -->
|
<!-- External item elements with improved structure -->
|
||||||
<div class="external-elements" *ngIf="hasExternalElements">
|
<div class="external-elements" *ngIf="hasExternalElements">
|
||||||
<ng-container *ngTemplateOutlet="participantPanelItemElementsTemplate"></ng-container>
|
<ng-container *ngTemplateOutlet="participantPanelItemElementsTemplate"></ng-container>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
</div>
|
</div>
|
||||||
</mat-list-item>
|
</mat-list-item>
|
||||||
</mat-list>
|
</mat-list>
|
||||||
|
|
|
||||||
|
|
@ -230,7 +230,8 @@ export class SessionComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
// this.subscribeToCaptionLanguage();
|
// this.subscribeToCaptionLanguage();
|
||||||
this.subcribeToActiveSpeakersChanged();
|
this.subscribeToEncryptionErrors();
|
||||||
|
this.subscribeToActiveSpeakersChanged();
|
||||||
this.subscribeToParticipantConnected();
|
this.subscribeToParticipantConnected();
|
||||||
this.subscribeToTrackSubscribed();
|
this.subscribeToTrackSubscribed();
|
||||||
this.subscribeToTrackUnsubscribed();
|
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);
|
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.room.on(RoomEvent.ActiveSpeakersChanged, (speakers: Participant[]) => {
|
||||||
this.participantService.setSpeaking(speakers);
|
this.participantService.setSpeaking(speakers);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -34,47 +34,50 @@
|
||||||
[avatarName]="_track.participant.name"
|
[avatarName]="_track.participant.name"
|
||||||
[muted]="_track.isMutedForcibly"
|
[muted]="_track.isMutedForcibly"
|
||||||
[isLocal]="_track.participant.isLocal"
|
[isLocal]="_track.participant.isLocal"
|
||||||
|
[hasEncryptionError]="_track.participant.hasEncryptionError"
|
||||||
></ov-media-element>
|
></ov-media-element>
|
||||||
|
|
||||||
<div class="status-icons">
|
@if (!_track.participant.hasEncryptionError) {
|
||||||
<mat-icon id="status-mic" fontIcon="mic_off" *ngIf="!_track.participant?.isMicrophoneEnabled"></mat-icon>
|
<div class="status-icons">
|
||||||
<mat-icon id="status-muted-forcibly" fontIcon="volume_off" *ngIf="_track.isMutedForcibly"></mat-icon>
|
<mat-icon id="status-mic" fontIcon="mic_off" *ngIf="!_track.participant?.isMicrophoneEnabled"></mat-icon>
|
||||||
<mat-icon id="status-pinned" fontIcon="push_pin" *ngIf="_track.isPinned"></mat-icon>
|
<mat-icon id="status-muted-forcibly" fontIcon="volume_off" *ngIf="_track.isMutedForcibly"></mat-icon>
|
||||||
</div>
|
<mat-icon id="status-pinned" fontIcon="push_pin" *ngIf="_track.isPinned"></mat-icon>
|
||||||
|
|
||||||
<div class="stream-video-controls" *ngIf="!isMinimal && showVideoControls && mouseHovering">
|
|
||||||
<div class="flex-container">
|
|
||||||
<button
|
|
||||||
mat-icon-button
|
|
||||||
id="pin-btn"
|
|
||||||
(click)="toggleVideoPinned()"
|
|
||||||
[matTooltip]="_track.isPinned ? ('STREAM.UNPIN' | translate) : ('STREAM.PIN' | translate)"
|
|
||||||
>
|
|
||||||
<mat-icon *ngIf="_track.isPinned" fontSet="material-symbols-outlined" fontIcon="keep">keep_off</mat-icon>
|
|
||||||
<mat-icon *ngIf="!_track.isPinned" id="status-pinned" fontIcon="push_pin"></mat-icon>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
*ngIf="!_track.participant.isLocal"
|
|
||||||
mat-icon-button
|
|
||||||
id="silence-btn"
|
|
||||||
(click)="toggleMuteForcibly()"
|
|
||||||
[class.muted-btn]="_track.isMutedForcibly"
|
|
||||||
[matTooltip]="_track.isMutedForcibly ? ('STREAM.UNMUTE_SOUND' | translate) : ('STREAM.MUTE_SOUND' | translate)"
|
|
||||||
>
|
|
||||||
<mat-icon *ngIf="_track.isMutedForcibly">volume_off</mat-icon>
|
|
||||||
<mat-icon *ngIf="!_track.isMutedForcibly">volume_up</mat-icon>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
*ngIf="_track.participant.isLocal"
|
|
||||||
mat-icon-button
|
|
||||||
id="minimize-btn"
|
|
||||||
[disabled]="_track.isPinned"
|
|
||||||
(click)="toggleMinimize()"
|
|
||||||
[matTooltip]="_track.isMinimized ? ('STREAM.MAXIMIZE' | translate) : ('STREAM.MINIMIZE' | translate)"
|
|
||||||
>
|
|
||||||
<mat-icon *ngIf="_track.isMinimized">open_in_full</mat-icon>
|
|
||||||
<mat-icon *ngIf="!_track.isMinimized">close_fullscreen</mat-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="stream-video-controls" *ngIf="!isMinimal && showVideoControls && mouseHovering">
|
||||||
|
<div class="flex-container">
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
id="pin-btn"
|
||||||
|
(click)="toggleVideoPinned()"
|
||||||
|
[matTooltip]="_track.isPinned ? ('STREAM.UNPIN' | translate) : ('STREAM.PIN' | translate)"
|
||||||
|
>
|
||||||
|
<mat-icon *ngIf="_track.isPinned" fontSet="material-symbols-outlined" fontIcon="keep">keep_off</mat-icon>
|
||||||
|
<mat-icon *ngIf="!_track.isPinned" id="status-pinned" fontIcon="push_pin"></mat-icon>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
*ngIf="!_track.participant.isLocal"
|
||||||
|
mat-icon-button
|
||||||
|
id="silence-btn"
|
||||||
|
(click)="toggleMuteForcibly()"
|
||||||
|
[class.muted-btn]="_track.isMutedForcibly"
|
||||||
|
[matTooltip]="_track.isMutedForcibly ? ('STREAM.UNMUTE_SOUND' | translate) : ('STREAM.MUTE_SOUND' | translate)"
|
||||||
|
>
|
||||||
|
<mat-icon *ngIf="_track.isMutedForcibly">volume_off</mat-icon>
|
||||||
|
<mat-icon *ngIf="!_track.isMutedForcibly">volume_up</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
*ngIf="_track.participant.isLocal"
|
||||||
|
mat-icon-button
|
||||||
|
id="minimize-btn"
|
||||||
|
[disabled]="_track.isPinned"
|
||||||
|
(click)="toggleMinimize()"
|
||||||
|
[matTooltip]="_track.isMinimized ? ('STREAM.MAXIMIZE' | translate) : ('STREAM.MINIMIZE' | translate)"
|
||||||
|
>
|
||||||
|
<mat-icon *ngIf="_track.isMinimized">open_in_full</mat-icon>
|
||||||
|
<mat-icon *ngIf="!_track.isMinimized">close_fullscreen</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</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,
|
RecordingActivityViewRecordingsButtonDirective,
|
||||||
RecordingActivityShowRecordingsListDirective,
|
RecordingActivityShowRecordingsListDirective,
|
||||||
ToolbarRoomNameDirective,
|
ToolbarRoomNameDirective,
|
||||||
ShowThemeSelectorDirective
|
ShowThemeSelectorDirective,
|
||||||
|
E2EEKeyDirective
|
||||||
} from './internals.directive';
|
} from './internals.directive';
|
||||||
import { ParticipantPanelItemMuteButtonDirective } from './participant-panel-item.directive';
|
import { ParticipantPanelItemMuteButtonDirective } from './participant-panel-item.directive';
|
||||||
import {
|
import {
|
||||||
|
|
@ -113,7 +114,8 @@ const directives = [
|
||||||
RecordingActivityViewRecordingsButtonDirective,
|
RecordingActivityViewRecordingsButtonDirective,
|
||||||
RecordingActivityShowRecordingsListDirective,
|
RecordingActivityShowRecordingsListDirective,
|
||||||
ToolbarRoomNameDirective,
|
ToolbarRoomNameDirective,
|
||||||
ShowThemeSelectorDirective
|
ShowThemeSelectorDirective,
|
||||||
|
E2EEKeyDirective
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|
|
||||||
|
|
@ -570,3 +570,51 @@ export class ShowThemeSelectorDirective implements AfterViewInit, OnDestroy {
|
||||||
this.libService.updateGeneralConfig({ showThemeSelector: value });
|
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_GENERIC": "加载视频时发生错误。",
|
||||||
"MEDIA_ERR_NETWORK": "网络错误导致视频下载中途失败。",
|
"MEDIA_ERR_NETWORK": "网络错误导致视频下载中途失败。",
|
||||||
"MEDIA_ERR_DECODE": "由于损坏问题或视频使用了您的浏览器不支持的功能,视频播放被中止。",
|
"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_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_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_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_GENERIC": "An error occurred while loading the video.",
|
||||||
"MEDIA_ERR_NETWORK": "A network error caused the video download to fail part-way.",
|
"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_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_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_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_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_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_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_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_GENERIC": "वीडियो लोड करते समय एक त्रुटि हुई।",
|
||||||
"MEDIA_ERR_NETWORK": "नेटवर्क त्रुटि के कारण वीडियो डाउनलोड बीच में ही विफल हो गया।",
|
"MEDIA_ERR_NETWORK": "नेटवर्क त्रुटि के कारण वीडियो डाउनलोड बीच में ही विफल हो गया।",
|
||||||
"MEDIA_ERR_DECODE": "वीडियो प्लेबैक को एक भ्रष्टाचार समस्या या क्योंकि वीडियो ने आपके ब्राउज़र द्वारा समर्थित नहीं की गई सुविधाओं का उपयोग किया था, के कारण रोक दिया गया था।",
|
"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_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_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_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_GENERIC": "ビデオの読み込み中にエラーが発生しました。",
|
||||||
"MEDIA_ERR_NETWORK": "ネットワークエラーにより、ビデオのダウンロードが途中で失敗しました。",
|
"MEDIA_ERR_NETWORK": "ネットワークエラーにより、ビデオのダウンロードが途中で失敗しました。",
|
||||||
"MEDIA_ERR_DECODE": "破損の問題またはビデオがブラウザでサポートされていない機能を使用したために、ビデオの再生が中止されました。",
|
"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_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_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_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_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_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_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 room: Room | undefined;
|
||||||
private speaking: boolean = false;
|
private speaking: boolean = false;
|
||||||
private customVideoTrack: Partial<ParticipantTrackPublication>;
|
private customVideoTrack: Partial<ParticipantTrackPublication>;
|
||||||
|
private _hasEncryptionError: boolean = false;
|
||||||
|
|
||||||
constructor(props: ParticipantProperties) {
|
constructor(props: ParticipantProperties) {
|
||||||
this.participant = props.participant;
|
this.participant = props.participant;
|
||||||
|
|
@ -550,4 +551,22 @@ export class ParticipantModel {
|
||||||
setMutedForcibly(muted: boolean) {
|
setMutedForcibly(muted: boolean) {
|
||||||
this.tracks.forEach((track) => (track.isMutedForcibly = muted));
|
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 { AdminDashboardComponent } from './admin/admin-dashboard/admin-dashboard.component';
|
||||||
import { AdminLoginComponent } from './admin/admin-login/admin-login.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 { CaptionsComponent } from './components/captions/captions.component';
|
||||||
import { ProFeatureDialogTemplateComponent } from './components/dialogs/pro-feature-dialog.component';
|
import { ProFeatureDialogTemplateComponent } from './components/dialogs/pro-feature-dialog.component';
|
||||||
import { ActivitiesPanelComponent } from './components/panel/activities-panel/activities-panel.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 { AppMaterialModule } from './openvidu-components-angular.material.module';
|
||||||
import { ThemeSelectorComponent } from './components/settings/theme-selector/theme-selector.component';
|
import { ThemeSelectorComponent } from './components/settings/theme-selector/theme-selector.component';
|
||||||
import { LandscapeWarningComponent } from './components/landscape-warning/landscape-warning.component';
|
import { LandscapeWarningComponent } from './components/landscape-warning/landscape-warning.component';
|
||||||
|
import { VideoPosterComponent } from './components/video-poster/video-poster.component';
|
||||||
|
|
||||||
const publicComponents = [
|
const publicComponents = [
|
||||||
AdminDashboardComponent,
|
AdminDashboardComponent,
|
||||||
|
|
@ -74,7 +74,7 @@ const privateComponents = [
|
||||||
ProFeatureDialogTemplateComponent,
|
ProFeatureDialogTemplateComponent,
|
||||||
RecordingDialogComponent,
|
RecordingDialogComponent,
|
||||||
DeleteDialogComponent,
|
DeleteDialogComponent,
|
||||||
AvatarProfileComponent,
|
VideoPosterComponent,
|
||||||
MediaElementComponent,
|
MediaElementComponent,
|
||||||
VideoDevicesComponent,
|
VideoDevicesComponent,
|
||||||
AudioDevicesComponent,
|
AudioDevicesComponent,
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@ interface GeneralConfig {
|
||||||
showDisconnectionDialog: boolean;
|
showDisconnectionDialog: boolean;
|
||||||
showThemeSelector: boolean;
|
showThemeSelector: boolean;
|
||||||
recordingStreamBaseUrl: string;
|
recordingStreamBaseUrl: string;
|
||||||
|
e2eeKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -302,7 +303,8 @@ export class OpenViduComponentsConfigService {
|
||||||
prejoinDisplayParticipantName: true,
|
prejoinDisplayParticipantName: true,
|
||||||
showDisconnectionDialog: true,
|
showDisconnectionDialog: true,
|
||||||
showThemeSelector: false,
|
showThemeSelector: false,
|
||||||
recordingStreamBaseUrl: 'call/api/recordings'
|
recordingStreamBaseUrl: 'call/api/recordings',
|
||||||
|
e2eeKey: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
private toolbarConfig = this.createToolbarConfigItem({
|
private toolbarConfig = this.createToolbarConfigItem({
|
||||||
|
|
@ -413,6 +415,11 @@ export class OpenViduComponentsConfigService {
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
shareReplay(1)
|
shareReplay(1)
|
||||||
);
|
);
|
||||||
|
e2eeKey$: Observable<string | undefined> = this.generalConfig.observable$.pipe(
|
||||||
|
map((config) => config.e2eeKey),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
shareReplay(1)
|
||||||
|
);
|
||||||
|
|
||||||
// Stream observables
|
// Stream observables
|
||||||
videoEnabled$: Observable<boolean> = this.streamConfig.observable$.pipe(map((config) => config.videoEnabled));
|
videoEnabled$: Observable<boolean> = this.streamConfig.observable$.pipe(map((config) => config.videoEnabled));
|
||||||
|
|
@ -565,6 +572,10 @@ export class OpenViduComponentsConfigService {
|
||||||
return baseUrl;
|
return baseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getE2EEKey(): string | undefined {
|
||||||
|
return this.generalConfig.subject.getValue().e2eeKey;
|
||||||
|
}
|
||||||
|
|
||||||
// Stream configuration methods
|
// Stream configuration methods
|
||||||
|
|
||||||
isVideoEnabled(): boolean {
|
isVideoEnabled(): boolean {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
AudioCaptureOptions,
|
AudioCaptureOptions,
|
||||||
ConnectionState,
|
ConnectionState,
|
||||||
CreateLocalTracksOptions,
|
CreateLocalTracksOptions,
|
||||||
|
ExternalE2EEKeyProvider,
|
||||||
LocalAudioTrack,
|
LocalAudioTrack,
|
||||||
LocalTrack,
|
LocalTrack,
|
||||||
LocalVideoTrack,
|
LocalVideoTrack,
|
||||||
|
|
@ -17,6 +18,7 @@ import {
|
||||||
createLocalTracks
|
createLocalTracks
|
||||||
} from 'livekit-client';
|
} from 'livekit-client';
|
||||||
import { StorageService } from '../storage/storage.service';
|
import { StorageService } from '../storage/storage.service';
|
||||||
|
import { OpenViduComponentsConfigService } from '../config/directive-config.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
|
|
@ -31,6 +33,7 @@ export class OpenViduService {
|
||||||
// private _isSttReady: BehaviorSubject<boolean> = new BehaviorSubject(true);
|
// private _isSttReady: BehaviorSubject<boolean> = new BehaviorSubject(true);
|
||||||
|
|
||||||
private room: Room;
|
private room: Room;
|
||||||
|
private keyProvider: ExternalE2EEKeyProvider | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
|
|
@ -53,7 +56,8 @@ export class OpenViduService {
|
||||||
constructor(
|
constructor(
|
||||||
private loggerSrv: LoggerService,
|
private loggerSrv: LoggerService,
|
||||||
private deviceService: DeviceService,
|
private deviceService: DeviceService,
|
||||||
private storageService: StorageService
|
private storageService: StorageService,
|
||||||
|
private configService: OpenViduComponentsConfigService
|
||||||
) {
|
) {
|
||||||
this.log = this.loggerSrv.get('OpenViduService');
|
this.log = this.loggerSrv.get('OpenViduService');
|
||||||
// this.isSttReadyObs = this._isSttReady.asObservable();
|
// this.isSttReadyObs = this._isSttReady.asObservable();
|
||||||
|
|
@ -64,14 +68,25 @@ export class OpenViduService {
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
initRoom(): void {
|
initRoom(): void {
|
||||||
// If room already exists, don't recreate it
|
// Check if E2EE configuration needs to be applied
|
||||||
if (this.room) {
|
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');
|
this.log.d('Room already initialized, skipping re-initialization');
|
||||||
return;
|
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 videoDeviceId = this.deviceService.getCameraSelected()?.device ?? undefined;
|
||||||
const audioDeviceId = this.deviceService.getMicrophoneSelected()?.device ?? undefined;
|
const audioDeviceId = this.deviceService.getMicrophoneSelected()?.device ?? undefined;
|
||||||
|
|
||||||
const roomOptions: RoomOptions = {
|
const roomOptions: RoomOptions = {
|
||||||
adaptiveStream: true,
|
adaptiveStream: true,
|
||||||
dynacast: true,
|
dynacast: true,
|
||||||
|
|
@ -93,15 +108,35 @@ export class OpenViduService {
|
||||||
stopLocalTrackOnUnpublish: true,
|
stopLocalTrackOnUnpublish: true,
|
||||||
disconnectOnPageLeave: 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.room = new Room(roomOptions);
|
||||||
this.log.d('Room initialized successfully');
|
this.log.d('Room initialized successfully');
|
||||||
}
|
} /**
|
||||||
|
|
||||||
/**
|
|
||||||
* Connects local participant to the room
|
* Connects local participant to the room
|
||||||
*/
|
*/
|
||||||
async connectRoom(): Promise<void> {
|
async connectRoom(): Promise<void> {
|
||||||
try {
|
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);
|
await this.room.connect(this.livekitUrl, this.livekitToken);
|
||||||
this.log.d(`Successfully connected to room ${this.room.name}`);
|
this.log.d(`Successfully connected to room ${this.room.name}`);
|
||||||
const participantName = this.storageService.getParticipantName();
|
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.
|
* Returns the local participant name.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue