观影室起步
This commit is contained in:
@@ -0,0 +1,388 @@
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user