389 lines
11 KiB
JavaScript
389 lines
11 KiB
JavaScript
// Next.js 自定义服务器 + Socket.IO
|
|
const { createServer } = require('http');
|
|
const { parse } = require('url');
|
|
const next = require('next');
|
|
const { Server } = require('socket.io');
|
|
|
|
const dev = process.env.NODE_ENV !== 'production';
|
|
const hostname = process.env.HOSTNAME || '0.0.0.0';
|
|
const port = parseInt(process.env.PORT || '3000', 10);
|
|
|
|
const app = next({ dev, hostname, port });
|
|
const handle = app.getRequestHandler();
|
|
|
|
// 观影室服务器类
|
|
class WatchRoomServer {
|
|
constructor(io) {
|
|
this.io = io;
|
|
this.rooms = new Map();
|
|
this.members = new Map();
|
|
this.socketToRoom = new Map();
|
|
this.cleanupInterval = null;
|
|
this.setupEventHandlers();
|
|
this.startCleanupTimer();
|
|
}
|
|
|
|
setupEventHandlers() {
|
|
this.io.on('connection', (socket) => {
|
|
console.log(`[WatchRoom] Client connected: ${socket.id}`);
|
|
|
|
// 创建房间
|
|
socket.on('room:create', (data, callback) => {
|
|
try {
|
|
const roomId = this.generateRoomId();
|
|
const userId = socket.id;
|
|
|
|
const room = {
|
|
id: roomId,
|
|
name: data.name,
|
|
description: data.description,
|
|
password: data.password,
|
|
isPublic: data.isPublic,
|
|
ownerId: userId,
|
|
ownerName: data.userName,
|
|
memberCount: 1,
|
|
currentState: null,
|
|
createdAt: Date.now(),
|
|
lastOwnerHeartbeat: Date.now(),
|
|
};
|
|
|
|
const member = {
|
|
id: userId,
|
|
name: data.userName,
|
|
isOwner: true,
|
|
lastHeartbeat: Date.now(),
|
|
};
|
|
|
|
this.rooms.set(roomId, room);
|
|
this.members.set(roomId, new Map([[userId, member]]));
|
|
this.socketToRoom.set(socket.id, {
|
|
roomId,
|
|
userId,
|
|
userName: data.userName,
|
|
isOwner: true,
|
|
});
|
|
|
|
socket.join(roomId);
|
|
|
|
console.log(`[WatchRoom] Room created: ${roomId} by ${data.userName}`);
|
|
callback({ success: true, room });
|
|
} catch (error) {
|
|
console.error('[WatchRoom] Error creating room:', error);
|
|
callback({ success: false, error: '创建房间失败' });
|
|
}
|
|
});
|
|
|
|
// 加入房间
|
|
socket.on('room:join', (data, callback) => {
|
|
try {
|
|
const room = this.rooms.get(data.roomId);
|
|
if (!room) {
|
|
return callback({ success: false, error: '房间不存在' });
|
|
}
|
|
|
|
if (room.password && room.password !== data.password) {
|
|
return callback({ success: false, error: '密码错误' });
|
|
}
|
|
|
|
const userId = socket.id;
|
|
const member = {
|
|
id: userId,
|
|
name: data.userName,
|
|
isOwner: false,
|
|
lastHeartbeat: Date.now(),
|
|
};
|
|
|
|
const roomMembers = this.members.get(data.roomId);
|
|
if (roomMembers) {
|
|
roomMembers.set(userId, member);
|
|
room.memberCount = roomMembers.size;
|
|
this.rooms.set(data.roomId, room);
|
|
}
|
|
|
|
this.socketToRoom.set(socket.id, {
|
|
roomId: data.roomId,
|
|
userId,
|
|
userName: data.userName,
|
|
isOwner: false,
|
|
});
|
|
|
|
socket.join(data.roomId);
|
|
socket.to(data.roomId).emit('room:member-joined', member);
|
|
|
|
console.log(`[WatchRoom] User ${data.userName} joined room ${data.roomId}`);
|
|
|
|
const members = Array.from(roomMembers?.values() || []);
|
|
callback({ success: true, room, members });
|
|
} catch (error) {
|
|
console.error('[WatchRoom] Error joining room:', error);
|
|
callback({ success: false, error: '加入房间失败' });
|
|
}
|
|
});
|
|
|
|
// 离开房间
|
|
socket.on('room:leave', () => {
|
|
this.handleLeaveRoom(socket);
|
|
});
|
|
|
|
// 获取房间列表
|
|
socket.on('room:list', (callback) => {
|
|
const publicRooms = Array.from(this.rooms.values()).filter((room) => room.isPublic);
|
|
callback(publicRooms);
|
|
});
|
|
|
|
// 播放状态更新
|
|
socket.on('play:update', (state) => {
|
|
const roomInfo = this.socketToRoom.get(socket.id);
|
|
if (!roomInfo || !roomInfo.isOwner) return;
|
|
|
|
const room = this.rooms.get(roomInfo.roomId);
|
|
if (room) {
|
|
room.currentState = state;
|
|
this.rooms.set(roomInfo.roomId, room);
|
|
socket.to(roomInfo.roomId).emit('play:update', state);
|
|
}
|
|
});
|
|
|
|
// 播放进度跳转
|
|
socket.on('play:seek', (currentTime) => {
|
|
const roomInfo = this.socketToRoom.get(socket.id);
|
|
if (!roomInfo) return;
|
|
socket.to(roomInfo.roomId).emit('play:seek', currentTime);
|
|
});
|
|
|
|
// 播放
|
|
socket.on('play:play', () => {
|
|
const roomInfo = this.socketToRoom.get(socket.id);
|
|
if (!roomInfo) return;
|
|
socket.to(roomInfo.roomId).emit('play:play');
|
|
});
|
|
|
|
// 暂停
|
|
socket.on('play:pause', () => {
|
|
const roomInfo = this.socketToRoom.get(socket.id);
|
|
if (!roomInfo) return;
|
|
socket.to(roomInfo.roomId).emit('play:pause');
|
|
});
|
|
|
|
// 切换视频/集数
|
|
socket.on('play:change', (state) => {
|
|
const roomInfo = this.socketToRoom.get(socket.id);
|
|
if (!roomInfo || !roomInfo.isOwner) return;
|
|
|
|
const room = this.rooms.get(roomInfo.roomId);
|
|
if (room) {
|
|
room.currentState = state;
|
|
this.rooms.set(roomInfo.roomId, room);
|
|
socket.to(roomInfo.roomId).emit('play:change', state);
|
|
}
|
|
});
|
|
|
|
// 切换直播频道
|
|
socket.on('live:change', (state) => {
|
|
const roomInfo = this.socketToRoom.get(socket.id);
|
|
if (!roomInfo || !roomInfo.isOwner) return;
|
|
|
|
const room = this.rooms.get(roomInfo.roomId);
|
|
if (room) {
|
|
room.currentState = state;
|
|
this.rooms.set(roomInfo.roomId, room);
|
|
socket.to(roomInfo.roomId).emit('live:change', state);
|
|
}
|
|
});
|
|
|
|
// 聊天消息
|
|
socket.on('chat:message', (data) => {
|
|
const roomInfo = this.socketToRoom.get(socket.id);
|
|
if (!roomInfo) return;
|
|
|
|
const message = {
|
|
id: this.generateMessageId(),
|
|
userId: roomInfo.userId,
|
|
userName: roomInfo.userName,
|
|
content: data.content,
|
|
type: data.type,
|
|
timestamp: Date.now(),
|
|
};
|
|
|
|
this.io.to(roomInfo.roomId).emit('chat:message', message);
|
|
});
|
|
|
|
// WebRTC 信令
|
|
socket.on('voice:offer', (data) => {
|
|
const roomInfo = this.socketToRoom.get(socket.id);
|
|
if (!roomInfo) return;
|
|
this.io.to(data.targetUserId).emit('voice:offer', {
|
|
userId: socket.id,
|
|
offer: data.offer,
|
|
});
|
|
});
|
|
|
|
socket.on('voice:answer', (data) => {
|
|
const roomInfo = this.socketToRoom.get(socket.id);
|
|
if (!roomInfo) return;
|
|
this.io.to(data.targetUserId).emit('voice:answer', {
|
|
userId: socket.id,
|
|
answer: data.answer,
|
|
});
|
|
});
|
|
|
|
socket.on('voice:ice', (data) => {
|
|
const roomInfo = this.socketToRoom.get(socket.id);
|
|
if (!roomInfo) return;
|
|
this.io.to(data.targetUserId).emit('voice:ice', {
|
|
userId: socket.id,
|
|
candidate: data.candidate,
|
|
});
|
|
});
|
|
|
|
// 心跳
|
|
socket.on('heartbeat', () => {
|
|
const roomInfo = this.socketToRoom.get(socket.id);
|
|
if (!roomInfo) return;
|
|
|
|
const roomMembers = this.members.get(roomInfo.roomId);
|
|
const member = roomMembers?.get(roomInfo.userId);
|
|
if (member) {
|
|
member.lastHeartbeat = Date.now();
|
|
roomMembers?.set(roomInfo.userId, member);
|
|
}
|
|
|
|
if (roomInfo.isOwner) {
|
|
const room = this.rooms.get(roomInfo.roomId);
|
|
if (room) {
|
|
room.lastOwnerHeartbeat = Date.now();
|
|
this.rooms.set(roomInfo.roomId, room);
|
|
}
|
|
}
|
|
});
|
|
|
|
// 断开连接
|
|
socket.on('disconnect', () => {
|
|
console.log(`[WatchRoom] Client disconnected: ${socket.id}`);
|
|
this.handleLeaveRoom(socket);
|
|
});
|
|
});
|
|
}
|
|
|
|
handleLeaveRoom(socket) {
|
|
const roomInfo = this.socketToRoom.get(socket.id);
|
|
if (!roomInfo) return;
|
|
|
|
const { roomId, userId, isOwner } = roomInfo;
|
|
const roomMembers = this.members.get(roomId);
|
|
|
|
if (roomMembers) {
|
|
roomMembers.delete(userId);
|
|
|
|
const room = this.rooms.get(roomId);
|
|
if (room) {
|
|
room.memberCount = roomMembers.size;
|
|
this.rooms.set(roomId, room);
|
|
}
|
|
|
|
socket.to(roomId).emit('room:member-left', userId);
|
|
|
|
if (isOwner) {
|
|
console.log(`[WatchRoom] Owner left room ${roomId}, will auto-delete after 5 minutes`);
|
|
}
|
|
|
|
if (roomMembers.size === 0) {
|
|
this.deleteRoom(roomId);
|
|
}
|
|
}
|
|
|
|
socket.leave(roomId);
|
|
this.socketToRoom.delete(socket.id);
|
|
}
|
|
|
|
deleteRoom(roomId) {
|
|
console.log(`[WatchRoom] Deleting room ${roomId}`);
|
|
this.io.to(roomId).emit('room:deleted');
|
|
this.rooms.delete(roomId);
|
|
this.members.delete(roomId);
|
|
}
|
|
|
|
startCleanupTimer() {
|
|
this.cleanupInterval = setInterval(() => {
|
|
const now = Date.now();
|
|
const timeout = 5 * 60 * 1000; // 5分钟
|
|
|
|
for (const [roomId, room] of this.rooms.entries()) {
|
|
if (now - room.lastOwnerHeartbeat > timeout) {
|
|
console.log(`[WatchRoom] Room ${roomId} owner timeout, deleting...`);
|
|
this.deleteRoom(roomId);
|
|
}
|
|
}
|
|
}, 30000); // 每30秒检查一次
|
|
}
|
|
|
|
generateRoomId() {
|
|
return Math.random().toString(36).substring(2, 8).toUpperCase();
|
|
}
|
|
|
|
generateMessageId() {
|
|
return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
}
|
|
|
|
destroy() {
|
|
if (this.cleanupInterval) {
|
|
clearInterval(this.cleanupInterval);
|
|
}
|
|
}
|
|
}
|
|
|
|
app.prepare().then(() => {
|
|
const httpServer = createServer(async (req, res) => {
|
|
try {
|
|
const parsedUrl = parse(req.url, true);
|
|
await handle(req, res, parsedUrl);
|
|
} catch (err) {
|
|
console.error('Error occurred handling', req.url, err);
|
|
res.statusCode = 500;
|
|
res.end('Internal server error');
|
|
}
|
|
});
|
|
|
|
// 初始化 Socket.IO
|
|
const io = new Server(httpServer, {
|
|
path: '/socket.io',
|
|
cors: {
|
|
origin: '*',
|
|
methods: ['GET', 'POST'],
|
|
},
|
|
});
|
|
|
|
// 初始化观影室服务器
|
|
const watchRoomServer = new WatchRoomServer(io);
|
|
console.log('[WatchRoom] Socket.IO server initialized');
|
|
|
|
httpServer
|
|
.once('error', (err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
})
|
|
.listen(port, () => {
|
|
console.log(`> Ready on http://${hostname}:${port}`);
|
|
console.log(`> Socket.IO ready on ws://${hostname}:${port}`);
|
|
});
|
|
|
|
// 优雅关闭
|
|
process.on('SIGINT', () => {
|
|
console.log('\n[Server] Shutting down...');
|
|
watchRoomServer.destroy();
|
|
httpServer.close(() => {
|
|
console.log('[Server] Server closed');
|
|
process.exit(0);
|
|
});
|
|
});
|
|
|
|
process.on('SIGTERM', () => {
|
|
console.log('\n[Server] Shutting down...');
|
|
watchRoomServer.destroy();
|
|
httpServer.close(() => {
|
|
console.log('[Server] Server closed');
|
|
process.exit(0);
|
|
});
|
|
});
|
|
});
|