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 files

Co-authored-by: ArnavSingh76533 <160649079+ArnavSingh76533@users.noreply.github.com>

.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.map((m) => (
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 (playEnded()) {
84
- playAgain()
85
- } else {
86
- setPaused(!paused)
 
 
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: getDefaultSrc(), resolution: "" }],
65
  sub: [],
 
66
  },
67
  ],
68
  currentIndex: 0,
69
  },
70
  playing: {
71
- src: [{ src: getDefaultSrc(), resolution: "" }],
72
  sub: [],
 
73
  },
74
- paused: false,
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 = -1
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.