Download packages/server/src/controllers/playerController.ts from QSLY/ghp: direct link, hf CLI and curl.
- Browser
- Download file 4.83 kB
-
https://huggingface.co/spaces/QSLY/ghp/resolve/main/packages/server/src/controllers/playerController.ts
- Command line
-
hf download hf://spaces/QSLY/ghp/packages/server/src/controllers/playerController.ts
-
curl -L -o playerController.ts https://huggingface.co/spaces/QSLY/ghp/resolve/main/packages/server/src/controllers/playerController.ts
4.83 kB
| import { EVENTS, ERROR_CODE, playerSeekSchema, playerSetModeSchema } from '@music-together/shared' | |
| import type { TypedServer, TypedSocket } from '../middleware/types.js' | |
| import { createWithPermission, defineAbilityForRoomUser } from '../middleware/withControl.js' | |
| import { createWithRoom } from '../middleware/withRoom.js' | |
| import { checkSocketRateLimit } from '../middleware/socketRateLimiter.js' | |
| import { roomRepo } from '../repositories/roomRepository.js' | |
| import * as playerService from '../services/playerService.js' | |
| import * as roomService from '../services/roomService.js' | |
| import { estimateCurrentTimeAt } from '../services/syncService.js' | |
| import { logger } from '../utils/logger.js' | |
| export function registerPlayerController(io: TypedServer, socket: TypedSocket) { | |
| const withPermission = createWithPermission(io) | |
| socket.on( | |
| EVENTS.PLAYER_PLAY, | |
| withPermission('play', 'Player', async (ctx, data) => { | |
| if (!(await checkSocketRateLimit(ctx.socket))) return | |
| const track = data?.track ?? ctx.room.currentTrack ?? ctx.room.queue[0] | |
| if (!track) return | |
| // Resume: same track already loaded and has stream URL → keep position | |
| if (!data?.track && ctx.room.currentTrack?.id === track.id && ctx.room.currentTrack?.streamUrl) { | |
| playerService.resumeTrack(ctx.io, ctx.roomId, ctx.socket) | |
| return | |
| } | |
| await playerService.playTrackInRoom(ctx.io, ctx.roomId, track) | |
| }), | |
| ) | |
| socket.on( | |
| EVENTS.PLAYER_PAUSE, | |
| withPermission('pause', 'Player', (ctx) => { | |
| playerService.pauseTrack(ctx.io, ctx.roomId, ctx.socket) | |
| }), | |
| ) | |
| socket.on( | |
| EVENTS.PLAYER_SEEK, | |
| withPermission('seek', 'Player', (ctx, data) => { | |
| const parsed = playerSeekSchema.safeParse(data) | |
| if (!parsed.success) return | |
| playerService.seekTrack(ctx.io, ctx.roomId, parsed.data.currentTime, ctx.socket) | |
| }), | |
| ) | |
| // Conductor (hostId) auto-next bypasses CASL — system behavior, not manual user action. | |
| // Non-conductor manual next still requires CASL permission check. | |
| const withRoom = createWithRoom(io) | |
| socket.on( | |
| EVENTS.PLAYER_NEXT, | |
| withRoom(async (ctx) => { | |
| if (ctx.user.id !== ctx.room.hostId) { | |
| const ability = defineAbilityForRoomUser(ctx.user.id, ctx.user.role) | |
| if (!ability.can('next', 'Player')) { | |
| ctx.socket.emit(EVENTS.ROOM_ERROR, { | |
| code: ERROR_CODE.NO_PERMISSION, | |
| message: '你没有权限执行此操作', | |
| }) | |
| return | |
| } | |
| } | |
| await playerService.playNextTrackInRoom(ctx.io, ctx.roomId, ctx.room.playMode) | |
| }), | |
| ) | |
| socket.on( | |
| EVENTS.PLAYER_PREV, | |
| withPermission('prev', 'Player', async (ctx) => { | |
| await playerService.playPrevTrackInRoom(ctx.io, ctx.roomId) | |
| }), | |
| ) | |
| socket.on( | |
| EVENTS.PLAYER_SET_MODE, | |
| withPermission('set-mode', 'Player', (ctx, data) => { | |
| const parsed = playerSetModeSchema.safeParse(data) | |
| if (!parsed.success) return | |
| ctx.room.playMode = parsed.data.mode | |
| roomRepo.persist(ctx.roomId) | |
| // Broadcast updated room state so all clients see the new play mode | |
| ctx.io.to(ctx.roomId).emit(EVENTS.ROOM_STATE, roomService.toPublicRoomState(ctx.room)) | |
| logger.info(`房间 ${ctx.roomId} 的播放模式已切换为 ${parsed.data.mode}`, { | |
| event: 'player.mode_changed', | |
| roomId: ctx.roomId, | |
| playMode: parsed.data.mode, | |
| operatorId: ctx.user.id, | |
| operator: ctx.user.nickname, | |
| }) | |
| }), | |
| ) | |
| // --------------------------------------------------------------------------- | |
| // NTP clock synchronisation – reply instantly with server time | |
| // --------------------------------------------------------------------------- | |
| socket.on(EVENTS.NTP_PING, (data) => { | |
| try { | |
| // Store client-reported RTT for adaptive scheduling delay | |
| if (data?.lastRttMs != null && data.lastRttMs > 0 && data.lastRttMs <= 10_000) { | |
| roomRepo.setSocketRTT(socket.id, data.lastRttMs) | |
| } | |
| socket.emit(EVENTS.NTP_PONG, { | |
| clientPingId: data?.clientPingId ?? 0, | |
| serverTime: Date.now(), | |
| }) | |
| } catch (err) { | |
| logger.error('NTP_PING handler error', err, { socketId: socket.id }) | |
| } | |
| }) | |
| socket.on(EVENTS.PLAYER_SYNC_REQUEST, () => { | |
| try { | |
| const mapping = roomRepo.getSocketMapping(socket.id) | |
| if (!mapping) return | |
| const room = roomRepo.get(mapping.roomId) | |
| if (!room) return | |
| const serverTimestamp = Date.now() | |
| socket.emit(EVENTS.PLAYER_SYNC_RESPONSE, { | |
| currentTime: estimateCurrentTimeAt(mapping.roomId, serverTimestamp), | |
| isPlaying: room.playState.isPlaying, | |
| serverTimestamp, | |
| }) | |
| } catch (err) { | |
| logger.error('PLAYER_SYNC_REQUEST handler error', err, { | |
| socketId: socket.id, | |
| }) | |
| } | |
| }) | |
| } | |