Add pause/resume

This commit is contained in:
Max Isom 2020-03-15 19:30:07 -05:00
parent e55acbb718
commit 2875c6ceb8
4 changed files with 123 additions and 26 deletions

29
src/commands/pause.ts Normal file
View file

@ -0,0 +1,29 @@
import {Message} from 'discord.js';
import {TYPES} from '../types';
import {inject, injectable} from 'inversify';
import PlayerManager from '../managers/player';
import {STATUS} from '../services/player';
import Command from '.';
@injectable()
export default class implements Command {
public name = 'pause';
public description = 'pause currently playing song';
private readonly playerManager: PlayerManager;
constructor(@inject(TYPES.Managers.Player) playerManager: PlayerManager) {
this.playerManager = playerManager;
}
public async execute(msg: Message, _: string []): Promise<void> {
const player = this.playerManager.get(msg.guild!.id);
if (player.status !== STATUS.PLAYING) {
await msg.channel.send('error: not currently playing');
return;
}
player.pause();
await msg.channel.send('paused');
}
}

View file

@ -36,6 +36,29 @@ export default class implements Command {
} }
public async execute(msg: Message, args: string []): Promise<void> { public async execute(msg: Message, args: string []): Promise<void> {
const queue = this.queueManager.get(msg.guild!.id);
if (args.length === 0) {
if (this.playerManager.get(msg.guild!.id).status === STATUS.PLAYING) {
await msg.channel.send('error: already playing, give me a song name');
return;
}
// Must be resuming play
if (queue.get().length === 0) {
await msg.channel.send('error: nothing to play');
return;
}
const channel = getMostPopularVoiceChannel(msg.guild!);
await this.playerManager.get(msg.guild!.id).connect(channel);
await this.playerManager.get(msg.guild!.id).play();
await msg.channel.send('play resuming');
return;
}
const newSongs: QueuedSong[] = []; const newSongs: QueuedSong[] = [];
const res = new LoadingMessage(msg.channel as TextChannel, 'hold on a sec'); const res = new LoadingMessage(msg.channel as TextChannel, 'hold on a sec');

View file

@ -24,6 +24,7 @@ import Command from './commands';
import Clear from './commands/clear'; import Clear from './commands/clear';
import Config from './commands/config'; import Config from './commands/config';
import ForwardSeek from './commands/fseek'; import ForwardSeek from './commands/fseek';
import Pause from './commands/pause';
import Play from './commands/play'; import Play from './commands/play';
import QueueCommad from './commands/queue'; import QueueCommad from './commands/queue';
import Seek from './commands/seek'; import Seek from './commands/seek';
@ -45,6 +46,7 @@ container.bind<QueueManager>(TYPES.Managers.Queue).to(QueueManager).inSingletonS
container.bind<Command>(TYPES.Command).to(Clear).inSingletonScope(); container.bind<Command>(TYPES.Command).to(Clear).inSingletonScope();
container.bind<Command>(TYPES.Command).to(Config).inSingletonScope(); container.bind<Command>(TYPES.Command).to(Config).inSingletonScope();
container.bind<Command>(TYPES.Command).to(ForwardSeek).inSingletonScope(); container.bind<Command>(TYPES.Command).to(ForwardSeek).inSingletonScope();
container.bind<Command>(TYPES.Command).to(Pause).inSingletonScope();
container.bind<Command>(TYPES.Command).to(Play).inSingletonScope(); container.bind<Command>(TYPES.Command).to(Play).inSingletonScope();
container.bind<Command>(TYPES.Command).to(QueueCommad).inSingletonScope(); container.bind<Command>(TYPES.Command).to(QueueCommad).inSingletonScope();
container.bind<Command>(TYPES.Command).to(Seek).inSingletonScope(); container.bind<Command>(TYPES.Command).to(Seek).inSingletonScope();

View file

@ -20,8 +20,8 @@ export default class {
private readonly cacheDir: string; private readonly cacheDir: string;
private voiceConnection: VoiceConnection | null = null; private voiceConnection: VoiceConnection | null = null;
private dispatcher: StreamDispatcher | null = null; private dispatcher: StreamDispatcher | null = null;
private playPositionInterval: NodeJS.Timeout | undefined;
private lastStreamTime = 0;
private positionInSeconds = 0; private positionInSeconds = 0;
constructor(queue: Queue, cacheDir: string) { constructor(queue: Queue, cacheDir: string) {
@ -37,6 +37,10 @@ export default class {
disconnect(): void { disconnect(): void {
if (this.voiceConnection) { if (this.voiceConnection) {
if (this.status === STATUS.PLAYING) {
this.pause();
}
this.voiceConnection.disconnect(); this.voiceConnection.disconnect();
} }
} }
@ -54,9 +58,12 @@ export default class {
await this.waitForCache(currentSong.url); await this.waitForCache(currentSong.url);
this.attachListeners(this.voiceConnection.play(this.getCachedPath(currentSong.url), {seek: positionSeconds})); this.dispatcher = this.voiceConnection.play(this.getCachedPath(currentSong.url), {seek: positionSeconds});
this.positionInSeconds = positionSeconds; this.attachListeners();
this.startTrackingPosition(positionSeconds);
this.status = STATUS.PLAYING;
} }
async forwardSeek(positionSeconds: number): Promise<void> { async forwardSeek(positionSeconds: number): Promise<void> {
@ -73,9 +80,14 @@ export default class {
} }
// Resume from paused state // Resume from paused state
if (this.status === STATUS.PAUSED && this.dispatcher) { if (this.status === STATUS.PAUSED) {
this.dispatcher.resume(); if (this.dispatcher) {
this.status = STATUS.PLAYING; this.dispatcher.resume();
this.status = STATUS.PLAYING;
} else {
await this.seek(this.getPosition());
}
return; return;
} }
@ -85,27 +97,32 @@ export default class {
throw new Error('Queue empty.'); throw new Error('Queue empty.');
} }
let dispatcher: StreamDispatcher;
if (await this.isCached(currentSong.url)) { if (await this.isCached(currentSong.url)) {
dispatcher = this.voiceConnection.play(this.getCachedPath(currentSong.url)); this.dispatcher = this.voiceConnection.play(this.getCachedPath(currentSong.url));
} else { } else {
const stream = await this.getStream(currentSong.url); const stream = await this.getStream(currentSong.url);
dispatcher = this.voiceConnection.play(stream, {type: 'webm/opus'}); this.dispatcher = this.voiceConnection.play(stream, {type: 'webm/opus'});
} }
this.attachListeners(dispatcher); this.attachListeners();
this.status = STATUS.PLAYING; this.status = STATUS.PLAYING;
this.dispatcher = dispatcher;
this.startTrackingPosition();
} }
pause(): void { pause(): void {
if (!this.dispatcher || this.status !== STATUS.PLAYING) { if (this.status !== STATUS.PLAYING) {
throw new Error('Not currently playing.'); throw new Error('Not currently playing.');
} }
this.dispatcher.pause(); this.status = STATUS.PAUSED;
if (this.dispatcher) {
this.dispatcher.pause();
}
this.stopTrackingPosition();
} }
private getCurrentSong(): QueuedSong|null { private getCurrentSong(): QueuedSong|null {
@ -235,12 +252,45 @@ export default class {
return capacitor.createReadStream(); return capacitor.createReadStream();
} }
private attachListeners(stream: StreamDispatcher): void { private startTrackingPosition(initalPosition?: number): void {
stream.on('speaking', async isSpeaking => { if (initalPosition) {
// Update position this.positionInSeconds = initalPosition;
this.positionInSeconds += (stream.streamTime - this.lastStreamTime) / 1000; }
this.lastStreamTime = stream.streamTime;
if (this.playPositionInterval) {
clearInterval(this.playPositionInterval);
}
this.playPositionInterval = setInterval(() => {
this.positionInSeconds++;
}, 1000);
}
private stopTrackingPosition(): void {
if (this.playPositionInterval) {
clearInterval(this.playPositionInterval);
}
}
private attachListeners(): void {
if (!this.voiceConnection) {
return;
}
this.voiceConnection.on('disconnect', () => {
// Automatically pause
if (this.status === STATUS.PLAYING) {
this.pause();
}
this.dispatcher = null;
});
if (!this.dispatcher) {
return;
}
this.dispatcher.on('speaking', async isSpeaking => {
// Automatically advance queued song at end // Automatically advance queued song at end
if (!isSpeaking && this.status === STATUS.PLAYING) { if (!isSpeaking && this.status === STATUS.PLAYING) {
if (this.queue.get().length > 0) { if (this.queue.get().length > 0) {
@ -249,12 +299,5 @@ export default class {
} }
} }
}); });
stream.on('close', () => {
// Remove dispatcher from guild player
this.dispatcher = null;
// TODO: set voiceConnection null as well?
});
} }
} }