Files
Toju/toju-app/src/app/domains/screen-share/feature/screen-share-viewer/screen-share-viewer.component.ts
Myx ee293d7daf
Some checks failed
Deploy Web Apps / deploy (push) Successful in 5m52s
Build Android APK / build-android-apk (push) Failing after 23m15s
Queue Release Build / prepare (push) Successful in 1m42s
Queue Release Build / build-linux (push) Failing after 9m33s
Queue Release Build / build-windows (push) Successful in 26m5s
Queue Release Build / finalize (push) Has been skipped
feat: Rename to Toju and add translation
2026-06-05 17:17:29 +02:00

296 lines
9.3 KiB
TypeScript

/* eslint-disable @typescript-eslint/member-ordering, @typescript-eslint/no-unused-vars */
import {
Component,
inject,
signal,
ElementRef,
ViewChild,
OnDestroy,
effect
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { Store } from '@ngrx/store';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { Subscription } from 'rxjs';
import {
lucideMaximize,
lucideMinimize,
lucideX,
lucideMonitor
} from '@ng-icons/lucide';
import { ScreenShareFacade } from '../../application/facades/screen-share.facade';
import { selectOnlineUsers } from '../../../../store/users/users.selectors';
import { User } from '../../../../shared-kernel';
import { DEFAULT_VOLUME } from '../../../../core/constants';
import { VoicePlaybackService } from '../../../../domains/voice-connection';
import { APP_TRANSLATE_IMPORTS, AppI18nService } from '../../../../core/i18n';
@Component({
selector: 'app-screen-share-viewer',
standalone: true,
imports: [
CommonModule,
NgIcon,
...APP_TRANSLATE_IMPORTS
],
viewProviders: [
provideIcons({
lucideMaximize,
lucideMinimize,
lucideX,
lucideMonitor
})
],
templateUrl: './screen-share-viewer.component.html'
})
/**
* Displays a local or remote screen-share stream in a video player.
* Supports fullscreen toggling, volume control, and viewer focus events.
*/
export class ScreenShareViewerComponent implements OnDestroy {
@ViewChild('screenVideo') videoRef!: ElementRef<HTMLVideoElement>;
private readonly screenShareService = inject(ScreenShareFacade);
private readonly voicePlayback = inject(VoicePlaybackService);
private readonly store = inject(Store);
private readonly appI18n = inject(AppI18nService);
private remoteStreamSub: Subscription | null = null;
onlineUsers = this.store.selectSignal(selectOnlineUsers);
activeScreenSharer = signal<User | null>(null);
// Track the userId we're currently watching (for detecting when they stop sharing)
private watchingUserId = signal<string | null>(null);
isFullscreen = signal(false);
hasStream = signal(false);
isLocalShare = signal(false);
screenVolume = signal(DEFAULT_VOLUME);
private streamSubscription: (() => void) | null = null;
private viewerFocusHandler = (evt: CustomEvent<{ userId: string }>) => {
try {
const userId = evt.detail?.userId;
if (!userId)
return;
const stream = this.screenShareService.getRemoteScreenShareStream(userId);
const user = this.onlineUsers().find((onlineUser) => onlineUser.id === userId || onlineUser.oderId === userId) || null;
if (stream && stream.getVideoTracks().length > 0) {
if (user) {
this.setRemoteStream(stream, user);
} else if (this.videoRef) {
this.videoRef.nativeElement.srcObject = stream;
this.videoRef.nativeElement.volume = 0;
this.videoRef.nativeElement.muted = true;
this.hasStream.set(true);
this.activeScreenSharer.set(null);
this.watchingUserId.set(userId);
this.screenVolume.set(this.voicePlayback.getUserVolume(userId));
this.isLocalShare.set(false);
}
}
} catch (_error) {
// Failed to focus viewer on user stream
}
};
constructor() {
// React to screen share stream changes
effect(() => {
const screenStream = this.screenShareService.screenStream();
if (screenStream && this.videoRef) {
// Local share: always mute to avoid audio feedback
this.videoRef.nativeElement.srcObject = screenStream;
this.videoRef.nativeElement.volume = 0;
this.videoRef.nativeElement.muted = true;
this.isLocalShare.set(true);
this.hasStream.set(true);
} else if (this.videoRef) {
this.videoRef.nativeElement.srcObject = null;
this.isLocalShare.set(false);
this.hasStream.set(false);
}
});
// Watch for when the user we're watching stops sharing
effect(() => {
const watchingId = this.watchingUserId();
const isWatchingRemote = this.hasStream() && !this.isLocalShare();
// Only check if we're actually watching a remote stream
if (!watchingId || !isWatchingRemote)
return;
const users = this.onlineUsers();
const watchedUser = users.find(user => user.id === watchingId || user.oderId === watchingId);
// If the user is no longer sharing (screenShareState.isSharing is false), stop watching
if (watchedUser && watchedUser.screenShareState?.isSharing === false) {
this.stopWatching();
return;
}
// Also check if the stream's video tracks are still available
const stream = this.screenShareService.getRemoteScreenShareStream(watchingId);
const hasActiveVideo = stream?.getVideoTracks().some(track => track.readyState === 'live');
if (!hasActiveVideo) {
// Stream or video tracks are gone - stop watching
this.stopWatching();
}
});
// Subscribe to remote streams with video (screen shares)
// NOTE: We no longer auto-display remote streams. Users must click "Live" to view.
// This subscription is kept for potential future use (e.g., tracking available streams)
this.remoteStreamSub = this.screenShareService.onRemoteStream.subscribe(({ peerId }) => {
if (peerId !== this.watchingUserId() || this.isLocalShare()) {
return;
}
const stream = this.screenShareService.getRemoteScreenShareStream(peerId);
const hasActiveVideo = stream?.getVideoTracks().some((track) => track.readyState === 'live') ?? false;
if (!hasActiveVideo) {
this.stopWatching();
}
});
// Listen for focus events dispatched by other components
window.addEventListener('viewer:focus', this.viewerFocusHandler as EventListener);
}
ngOnDestroy(): void {
if (this.isFullscreen()) {
this.exitFullscreen();
}
// Cleanup subscription
this.remoteStreamSub?.unsubscribe();
// Remove event listener
window.removeEventListener('viewer:focus', this.viewerFocusHandler as EventListener);
}
/** Toggle between fullscreen and windowed display. */
toggleFullscreen(): void {
if (this.isFullscreen()) {
this.exitFullscreen();
} else {
this.enterFullscreen();
}
}
/** Enter fullscreen mode, requesting browser fullscreen if available. */
enterFullscreen(): void {
this.isFullscreen.set(true);
// Request browser fullscreen if available
if (this.videoRef?.nativeElement.requestFullscreen) {
this.videoRef.nativeElement.requestFullscreen().catch(() => {
// Fallback to CSS fullscreen
});
}
}
/** Exit fullscreen mode. */
exitFullscreen(): void {
this.isFullscreen.set(false);
if (document.fullscreenElement) {
document.exitFullscreen().catch(() => {});
}
}
/** Stop the local screen share and reset viewer state. */
stopSharing(): void {
this.screenShareService.stopScreenShare();
this.activeScreenSharer.set(null);
this.hasStream.set(false);
this.isLocalShare.set(false);
}
/** Stop watching a remote stream and reset the viewer. */
// Stop watching a remote stream (for viewers)
stopWatching(): void {
if (this.videoRef) {
this.videoRef.nativeElement.srcObject = null;
}
this.activeScreenSharer.set(null);
this.watchingUserId.set(null);
this.hasStream.set(false);
this.isLocalShare.set(false);
if (this.isFullscreen()) {
this.exitFullscreen();
}
}
/** Attach and play a remote peer's screen-share stream. */
// Called by parent when a remote peer starts sharing
setRemoteStream(stream: MediaStream, user: User): void {
this.activeScreenSharer.set(user);
this.watchingUserId.set(user.id || user.oderId || null);
this.isLocalShare.set(false);
this.screenVolume.set(this.voicePlayback.getUserVolume(user.id || user.oderId || ''));
if (this.videoRef) {
const el = this.videoRef.nativeElement;
el.srcObject = stream;
// Keep the viewer muted so screen-share audio only plays once via VoicePlaybackService.
el.muted = true;
el.volume = 0;
el.play().catch(() => {});
this.hasStream.set(true);
}
}
/** Attach and play the local user's screen-share stream (always muted). */
// Called when local user starts sharing
setLocalStream(stream: MediaStream, user: User): void {
this.activeScreenSharer.set(user);
this.isLocalShare.set(true);
if (this.videoRef) {
this.videoRef.nativeElement.srcObject = stream;
// Always mute local share playback
this.videoRef.nativeElement.volume = 0;
this.videoRef.nativeElement.muted = true;
this.hasStream.set(true);
}
}
/** Handle volume slider changes, applying only to remote streams. */
sharingLabel(): string {
const sharer = this.activeScreenSharer();
if (sharer?.displayName) {
return this.appI18n.instant('screenShare.viewer.userSharing', { name: sharer.displayName });
}
return this.appI18n.instant('screenShare.viewer.someoneSharing');
}
onScreenVolumeChange(event: Event): void {
const input = event.target as HTMLInputElement;
const val = Math.max(0, Math.min(200, parseInt(input.value, 10)));
this.screenVolume.set(val);
if (!this.isLocalShare()) {
const userId = this.watchingUserId();
if (userId) {
this.voicePlayback.setUserVolume(userId, val);
}
}
}
}