Move toju-app into own its folder
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
/* 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/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/application/voice-playback.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-screen-share-viewer',
|
||||
standalone: true,
|
||||
imports: [CommonModule, NgIcon],
|
||||
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 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. */
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user