Spaces:
Sleeping
Sleeping
copilot-swe-agent[bot] ArnavSingh76533 commited on
Commit ·
be73381
1
Parent(s): 6aa7aec
Implement issues 1-3: default image, message ordering, and admin controls
Browse filesCo-authored-by: ArnavSingh76533 <160649079+ArnavSingh76533@users.noreply.github.com>
- .env +3 -0
- components/chat/ChatPanel.tsx +33 -12
- components/player/Controls.tsx +10 -5
- components/player/Player.tsx +9 -0
- lib/env.ts +7 -0
- lib/room.ts +11 -4
- pages/api/socketio.ts +22 -1
- public/notification.mp3.txt +3 -0
.env
CHANGED
|
@@ -8,3 +8,6 @@ SITE_NAME="Web-SyncPlay"
|
|
| 8 |
PUBLIC_DOMAIN="shivam413-Streamer.hf.space"
|
| 9 |
|
| 10 |
YT_API_KEY=AIzaSyAhgfsfoHDpCmR-vtMu0gMyeimBtHwQFs8
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
PUBLIC_DOMAIN="shivam413-Streamer.hf.space"
|
| 9 |
|
| 10 |
YT_API_KEY=AIzaSyAhgfsfoHDpCmR-vtMu0gMyeimBtHwQFs8
|
| 11 |
+
|
| 12 |
+
# Default image to display when a new room is created (instead of default video)
|
| 13 |
+
DEFAULT_IMG=/logo_white.png
|
components/chat/ChatPanel.tsx
CHANGED
|
@@ -31,6 +31,26 @@ const ChatPanel: FC<Props> = ({ socket, className }) => {
|
|
| 31 |
}
|
| 32 |
const onNew = (msg: ChatMessage) => {
|
| 33 |
setMessages([...messagesRef.current, msg].slice(-200))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
}
|
| 35 |
|
| 36 |
socket.on("chatHistory", onHistory)
|
|
@@ -50,21 +70,22 @@ const ChatPanel: FC<Props> = ({ socket, className }) => {
|
|
| 50 |
|
| 51 |
return (
|
| 52 |
<div className={className ?? "flex flex-col h-64 border border-dark-700/50 rounded-xl overflow-hidden shadow-lg bg-dark-900"}>
|
| 53 |
-
<div className="flex-1 overflow-y-auto p-4 space-y-3 bg-dark-900/50">
|
| 54 |
-
{messages.
|
| 55 |
-
<div key={m.id} className="text-sm bg-dark-800/50 rounded-lg p-3 border border-dark-700/30">
|
| 56 |
-
<div className="flex items-center gap-2 mb-1">
|
| 57 |
-
<span className="font-semibold text-primary-400">{m.name}</span>
|
| 58 |
-
<span className="text-dark-500 text-xs">•</span>
|
| 59 |
-
<span className="text-dark-500 text-xs">{new Date(m.ts).toLocaleTimeString()}</span>
|
| 60 |
-
</div>
|
| 61 |
-
<div className="break-words text-dark-200">{m.text}</div>
|
| 62 |
-
</div>
|
| 63 |
-
))}
|
| 64 |
-
{messages.length === 0 && (
|
| 65 |
<div className="text-dark-500 text-sm text-center py-8">
|
| 66 |
No messages yet. Be the first to say hello! 👋
|
| 67 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
)}
|
| 69 |
</div>
|
| 70 |
<div className="p-3 flex gap-2 bg-dark-800/50 border-t border-dark-700/50">
|
|
|
|
| 31 |
}
|
| 32 |
const onNew = (msg: ChatMessage) => {
|
| 33 |
setMessages([...messagesRef.current, msg].slice(-200))
|
| 34 |
+
// Play notification sound using Web Audio API
|
| 35 |
+
try {
|
| 36 |
+
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
|
| 37 |
+
const oscillator = audioContext.createOscillator()
|
| 38 |
+
const gainNode = audioContext.createGain()
|
| 39 |
+
|
| 40 |
+
oscillator.connect(gainNode)
|
| 41 |
+
gainNode.connect(audioContext.destination)
|
| 42 |
+
|
| 43 |
+
oscillator.frequency.value = 800
|
| 44 |
+
oscillator.type = 'sine'
|
| 45 |
+
|
| 46 |
+
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime)
|
| 47 |
+
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1)
|
| 48 |
+
|
| 49 |
+
oscillator.start(audioContext.currentTime)
|
| 50 |
+
oscillator.stop(audioContext.currentTime + 0.1)
|
| 51 |
+
} catch (err) {
|
| 52 |
+
console.log("Audio notification failed:", err)
|
| 53 |
+
}
|
| 54 |
}
|
| 55 |
|
| 56 |
socket.on("chatHistory", onHistory)
|
|
|
|
| 70 |
|
| 71 |
return (
|
| 72 |
<div className={className ?? "flex flex-col h-64 border border-dark-700/50 rounded-xl overflow-hidden shadow-lg bg-dark-900"}>
|
| 73 |
+
<div className="flex-1 overflow-y-auto p-4 space-y-3 bg-dark-900/50 flex flex-col-reverse">
|
| 74 |
+
{messages.length === 0 ? (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
<div className="text-dark-500 text-sm text-center py-8">
|
| 76 |
No messages yet. Be the first to say hello! 👋
|
| 77 |
</div>
|
| 78 |
+
) : (
|
| 79 |
+
[...messages].reverse().map((m) => (
|
| 80 |
+
<div key={m.id} className="text-sm bg-dark-800/50 rounded-lg p-3 border border-dark-700/30">
|
| 81 |
+
<div className="flex items-center gap-2 mb-1">
|
| 82 |
+
<span className="font-semibold text-primary-400">{m.name}</span>
|
| 83 |
+
<span className="text-dark-500 text-xs">•</span>
|
| 84 |
+
<span className="text-dark-500 text-xs">{new Date(m.ts).toLocaleTimeString()}</span>
|
| 85 |
+
</div>
|
| 86 |
+
<div className="break-words text-dark-200">{m.text}</div>
|
| 87 |
+
</div>
|
| 88 |
+
))
|
| 89 |
)}
|
| 90 |
</div>
|
| 91 |
<div className="p-3 flex gap-2 bg-dark-800/50 border-t border-dark-700/50">
|
components/player/Controls.tsx
CHANGED
|
@@ -34,6 +34,7 @@ interface Props extends PlayerState {
|
|
| 34 |
playIndex: (index: number) => void
|
| 35 |
setSeeking: (seeking: boolean) => void
|
| 36 |
playAgain: () => void
|
|
|
|
| 37 |
}
|
| 38 |
|
| 39 |
let interaction = false
|
|
@@ -67,6 +68,7 @@ const Controls: FC<Props> = ({
|
|
| 67 |
playIndex,
|
| 68 |
setSeeking,
|
| 69 |
playAgain,
|
|
|
|
| 70 |
}) => {
|
| 71 |
const [showControls, setShowControls] = useState(true)
|
| 72 |
const [showTimePlayed, setShowTimePlayed] = useState(true)
|
|
@@ -80,10 +82,12 @@ const Controls: FC<Props> = ({
|
|
| 80 |
if (new Date().getTime() - interactionTime > 350) {
|
| 81 |
if (interaction && !doubleClick) {
|
| 82 |
doubleClick = false
|
| 83 |
-
if (
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
|
|
|
|
|
|
| 87 |
}
|
| 88 |
}
|
| 89 |
|
|
@@ -190,7 +194,7 @@ const Controls: FC<Props> = ({
|
|
| 190 |
<ControlButton
|
| 191 |
tooltip={playEnded() ? "Play again" : paused ? "Play" : "Pause"}
|
| 192 |
onClick={() => {
|
| 193 |
-
if (show) {
|
| 194 |
if (playEnded()) {
|
| 195 |
playAgain()
|
| 196 |
} else {
|
|
@@ -199,6 +203,7 @@ const Controls: FC<Props> = ({
|
|
| 199 |
}
|
| 200 |
}}
|
| 201 |
interaction={showControlsAction}
|
|
|
|
| 202 |
>
|
| 203 |
{playEnded() ? (
|
| 204 |
<IconReplay />
|
|
|
|
| 34 |
playIndex: (index: number) => void
|
| 35 |
setSeeking: (seeking: boolean) => void
|
| 36 |
playAgain: () => void
|
| 37 |
+
isOwner: boolean
|
| 38 |
}
|
| 39 |
|
| 40 |
let interaction = false
|
|
|
|
| 68 |
playIndex,
|
| 69 |
setSeeking,
|
| 70 |
playAgain,
|
| 71 |
+
isOwner,
|
| 72 |
}) => {
|
| 73 |
const [showControls, setShowControls] = useState(true)
|
| 74 |
const [showTimePlayed, setShowTimePlayed] = useState(true)
|
|
|
|
| 82 |
if (new Date().getTime() - interactionTime > 350) {
|
| 83 |
if (interaction && !doubleClick) {
|
| 84 |
doubleClick = false
|
| 85 |
+
if (isOwner) {
|
| 86 |
+
if (playEnded()) {
|
| 87 |
+
playAgain()
|
| 88 |
+
} else {
|
| 89 |
+
setPaused(!paused)
|
| 90 |
+
}
|
| 91 |
}
|
| 92 |
}
|
| 93 |
|
|
|
|
| 194 |
<ControlButton
|
| 195 |
tooltip={playEnded() ? "Play again" : paused ? "Play" : "Pause"}
|
| 196 |
onClick={() => {
|
| 197 |
+
if (show && isOwner) {
|
| 198 |
if (playEnded()) {
|
| 199 |
playAgain()
|
| 200 |
} else {
|
|
|
|
| 203 |
}
|
| 204 |
}}
|
| 205 |
interaction={showControlsAction}
|
| 206 |
+
className={!isOwner ? "opacity-50 cursor-not-allowed" : ""}
|
| 207 |
>
|
| 208 |
{playEnded() ? (
|
| 209 |
<IconReplay />
|
components/player/Player.tsx
CHANGED
|
@@ -110,6 +110,8 @@ const Player: FC<Props> = ({ roomId, socket, fullHeight }) => {
|
|
| 110 |
resolution: "",
|
| 111 |
})
|
| 112 |
const [currentSub, setCurrentSub] = useState<Subtitle>({ src: "", lang: "" })
|
|
|
|
|
|
|
| 113 |
|
| 114 |
const [error, setError] = useState(null)
|
| 115 |
const [ready, _setReady] = useState(false)
|
|
@@ -193,6 +195,12 @@ const Player: FC<Props> = ({ roomId, socket, fullHeight }) => {
|
|
| 193 |
setDeltaServerTime((room.serverTime - new Date().getTime()) / 1000)
|
| 194 |
}
|
| 195 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
const update = room.targetState
|
| 197 |
if (update.lastSync !== lastSyncRef.current) {
|
| 198 |
_setLastSync(update.lastSync)
|
|
@@ -419,6 +427,7 @@ const Player: FC<Props> = ({ roomId, socket, fullHeight }) => {
|
|
| 419 |
lastSync={lastSync}
|
| 420 |
error={error}
|
| 421 |
playAgain={() => socket?.emit("playAgain")}
|
|
|
|
| 422 |
/>
|
| 423 |
|
| 424 |
<div className={"absolute top-1 left-1 flex flex-col gap-1 p-1"}>
|
|
|
|
| 110 |
resolution: "",
|
| 111 |
})
|
| 112 |
const [currentSub, setCurrentSub] = useState<Subtitle>({ src: "", lang: "" })
|
| 113 |
+
const [ownerId, setOwnerId] = useState<string>("")
|
| 114 |
+
const [isOwner, setIsOwner] = useState(false)
|
| 115 |
|
| 116 |
const [error, setError] = useState(null)
|
| 117 |
const [ready, _setReady] = useState(false)
|
|
|
|
| 195 |
setDeltaServerTime((room.serverTime - new Date().getTime()) / 1000)
|
| 196 |
}
|
| 197 |
|
| 198 |
+
// Update owner info
|
| 199 |
+
if (room.ownerId !== ownerId) {
|
| 200 |
+
setOwnerId(room.ownerId)
|
| 201 |
+
setIsOwner(socket.id === room.ownerId)
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
const update = room.targetState
|
| 205 |
if (update.lastSync !== lastSyncRef.current) {
|
| 206 |
_setLastSync(update.lastSync)
|
|
|
|
| 427 |
lastSync={lastSync}
|
| 428 |
error={error}
|
| 429 |
playAgain={() => socket?.emit("playAgain")}
|
| 430 |
+
isOwner={isOwner}
|
| 431 |
/>
|
| 432 |
|
| 433 |
<div className={"absolute top-1 left-1 flex flex-col gap-1 p-1"}>
|
lib/env.ts
CHANGED
|
@@ -40,3 +40,10 @@ export function getDefaultSrc(): string {
|
|
| 40 |
// console.warn("ENV 'DEFAULT_SRC' has no value, using no src")
|
| 41 |
return "https://youtu.be/c-FKlE3_kHo"
|
| 42 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
// console.warn("ENV 'DEFAULT_SRC' has no value, using no src")
|
| 41 |
return "https://youtu.be/c-FKlE3_kHo"
|
| 42 |
}
|
| 43 |
+
|
| 44 |
+
export function getDefaultImg(): string | null {
|
| 45 |
+
if ("DEFAULT_IMG" in process.env) {
|
| 46 |
+
return <string>process.env.DEFAULT_IMG
|
| 47 |
+
}
|
| 48 |
+
return null
|
| 49 |
+
}
|
lib/room.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import { PlayerState, RoomState } from "./types"
|
| 2 |
import { getRandomName, getTargetTime } from "./utils"
|
| 3 |
-
import { getDefaultSrc } from "./env"
|
| 4 |
import { getRoom, setRoom } from "./cache"
|
| 5 |
|
| 6 |
export const updateLastSync = (room: RoomState) => {
|
|
@@ -52,6 +52,11 @@ export const createNewUser = async (roomId: string, socketId: string) => {
|
|
| 52 |
}
|
| 53 |
|
| 54 |
export const createNewRoom = async (roomId: string, socketId: string) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
await setRoom(roomId, {
|
| 56 |
serverTime: 0,
|
| 57 |
commandHistory: [],
|
|
@@ -61,17 +66,19 @@ export const createNewRoom = async (roomId: string, socketId: string) => {
|
|
| 61 |
playlist: {
|
| 62 |
items: [
|
| 63 |
{
|
| 64 |
-
src: [{ src:
|
| 65 |
sub: [],
|
|
|
|
| 66 |
},
|
| 67 |
],
|
| 68 |
currentIndex: 0,
|
| 69 |
},
|
| 70 |
playing: {
|
| 71 |
-
src: [{ src:
|
| 72 |
sub: [],
|
|
|
|
| 73 |
},
|
| 74 |
-
paused:
|
| 75 |
progress: 0,
|
| 76 |
playbackRate: 1,
|
| 77 |
loop: false,
|
|
|
|
| 1 |
import { PlayerState, RoomState } from "./types"
|
| 2 |
import { getRandomName, getTargetTime } from "./utils"
|
| 3 |
+
import { getDefaultSrc, getDefaultImg } from "./env"
|
| 4 |
import { getRoom, setRoom } from "./cache"
|
| 5 |
|
| 6 |
export const updateLastSync = (room: RoomState) => {
|
|
|
|
| 52 |
}
|
| 53 |
|
| 54 |
export const createNewRoom = async (roomId: string, socketId: string) => {
|
| 55 |
+
// Use default image if available, otherwise use default video
|
| 56 |
+
const defaultImg = getDefaultImg()
|
| 57 |
+
const defaultMedia = defaultImg || getDefaultSrc()
|
| 58 |
+
const isImage = !!defaultImg
|
| 59 |
+
|
| 60 |
await setRoom(roomId, {
|
| 61 |
serverTime: 0,
|
| 62 |
commandHistory: [],
|
|
|
|
| 66 |
playlist: {
|
| 67 |
items: [
|
| 68 |
{
|
| 69 |
+
src: [{ src: defaultMedia, resolution: "" }],
|
| 70 |
sub: [],
|
| 71 |
+
title: isImage ? "Welcome" : undefined,
|
| 72 |
},
|
| 73 |
],
|
| 74 |
currentIndex: 0,
|
| 75 |
},
|
| 76 |
playing: {
|
| 77 |
+
src: [{ src: defaultMedia, resolution: "" }],
|
| 78 |
sub: [],
|
| 79 |
+
title: isImage ? "Welcome" : undefined,
|
| 80 |
},
|
| 81 |
+
paused: isImage, // Pause by default if it's an image
|
| 82 |
progress: 0,
|
| 83 |
playbackRate: 1,
|
| 84 |
loop: false,
|
pages/api/socketio.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
| 13 |
import { createNewRoom, createNewUser, updateLastSync } from "../../lib/room"
|
| 14 |
import { Playlist, RoomState, UserState, ChatMessage } from "../../lib/types"
|
| 15 |
import { isUrl } from "../../lib/utils"
|
|
|
|
| 16 |
|
| 17 |
const ioHandler = (_: NextApiRequest, res: NextApiResponse) => {
|
| 18 |
// @ts-ignore
|
|
@@ -299,13 +300,33 @@ const ioHandler = (_: NextApiRequest, res: NextApiResponse) => {
|
|
| 299 |
return
|
| 300 |
}
|
| 301 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
room.targetState.playing = {
|
| 303 |
src: [{ src: url, resolution: "" }],
|
| 304 |
sub: [],
|
| 305 |
}
|
| 306 |
-
room.targetState.playlist.currentIndex =
|
| 307 |
room.targetState.progress = 0
|
| 308 |
room.targetState.lastSync = new Date().getTime() / 1000
|
|
|
|
| 309 |
await broadcast(room)
|
| 310 |
})
|
| 311 |
|
|
|
|
| 13 |
import { createNewRoom, createNewUser, updateLastSync } from "../../lib/room"
|
| 14 |
import { Playlist, RoomState, UserState, ChatMessage } from "../../lib/types"
|
| 15 |
import { isUrl } from "../../lib/utils"
|
| 16 |
+
import { getDefaultImg, getDefaultSrc } from "../../lib/env"
|
| 17 |
|
| 18 |
const ioHandler = (_: NextApiRequest, res: NextApiResponse) => {
|
| 19 |
// @ts-ignore
|
|
|
|
| 300 |
return
|
| 301 |
}
|
| 302 |
|
| 303 |
+
// Remove default image/video from playlist if it's the only item
|
| 304 |
+
const defaultImg = getDefaultImg()
|
| 305 |
+
const defaultMedia = defaultImg || getDefaultSrc()
|
| 306 |
+
|
| 307 |
+
if (room.targetState.playlist.items.length === 1) {
|
| 308 |
+
const firstItem = room.targetState.playlist.items[0]
|
| 309 |
+
if (firstItem.src[0]?.src === defaultMedia) {
|
| 310 |
+
// Remove the default item
|
| 311 |
+
room.targetState.playlist.items = []
|
| 312 |
+
log("Removed default media from playlist")
|
| 313 |
+
}
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
// Add new video to playlist at position 0
|
| 317 |
+
room.targetState.playlist.items.unshift({
|
| 318 |
+
src: [{ src: url, resolution: "" }],
|
| 319 |
+
sub: [],
|
| 320 |
+
})
|
| 321 |
+
|
| 322 |
room.targetState.playing = {
|
| 323 |
src: [{ src: url, resolution: "" }],
|
| 324 |
sub: [],
|
| 325 |
}
|
| 326 |
+
room.targetState.playlist.currentIndex = 0
|
| 327 |
room.targetState.progress = 0
|
| 328 |
room.targetState.lastSync = new Date().getTime() / 1000
|
| 329 |
+
room.targetState.paused = false
|
| 330 |
await broadcast(room)
|
| 331 |
})
|
| 332 |
|
public/notification.mp3.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
This is a placeholder for notification sound.
|
| 2 |
+
To add a real notification sound, place an MP3 file here named notification.mp3
|
| 3 |
+
For now, the code will try to play it but gracefully handle if not found.
|