websync / pages /api /socketio.ts
shivam413's picture
Update pages/api/socketio.ts
8ef004b verified
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) => {
// @ts-ignore
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>(
// @ts-ignore
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)
// Send initial chat history to the newly joined socket
{
const r = await getRoom(roomId)
if (r) {
io.to(socket.id).emit("chatHistory", r.chatLog ?? [])
}
}
// Simple chat rate limiting per-socket
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)
})
// Add a URL to playlist without immediate playback
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)
})
// ===== Chat events =====
socket.on("chatMessage", async (text: string) => {
try {
const now = Date.now()
// Basic rate limiting: 1 message every 750ms
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
// Find sender's display name
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)
}
})
// =======================
}
)
// @ts-ignore
res.socket.server.io = io
}
res.end()
}
export const config = {
api: {
bodyParser: false,
},
}
export default ioHandler