mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
fix: server-side elapsed tracking via frame count, periodic sync, playlist respects play mode
Progress/lyrics sync: - AudioPlayer tracks framesPlayed (ground truth: frames * 20ms + seekOffset) - BotStatus.elapsed is the real elapsed from server - Frontend polls GET /elapsed every 3s for ground truth - Client interpolates between syncs (serverElapsed + timeSinceSync) - Pause freezes at interpolated value, resume resyncs from server - No more client-side timestamp drift Playlist play all: - Stops current playback before loading - Respects play mode: random/rloop picks random first song, seq plays first Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
1cdface33c
commit
0439bd047f
6 files changed
+131
-109
No files matched your search
+72
-53
@@ -21,8 +21,7 @@ export interface BotStatus {
|
||||
queueSize: number;
|
||||
volume: number;
|
||||
playMode: string;
|
||||
seekOffset?: number;
|
||||
playStartTime?: number;
|
||||
elapsed?: number;
|
||||
}
|
||||
|
||||
export interface PlaylistItem {
|
||||
@@ -33,7 +32,7 @@ export interface PlaylistItem {
|
||||
platform: string;
|
||||
}
|
||||
|
||||
const HOME_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
||||
const HOME_CACHE_TTL = 5 * 60 * 1000;
|
||||
|
||||
export const usePlayerStore = defineStore('player', {
|
||||
state: () => ({
|
||||
@@ -41,10 +40,13 @@ export const usePlayerStore = defineStore('player', {
|
||||
activeBotId: null as string | null,
|
||||
queue: [] as Song[],
|
||||
theme: 'dark' as 'dark' | 'light',
|
||||
playStartedAt: 0, // client timestamp when playback started
|
||||
seekOffset: 0, // server seek offset in seconds
|
||||
pausedElapsed: 0, // elapsed seconds when paused
|
||||
// Home page cached data
|
||||
|
||||
// Server-synced elapsed time (ground truth)
|
||||
serverElapsed: 0, // last known server elapsed (seconds)
|
||||
serverSyncTime: 0, // Date.now() when we got serverElapsed
|
||||
wasPlaying: false, // was playing at last sync
|
||||
|
||||
// Home page cache
|
||||
recommendPlaylists: [] as PlaylistItem[],
|
||||
dailySongs: [] as Song[],
|
||||
userPlaylists: [] as PlaylistItem[],
|
||||
@@ -64,11 +66,12 @@ export const usePlayerStore = defineStore('player', {
|
||||
isPaused(): boolean {
|
||||
return this.activeBot?.paused ?? false;
|
||||
},
|
||||
/** Interpolated elapsed: serverElapsed + time since last sync (if playing) */
|
||||
elapsed(): number {
|
||||
if (!this.activeBot?.currentSong) return 0;
|
||||
if (this.isPaused) return this.pausedElapsed;
|
||||
if (!this.isPlaying || this.playStartedAt === 0) return 0;
|
||||
return this.seekOffset + (Date.now() - this.playStartedAt) / 1000;
|
||||
if (!this.wasPlaying || this.serverSyncTime === 0) return this.serverElapsed;
|
||||
if (this.isPaused) return this.serverElapsed;
|
||||
return this.serverElapsed + (Date.now() - this.serverSyncTime) / 1000;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -80,9 +83,6 @@ export const usePlayerStore = defineStore('player', {
|
||||
updateBotStatus(botId: string, status: BotStatus) {
|
||||
const prev = this.bots.find((b) => b.id === botId);
|
||||
const prevSongId = prev?.currentSong?.id;
|
||||
const prevPaused = prev?.paused ?? false;
|
||||
const prevPlaying = prev?.playing ?? false;
|
||||
const newSongId = status.currentSong?.id;
|
||||
|
||||
const index = this.bots.findIndex((b) => b.id === botId);
|
||||
if (index >= 0) {
|
||||
@@ -93,28 +93,18 @@ export const usePlayerStore = defineStore('player', {
|
||||
|
||||
if (botId !== (this.activeBotId ?? this.bots[0]?.id)) return;
|
||||
|
||||
// Use server-provided seekOffset
|
||||
const serverSeek = status.seekOffset ?? 0;
|
||||
|
||||
// Song changed or seek offset changed — reset timer
|
||||
if ((newSongId && newSongId !== prevSongId) || serverSeek !== this.seekOffset) {
|
||||
this.seekOffset = serverSeek;
|
||||
this.playStartedAt = Date.now();
|
||||
this.pausedElapsed = 0;
|
||||
return;
|
||||
// Sync elapsed from server status
|
||||
if (status.elapsed !== undefined) {
|
||||
this.serverElapsed = status.elapsed;
|
||||
this.serverSyncTime = Date.now();
|
||||
this.wasPlaying = status.playing && !status.paused;
|
||||
}
|
||||
|
||||
// Resumed
|
||||
if (status.playing && !status.paused && prevPaused) {
|
||||
this.playStartedAt = Date.now() - this.pausedElapsed * 1000;
|
||||
return;
|
||||
}
|
||||
|
||||
// Paused
|
||||
if (status.paused && prevPlaying && !prevPaused) {
|
||||
this.pausedElapsed = this.playStartedAt > 0
|
||||
? this.seekOffset + (Date.now() - this.playStartedAt) / 1000
|
||||
: this.seekOffset;
|
||||
// Song changed — reset
|
||||
if (status.currentSong?.id !== prevSongId) {
|
||||
this.serverElapsed = status.elapsed ?? 0;
|
||||
this.serverSyncTime = Date.now();
|
||||
this.wasPlaying = status.playing && !status.paused;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -142,10 +132,25 @@ export const usePlayerStore = defineStore('player', {
|
||||
if (!this.activeBotId && this.bots.length > 0) {
|
||||
this.activeBotId = this.bots[0].id;
|
||||
}
|
||||
// If a song is playing, set the start time
|
||||
// Sync elapsed from first status
|
||||
const bot = this.activeBot;
|
||||
if (bot?.playing && bot.currentSong && this.playStartedAt === 0) {
|
||||
this.playStartedAt = Date.now();
|
||||
if (bot?.elapsed !== undefined) {
|
||||
this.serverElapsed = bot.elapsed;
|
||||
this.serverSyncTime = Date.now();
|
||||
this.wasPlaying = bot.playing && !bot.paused;
|
||||
}
|
||||
},
|
||||
|
||||
/** Poll server for real elapsed time */
|
||||
async syncElapsed() {
|
||||
if (!this.activeBotId || !this.isPlaying) return;
|
||||
try {
|
||||
const res = await axios.get(`/api/player/${this.activeBotId}/elapsed`);
|
||||
this.serverElapsed = res.data.elapsed;
|
||||
this.serverSyncTime = Date.now();
|
||||
this.wasPlaying = true;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
|
||||
@@ -159,22 +164,25 @@ export const usePlayerStore = defineStore('player', {
|
||||
}
|
||||
},
|
||||
|
||||
_resetTiming(seekSec = 0) {
|
||||
this.seekOffset = seekSec;
|
||||
this.playStartedAt = Date.now();
|
||||
this.pausedElapsed = 0;
|
||||
_syncAfterAction() {
|
||||
this.serverSyncTime = Date.now();
|
||||
this.wasPlaying = true;
|
||||
// Sync from server after a short delay for accuracy
|
||||
setTimeout(() => this.syncElapsed(), 500);
|
||||
},
|
||||
|
||||
async play(query: string, platform = 'netease') {
|
||||
if (!this.activeBotId) return;
|
||||
await axios.post(`/api/player/${this.activeBotId}/play`, { query, platform });
|
||||
this._resetTiming();
|
||||
this.serverElapsed = 0;
|
||||
this._syncAfterAction();
|
||||
},
|
||||
|
||||
async playById(songId: string, platform = 'netease') {
|
||||
if (!this.activeBotId) return;
|
||||
await axios.post(`/api/player/${this.activeBotId}/play-by-id`, { songId, platform });
|
||||
this._resetTiming();
|
||||
this.serverElapsed = 0;
|
||||
this._syncAfterAction();
|
||||
},
|
||||
|
||||
async addToQueue(query: string, platform = 'netease') {
|
||||
@@ -190,41 +198,53 @@ export const usePlayerStore = defineStore('player', {
|
||||
async playPlaylist(playlistId: string, platform = 'netease') {
|
||||
if (!this.activeBotId) return;
|
||||
await axios.post(`/api/player/${this.activeBotId}/play-playlist`, { playlistId, platform });
|
||||
this._resetTiming();
|
||||
this.serverElapsed = 0;
|
||||
this._syncAfterAction();
|
||||
},
|
||||
|
||||
async pause() {
|
||||
if (!this.activeBotId) return;
|
||||
// Save current elapsed including seekOffset
|
||||
this.pausedElapsed = this.elapsed;
|
||||
// Freeze elapsed at current interpolated value
|
||||
this.serverElapsed = this.elapsed;
|
||||
this.wasPlaying = false;
|
||||
await axios.post(`/api/player/${this.activeBotId}/pause`);
|
||||
},
|
||||
|
||||
async resume() {
|
||||
if (!this.activeBotId) return;
|
||||
// Restore: playStartedAt = now - (pausedElapsed - seekOffset)
|
||||
this.playStartedAt = Date.now() - (this.pausedElapsed - this.seekOffset) * 1000;
|
||||
await axios.post(`/api/player/${this.activeBotId}/resume`);
|
||||
this.serverSyncTime = Date.now();
|
||||
this.wasPlaying = true;
|
||||
setTimeout(() => this.syncElapsed(), 300);
|
||||
},
|
||||
|
||||
async next() {
|
||||
if (!this.activeBotId) return;
|
||||
await axios.post(`/api/player/${this.activeBotId}/next`);
|
||||
this._resetTiming();
|
||||
this.serverElapsed = 0;
|
||||
this._syncAfterAction();
|
||||
},
|
||||
|
||||
async prev() {
|
||||
if (!this.activeBotId) return;
|
||||
await axios.post(`/api/player/${this.activeBotId}/prev`);
|
||||
this._resetTiming();
|
||||
this.serverElapsed = 0;
|
||||
this._syncAfterAction();
|
||||
},
|
||||
|
||||
async stop() {
|
||||
if (!this.activeBotId) return;
|
||||
await axios.post(`/api/player/${this.activeBotId}/stop`);
|
||||
this.seekOffset = 0;
|
||||
this.playStartedAt = 0;
|
||||
this.pausedElapsed = 0;
|
||||
this.serverElapsed = 0;
|
||||
this.serverSyncTime = 0;
|
||||
this.wasPlaying = false;
|
||||
},
|
||||
|
||||
async seek(position: number) {
|
||||
if (!this.activeBotId) return;
|
||||
await axios.post(`/api/player/${this.activeBotId}/seek`, { position });
|
||||
this.serverElapsed = position;
|
||||
this._syncAfterAction();
|
||||
},
|
||||
|
||||
async setVolume(volume: number) {
|
||||
@@ -238,7 +258,6 @@ export const usePlayerStore = defineStore('player', {
|
||||
},
|
||||
|
||||
async fetchHomeData() {
|
||||
// Skip if data was fetched within the last 5 minutes
|
||||
if (this.lastFetchTime > 0 && Date.now() - this.lastFetchTime < HOME_CACHE_TTL) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user