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
296 lines
9.3 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
}
|
|
}
|