| 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 |
|
|
| |
| 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) |
| }), |
| ) |
|
|
| |
| |
| 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) |
| |
| 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, |
| }) |
| }), |
| ) |
|
|
| |
| |
| |
| socket.on(EVENTS.NTP_PING, (data) => { |
| try { |
| |
| 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, |
| }) |
| } |
| }) |
| } |
|
|