/* 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; 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(null); // Track the userId we're currently watching (for detecting when they stop sharing) private watchingUserId = signal(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); } } } }