|
|
import * as socketIo from "socket.io" |
|
|
import { Server } from "socket.io" |
|
|
import { NextApiRequest, NextApiResponse } from "next" |
|
|
import { ClientToServerEvents, ServerToClientEvents } from "../../lib/socket" |
|
|
import { |
|
|
decUsers, |
|
|
deleteRoom, |
|
|
getRoom, |
|
|
incUsers, |
|
|
roomExists, |
|
|
setRoom, |
|
|
} from "../../lib/cache" |
|
|
import { createNewRoom, createNewUser, updateLastSync } from "../../lib/room" |
|
|
import { Playlist, RoomState, UserState, ChatMessage } from "../../lib/types" |
|
|
import { isUrl } from "../../lib/utils" |
|
|
|
|
|
const ioHandler = (_: NextApiRequest, res: NextApiResponse) => { |
|
|
|
|
|
if (res.socket !== null && "server" in res.socket && !res.socket.server.io) { |
|
|
console.log("*First use, starting socket.io") |
|
|
|
|
|
const io = new Server<ClientToServerEvents, ServerToClientEvents>( |
|
|
|
|
|
res.socket.server, |
|
|
{ |
|
|
path: "/api/socketio", |
|
|
} |
|
|
) |
|
|
|
|
|
const broadcast = async (room: string | RoomState) => { |
|
|
const roomId = typeof room === "string" ? room : room.id |
|
|
|
|
|
if (typeof room !== "string") { |
|
|
await setRoom(roomId, room) |
|
|
} else { |
|
|
const d = await getRoom(roomId) |
|
|
if (d === null) { |
|
|
throw Error("Impossible room state of null for room: " + roomId) |
|
|
} |
|
|
room = d |
|
|
} |
|
|
|
|
|
room.serverTime = new Date().getTime() |
|
|
io.to(roomId).emit("update", room) |
|
|
} |
|
|
|
|
|
io.on( |
|
|
"connection", |
|
|
async ( |
|
|
socket: socketIo.Socket<ClientToServerEvents, ServerToClientEvents> |
|
|
) => { |
|
|
if ( |
|
|
!("roomId" in socket.handshake.query) || |
|
|
typeof socket.handshake.query.roomId !== "string" |
|
|
) { |
|
|
socket.disconnect() |
|
|
return |
|
|
} |
|
|
|
|
|
const roomId = socket.handshake.query.roomId.toLowerCase() |
|
|
const log = (...props: any[]) => { |
|
|
console.log( |
|
|
"[" + new Date().toUTCString() + "][room " + roomId + "]", |
|
|
socket.id, |
|
|
...props |
|
|
) |
|
|
} |
|
|
|
|
|
if (!(await roomExists(roomId))) { |
|
|
await createNewRoom(roomId, socket.id) |
|
|
log("created room") |
|
|
} |
|
|
|
|
|
socket.join(roomId) |
|
|
await incUsers() |
|
|
log("joined") |
|
|
|
|
|
await createNewUser(roomId, socket.id) |
|
|
|
|
|
|
|
|
{ |
|
|
const r = await getRoom(roomId) |
|
|
if (r) { |
|
|
io.to(socket.id).emit("chatHistory", r.chatLog ?? []) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
let lastChatAt = 0 |
|
|
|
|
|
socket.on("disconnect", async () => { |
|
|
await decUsers() |
|
|
log("disconnected") |
|
|
const room = await getRoom(roomId) |
|
|
if (room === null) return |
|
|
|
|
|
room.users = room.users.filter( |
|
|
(user) => user.socketIds[0] !== socket.id |
|
|
) |
|
|
if (room.users.length === 0) { |
|
|
await deleteRoom(roomId) |
|
|
log("deleted empty room") |
|
|
} else { |
|
|
if (room.ownerId === socket.id) { |
|
|
room.ownerId = room.users[0].uid |
|
|
} |
|
|
await broadcast(room) |
|
|
} |
|
|
}) |
|
|
|
|
|
socket.on("setPaused", async (paused) => { |
|
|
let room = await getRoom(roomId) |
|
|
if (room === null) { |
|
|
throw new Error("Setting pause for non existing room:" + roomId) |
|
|
} |
|
|
log("set paused to", paused) |
|
|
|
|
|
room = updateLastSync(room) |
|
|
room.targetState.paused = paused |
|
|
await broadcast(room) |
|
|
}) |
|
|
|
|
|
socket.on("setLoop", async (loop) => { |
|
|
const room = await getRoom(roomId) |
|
|
if (room === null) { |
|
|
throw new Error("Setting loop for non existing room:" + roomId) |
|
|
} |
|
|
log("set loop to", loop) |
|
|
|
|
|
room.targetState.loop = loop |
|
|
await broadcast(updateLastSync(room)) |
|
|
}) |
|
|
|
|
|
socket.on("setProgress", async (progress) => { |
|
|
const room = await getRoom(roomId) |
|
|
if (room === null) { |
|
|
throw new Error("Setting progress for non existing room:" + roomId) |
|
|
} |
|
|
|
|
|
room.users = room.users.map((user) => { |
|
|
if (user.socketIds[0] === socket.id) { |
|
|
user.player.progress = progress |
|
|
} |
|
|
return user |
|
|
}) |
|
|
|
|
|
await broadcast(room) |
|
|
}) |
|
|
|
|
|
socket.on("setPlaybackRate", async (playbackRate) => { |
|
|
let room = await getRoom(roomId) |
|
|
if (room === null) { |
|
|
throw new Error( |
|
|
"Setting playbackRate for non existing room:" + roomId |
|
|
) |
|
|
} |
|
|
log("set playbackRate to", playbackRate) |
|
|
|
|
|
room = updateLastSync(room) |
|
|
room.targetState.playbackRate = playbackRate |
|
|
await broadcast(room) |
|
|
}) |
|
|
|
|
|
socket.on("seek", async (progress) => { |
|
|
const room = await getRoom(roomId) |
|
|
if (room === null) { |
|
|
throw new Error("Setting progress for non existing room:" + roomId) |
|
|
} |
|
|
log("seeking to", progress) |
|
|
|
|
|
room.targetState.progress = progress |
|
|
room.targetState.lastSync = new Date().getTime() / 1000 |
|
|
await broadcast(room) |
|
|
}) |
|
|
|
|
|
socket.on("playEnded", async () => { |
|
|
let room = await getRoom(roomId) |
|
|
if (room === null) { |
|
|
throw new Error("Play ended for non existing room:" + roomId) |
|
|
} |
|
|
log("playback ended") |
|
|
|
|
|
if (room.targetState.loop) { |
|
|
room.targetState.progress = 0 |
|
|
room.targetState.paused = false |
|
|
} else if ( |
|
|
room.targetState.playlist.currentIndex + 1 < |
|
|
room.targetState.playlist.items.length |
|
|
) { |
|
|
room.targetState.playing = |
|
|
room.targetState.playlist.items[ |
|
|
room.targetState.playlist.currentIndex + 1 |
|
|
] |
|
|
room.targetState.playlist.currentIndex += 1 |
|
|
room.targetState.progress = 0 |
|
|
room.targetState.paused = false |
|
|
} else { |
|
|
room.targetState.progress = |
|
|
room.users.find((user) => user.socketIds[0] === socket.id)?.player |
|
|
.progress || 0 |
|
|
room.targetState.paused = true |
|
|
} |
|
|
room.targetState.lastSync = new Date().getTime() / 1000 |
|
|
await broadcast(room) |
|
|
}) |
|
|
|
|
|
socket.on("playAgain", async () => { |
|
|
let room = await getRoom(roomId) |
|
|
if (room === null) { |
|
|
throw new Error("Play again for non existing room:" + roomId) |
|
|
} |
|
|
log("play same media again") |
|
|
|
|
|
room.targetState.progress = 0 |
|
|
room.targetState.paused = false |
|
|
room.targetState.lastSync = new Date().getTime() / 1000 |
|
|
await broadcast(room) |
|
|
}) |
|
|
|
|
|
socket.on("playItemFromPlaylist", async (index) => { |
|
|
let room = await getRoom(roomId) |
|
|
if (room === null) { |
|
|
throw new Error("Play ended for non existing room:" + roomId) |
|
|
} |
|
|
|
|
|
if (index < 0 || index >= room.targetState.playlist.items.length) { |
|
|
return log( |
|
|
"out of index:", |
|
|
index, |
|
|
"playlist.length:", |
|
|
room.targetState.playlist.items.length |
|
|
) |
|
|
} |
|
|
|
|
|
log("playing item", index, "from playlist") |
|
|
room.targetState.playing = room.targetState.playlist.items[index] |
|
|
room.targetState.playlist.currentIndex = index |
|
|
room.targetState.progress = 0 |
|
|
room.targetState.lastSync = new Date().getTime() / 1000 |
|
|
await broadcast(room) |
|
|
}) |
|
|
|
|
|
socket.on("updatePlaylist", async (playlist: Playlist) => { |
|
|
const room = await getRoom(roomId) |
|
|
if (room === null) { |
|
|
throw new Error("Setting playlist for non existing room:" + roomId) |
|
|
} |
|
|
log("playlist update", playlist) |
|
|
|
|
|
if ( |
|
|
playlist.currentIndex < -1 || |
|
|
playlist.currentIndex >= playlist.items.length |
|
|
) { |
|
|
return log( |
|
|
"out of index:", |
|
|
playlist.currentIndex, |
|
|
"playlist.length:", |
|
|
playlist.items.length |
|
|
) |
|
|
} |
|
|
|
|
|
room.targetState.playlist = playlist |
|
|
await broadcast(room) |
|
|
}) |
|
|
|
|
|
socket.on("updateUser", async (user: UserState) => { |
|
|
const room = await getRoom(roomId) |
|
|
if (room === null) { |
|
|
throw new Error("Setting user for non existing room:" + roomId) |
|
|
} |
|
|
log("user update", user) |
|
|
|
|
|
room.users = room.users.map((u) => { |
|
|
if (u.socketIds[0] !== socket.id) { |
|
|
return u |
|
|
} |
|
|
if (u.avatar !== user.avatar) { |
|
|
u.avatar = user.avatar |
|
|
} |
|
|
if (u.name !== user.name) { |
|
|
u.name = user.name |
|
|
} |
|
|
return u |
|
|
}) |
|
|
|
|
|
await broadcast(room) |
|
|
}) |
|
|
|
|
|
socket.on("playUrl", async (url) => { |
|
|
const room = await getRoom(roomId) |
|
|
if (room === null) { |
|
|
throw new Error( |
|
|
"Impossible non existing room, cannot send anything:" + roomId |
|
|
) |
|
|
} |
|
|
log("playing url", url) |
|
|
|
|
|
if (!isUrl(url)) { |
|
|
return |
|
|
} |
|
|
|
|
|
room.targetState.playing = { |
|
|
src: [{ src: url, resolution: "" }], |
|
|
sub: [], |
|
|
} |
|
|
room.targetState.playlist.currentIndex = -1 |
|
|
room.targetState.progress = 0 |
|
|
room.targetState.lastSync = new Date().getTime() / 1000 |
|
|
await broadcast(room) |
|
|
}) |
|
|
|
|
|
|
|
|
socket.on("addToPlaylist", async (url) => { |
|
|
const room = await getRoom(roomId) |
|
|
if (room === null) { |
|
|
throw new Error( |
|
|
"Impossible non existing room, cannot add to playlist:" + roomId |
|
|
) |
|
|
} |
|
|
if (!isUrl(url)) return log("addToPlaylist invalid url", url) |
|
|
log("add to playlist", url) |
|
|
|
|
|
room.targetState.playlist.items.push({ |
|
|
src: [{ src: url, resolution: "" }], |
|
|
sub: [], |
|
|
}) |
|
|
|
|
|
await broadcast(room) |
|
|
}) |
|
|
|
|
|
socket.on("fetch", async () => { |
|
|
const room = await getRoom(roomId) |
|
|
if (room === null) { |
|
|
throw new Error( |
|
|
"Impossible non existing room, cannot send anything:" + roomId |
|
|
) |
|
|
} |
|
|
|
|
|
room.serverTime = new Date().getTime() |
|
|
socket.emit("update", room) |
|
|
}) |
|
|
|
|
|
|
|
|
socket.on("chatMessage", async (text: string) => { |
|
|
try { |
|
|
const now = Date.now() |
|
|
|
|
|
if (now - lastChatAt < 750) return |
|
|
lastChatAt = now |
|
|
|
|
|
const msgText = (text || "").toString().trim() |
|
|
if (!msgText) return |
|
|
if (msgText.length > 500) return |
|
|
|
|
|
const room = await getRoom(roomId) |
|
|
if (room === null) return |
|
|
|
|
|
|
|
|
const sender = room.users.find((u) => u.socketIds[0] === socket.id) |
|
|
const name = sender?.name ?? "Anonymous" |
|
|
|
|
|
const msg: ChatMessage = { |
|
|
id: `${now}-${socket.id}`, |
|
|
userId: socket.id, |
|
|
name, |
|
|
text: msgText, |
|
|
ts: now, |
|
|
} |
|
|
|
|
|
room.chatLog = [...(room.chatLog ?? []), msg].slice(-200) |
|
|
await setRoom(roomId, room) |
|
|
|
|
|
io.to(roomId).emit("chatNew", msg) |
|
|
} catch (e) { |
|
|
console.error("chatMessage failed:", e) |
|
|
} |
|
|
}) |
|
|
|
|
|
} |
|
|
) |
|
|
|
|
|
|
|
|
res.socket.server.io = io |
|
|
} |
|
|
|
|
|
res.end() |
|
|
} |
|
|
|
|
|
export const config = { |
|
|
api: { |
|
|
bodyParser: false, |
|
|
}, |
|
|
} |
|
|
|
|
|
export default ioHandler |