diff --git a/src/core/player.ts b/src/core/player.ts index eddf3a7..e71c922 100644 --- a/src/core/player.ts +++ b/src/core/player.ts @@ -66,8 +66,8 @@ export class GuildPlayer extends EventEmitter { current: Track | null = null; volume = config.DEFAULT_VOLUME; loop: LoopMode = "off"; - shuffleUsed = false; - /** Per-server switch for publishing the clip; the env flag gates it too. */ + shuffleUsed = false; + /** Per-server switch for publishing the clip; the env flag gates it too. */ videoEnabled = false; private status: PlayerStatus = "idle"; @@ -115,7 +115,7 @@ export class GuildPlayer extends EventEmitter { history: this.history.slice(0, 10), volume: this.volume, loop: this.loop, - shuffleUsed: this.shuffleUsed, + shuffleUsed: this.shuffleUsed, videoEnabled: this.videoEnabled, updatedAt: Date.now(), }; @@ -262,16 +262,16 @@ export class GuildPlayer extends EventEmitter { * participant name, so nobody is ever dropped and the channel never looks * empty — the bot would sit in it forever. */ - /** Whether this user is in the channel the bot is sitting in, per LiveKit. */ - hasParticipant(userId: string): boolean { - const participants = this.connection?.room?.remoteParticipants; - if (!participants) return false; - for (const participant of participants.values()) { - if (participant.identity === userId) return true; - } - return false; - } - + /** Whether this user is in the channel the bot is sitting in, per LiveKit. */ + hasParticipant(userId: string): boolean { + const participants = this.connection?.room?.remoteParticipants; + if (!participants) return false; + for (const participant of participants.values()) { + if (participant.identity === userId) return true; + } + return false; + } + private listenersInChannel(): number { return this.connection?.room?.remoteParticipants?.size ?? 0; } @@ -379,14 +379,14 @@ export class GuildPlayer extends EventEmitter { private async startScreenShare(video: NonNullable): Promise { const room = this.connection?.room; if (!room) return; - const publisher = new VideoPublisher({ - width: video.width, - height: video.height, - fps: video.fps, - // Frames are released against the sound people have actually heard, not - // the sound handed to LiveKit: its audio source buffers up to a second, - // and gating on the queued position showed every frame that much early. - audioClock: () => this.heardSeconds(), + const publisher = new VideoPublisher({ + width: video.width, + height: video.height, + fps: video.fps, + // Frames are released against the sound people have actually heard, not + // the sound handed to LiveKit: its audio source buffers up to a second, + // and gating on the queued position showed every frame that much early. + audioClock: () => this.heardSeconds(), }); try { await publisher.start(room, video.stream, (reason) => { @@ -402,14 +402,14 @@ export class GuildPlayer extends EventEmitter { } } - /** Playback position as heard, i.e. minus whatever is still queued in LiveKit. */ - private heardSeconds(): number { - const media = this.media; - if (!media) return 0; - const queuedMs = media.source?.queuedDuration ?? 0; - return Math.max(0, media.seconds - queuedMs / 1000); - } - + /** Playback position as heard, i.e. minus whatever is still queued in LiveKit. */ + private heardSeconds(): number { + const media = this.media; + if (!media) return 0; + const queuedMs = media.source?.queuedDuration ?? 0; + return Math.max(0, media.seconds - queuedMs / 1000); + } + private stopScreenShare(): void { const publisher = this.videoPublisher; this.videoPublisher = null; @@ -428,6 +428,9 @@ export class GuildPlayer extends EventEmitter { private async advance(skipLoop: boolean): Promise { const finished = this.current; this.current = null; + // The track has to go with the sound: a published video track keeps showing + // its last frame, so the clip would hang on screen after the music stopped. + this.stopScreenShare(); this.currentInput?.cleanup(); this.currentInput = null; this.seekOffset = 0;