mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-01 20:42:50 +08:00
feat: implement seeking (FFmpeg -ss restart), fix lyrics sync with seekOffset
- AudioPlayer.seek() restarts FFmpeg with -ss parameter for actual seeking - New POST /api/player/:botId/seek endpoint - BotStatus includes seekOffset and playStartTime from server - Frontend elapsed = seekOffset + (now - playStartedAt) - Progress bar click now seeks to clicked position - All timing actions use _resetTiming() helper with seekOffset - Lyrics sync uses store.elapsed which includes seekOffset Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
080c2836c9
commit
1cdface33c
5 files changed
+101
-50
No files matched your search
+36
-16
@@ -22,6 +22,8 @@ export class AudioPlayer extends EventEmitter {
|
||||
private logger: Logger;
|
||||
private frameLoopRunning = false;
|
||||
private nextFrameTime = 0;
|
||||
private currentUrl = "";
|
||||
private seekOffset = 0; // seconds offset from seek
|
||||
|
||||
constructor(logger: Logger) {
|
||||
super();
|
||||
@@ -29,27 +31,32 @@ export class AudioPlayer extends EventEmitter {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
play(url: string): void {
|
||||
play(url: string, seekSeconds = 0): void {
|
||||
this.stop();
|
||||
this.currentUrl = url;
|
||||
this.seekOffset = seekSeconds;
|
||||
|
||||
this.logger.info({ url: url.slice(0, 80) }, "Starting playback");
|
||||
this.logger.info({ url: url.slice(0, 80), seek: seekSeconds }, "Starting playback");
|
||||
|
||||
this.ffmpeg = spawn(
|
||||
"ffmpeg",
|
||||
[
|
||||
"-reconnect", "1",
|
||||
"-reconnect_streamed", "1",
|
||||
"-reconnect_delay_max", "5",
|
||||
"-i", url,
|
||||
"-f", "s16le",
|
||||
"-ar", "48000",
|
||||
"-ac", "2",
|
||||
"-acodec", "pcm_s16le",
|
||||
"-",
|
||||
],
|
||||
{ stdio: ["ignore", "pipe", "pipe"] }
|
||||
const args = [
|
||||
"-reconnect", "1",
|
||||
"-reconnect_streamed", "1",
|
||||
"-reconnect_delay_max", "5",
|
||||
];
|
||||
if (seekSeconds > 0) {
|
||||
args.push("-ss", String(seekSeconds));
|
||||
}
|
||||
args.push(
|
||||
"-i", url,
|
||||
"-f", "s16le",
|
||||
"-ar", "48000",
|
||||
"-ac", "2",
|
||||
"-acodec", "pcm_s16le",
|
||||
"-",
|
||||
);
|
||||
|
||||
this.ffmpeg = spawn("ffmpeg", args, { stdio: ["ignore", "pipe", "pipe"] });
|
||||
|
||||
this.ffmpeg.stderr!.on("data", () => {
|
||||
// Suppress FFmpeg stderr output
|
||||
});
|
||||
@@ -159,6 +166,17 @@ export class AudioPlayer extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/** Seek to a position in seconds. Restarts FFmpeg with -ss offset. */
|
||||
seek(seconds: number): void {
|
||||
if (!this.currentUrl) return;
|
||||
this.logger.info({ seek: seconds }, "Seeking");
|
||||
this.play(this.currentUrl, seconds);
|
||||
}
|
||||
|
||||
getSeekOffset(): number {
|
||||
return this.seekOffset;
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.frameLoopRunning = false;
|
||||
if (this.ffmpeg) {
|
||||
@@ -167,6 +185,8 @@ export class AudioPlayer extends EventEmitter {
|
||||
}
|
||||
this.pcmBuffer = Buffer.alloc(0);
|
||||
this.state = "idle";
|
||||
this.currentUrl = "";
|
||||
this.seekOffset = 0;
|
||||
}
|
||||
|
||||
setVolume(vol: number): void {
|
||||
|
||||
@@ -37,6 +37,8 @@ export interface BotStatus {
|
||||
queueSize: number;
|
||||
volume: number;
|
||||
playMode: PlayMode;
|
||||
seekOffset: number; // seconds offset from last seek/play start
|
||||
playStartTime: number; // server timestamp when playback started
|
||||
}
|
||||
|
||||
export class BotInstance extends EventEmitter {
|
||||
@@ -505,6 +507,8 @@ export class BotInstance extends EventEmitter {
|
||||
queueSize: this.queue.size(),
|
||||
volume: this.player.getVolume(),
|
||||
playMode: this.queue.getMode(),
|
||||
seekOffset: this.player.getSeekOffset(),
|
||||
playStartTime: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -104,6 +104,22 @@ export function createPlayerRouter(
|
||||
}
|
||||
});
|
||||
|
||||
// Seek to position
|
||||
router.post("/:botId/seek", async (req, res) => {
|
||||
try {
|
||||
const bot = (req as any).bot;
|
||||
const { position } = req.body; // seconds
|
||||
if (typeof position !== "number" || position < 0) {
|
||||
res.status(400).json({ error: "position (seconds) is required" });
|
||||
return;
|
||||
}
|
||||
bot.getPlayer().seek(position);
|
||||
res.json({ message: `Seeked to ${Math.floor(position)}s`, seekOffset: position });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: (err as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/:botId/queue", (req, res) => {
|
||||
const bot = (req as any).bot;
|
||||
res.json({ queue: bot.getQueue(), status: bot.getStatus() });
|
||||
|
||||
Reference in new issue
Block a user