Arnav Singh commited on
Commit
97a853c
·
unverified ·
2 Parent(s): 5ebde92cd35cdb

Merge pull request #2 from ArnavSingh76533/copilot/fix-default-video-playlist

Browse files
.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
@@ -16,6 +16,22 @@ interface Props {
16
  className?: string
17
  }
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  const ChatPanel: FC<Props> = ({ socket, className }) => {
20
  const [messages, _setMessages] = useState<ChatMessage[]>([])
21
  const [text, setText] = useState("")
@@ -31,6 +47,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 +86,27 @@ 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">
 
16
  className?: string
17
  }
18
 
19
+ // Audio notification constants
20
+ const NOTIFICATION_FREQUENCY = 800
21
+ const NOTIFICATION_VOLUME = 0.3
22
+ const NOTIFICATION_VOLUME_END = 0.01
23
+ const NOTIFICATION_DURATION = 0.1
24
+
25
+ // Reusable audio context for notifications
26
+ let audioContext: AudioContext | null = null
27
+
28
+ const getAudioContext = () => {
29
+ if (!audioContext) {
30
+ audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
31
+ }
32
+ return audioContext
33
+ }
34
+
35
  const ChatPanel: FC<Props> = ({ socket, className }) => {
36
  const [messages, _setMessages] = useState<ChatMessage[]>([])
37
  const [text, setText] = useState("")
 
47
  }
48
  const onNew = (msg: ChatMessage) => {
49
  setMessages([...messagesRef.current, msg].slice(-200))
50
+ // Play notification sound using Web Audio API
51
+ try {
52
+ const ctx = getAudioContext()
53
+ const oscillator = ctx.createOscillator()
54
+ const gainNode = ctx.createGain()
55
+
56
+ oscillator.connect(gainNode)
57
+ gainNode.connect(ctx.destination)
58
+
59
+ oscillator.frequency.value = NOTIFICATION_FREQUENCY
60
+ oscillator.type = 'sine'
61
+
62
+ gainNode.gain.setValueAtTime(NOTIFICATION_VOLUME, ctx.currentTime)
63
+ gainNode.gain.exponentialRampToValueAtTime(NOTIFICATION_VOLUME_END, ctx.currentTime + NOTIFICATION_DURATION)
64
+
65
+ oscillator.start(ctx.currentTime)
66
+ oscillator.stop(ctx.currentTime + NOTIFICATION_DURATION)
67
+ } catch (err) {
68
+ console.log("Audio notification failed:", err)
69
+ }
70
  }
71
 
72
  socket.on("chatHistory", onHistory)
 
86
 
87
  return (
88
  <div className={className ?? "flex flex-col h-64 border border-dark-700/50 rounded-xl overflow-hidden shadow-lg bg-dark-900"}>
89
+ <div className="flex-1 overflow-y-auto p-4 space-y-3 bg-dark-900/50 flex flex-col-reverse">
90
+ {messages.length === 0 ? (
 
 
 
 
 
 
 
 
 
 
91
  <div className="text-dark-500 text-sm text-center py-8">
92
  No messages yet. Be the first to say hello! 👋
93
  </div>
94
+ ) : (
95
+ // Render messages in reverse order without creating a new array
96
+ messages.map((_, idx) => {
97
+ const reverseIdx = messages.length - 1 - idx
98
+ const msg = messages[reverseIdx]
99
+ return (
100
+ <div key={msg.id} className="text-sm bg-dark-800/50 rounded-lg p-3 border border-dark-700/30">
101
+ <div className="flex items-center gap-2 mb-1">
102
+ <span className="font-semibold text-primary-400">{msg.name}</span>
103
+ <span className="text-dark-500 text-xs">•</span>
104
+ <span className="text-dark-500 text-xs">{new Date(msg.ts).toLocaleTimeString()}</span>
105
+ </div>
106
+ <div className="break-words text-dark-200">{msg.text}</div>
107
+ </div>
108
+ )
109
+ })
110
  )}
111
  </div>
112
  <div className="p-3 flex gap-2 bg-dark-800/50 border-t border-dark-700/50">
components/icon/IconPip.tsx ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { FC } from "react"
2
+ import Icon, { IconProps } from "./Icon"
3
+
4
+ const IconPip: FC<IconProps> = ({ className = "" }) => {
5
+ return (
6
+ <Icon className={className} viewBox='0 0 24 24'>
7
+ <path
8
+ fill='currentColor'
9
+ d='M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 1.98 2 1.98h18c1.1 0 2-.88 2-1.98V5c0-1.1-.9-2-2-2zm0 16.01H3V4.98h18v14.03z'
10
+ />
11
+ </Icon>
12
+ )
13
+ }
14
+
15
+ export default IconPip
components/player/Controls.tsx CHANGED
@@ -19,6 +19,8 @@ import PlayerMenu from "./PlayerMenu"
19
  import { Tooltip } from "react-tooltip"
20
  import IconNewTab from "../icon/IconNewTab"
21
  import Link from "next/link"
 
 
22
 
23
  interface Props extends PlayerState {
24
  roomId: string
@@ -34,6 +36,11 @@ 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 +74,11 @@ 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 +92,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 +204,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 +213,7 @@ const Controls: FC<Props> = ({
199
  }
200
  }}
201
  interaction={showControlsAction}
 
202
  >
203
  {playEnded() ? (
204
  <IconReplay />
@@ -275,6 +290,32 @@ const Controls: FC<Props> = ({
275
  setMenuOpen={setMenuOpen}
276
  />
277
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  <ControlButton
279
  tooltip={fullscreen ? "Leave fullscreen" : "Enter fullscreen"}
280
  onClick={() => {
 
19
  import { Tooltip } from "react-tooltip"
20
  import IconNewTab from "../icon/IconNewTab"
21
  import Link from "next/link"
22
+ import IconMusic from "../icon/IconMusic"
23
+ import IconPip from "../icon/IconPip"
24
 
25
  interface Props extends PlayerState {
26
  roomId: string
 
36
  playIndex: (index: number) => void
37
  setSeeking: (seeking: boolean) => void
38
  playAgain: () => void
39
+ isOwner: boolean
40
+ pipEnabled: boolean
41
+ setPipEnabled: (enabled: boolean) => void
42
+ musicMode: boolean
43
+ setMusicMode: (enabled: boolean) => void
44
  }
45
 
46
  let interaction = false
 
74
  playIndex,
75
  setSeeking,
76
  playAgain,
77
+ isOwner,
78
+ pipEnabled,
79
+ setPipEnabled,
80
+ musicMode,
81
+ setMusicMode,
82
  }) => {
83
  const [showControls, setShowControls] = useState(true)
84
  const [showTimePlayed, setShowTimePlayed] = useState(true)
 
92
  if (new Date().getTime() - interactionTime > 350) {
93
  if (interaction && !doubleClick) {
94
  doubleClick = false
95
+ if (isOwner) {
96
+ if (playEnded()) {
97
+ playAgain()
98
+ } else {
99
+ setPaused(!paused)
100
+ }
101
  }
102
  }
103
 
 
204
  <ControlButton
205
  tooltip={playEnded() ? "Play again" : paused ? "Play" : "Pause"}
206
  onClick={() => {
207
+ if (show && isOwner) {
208
  if (playEnded()) {
209
  playAgain()
210
  } else {
 
213
  }
214
  }}
215
  interaction={showControlsAction}
216
+ className={!isOwner ? "opacity-50 cursor-not-allowed" : ""}
217
  >
218
  {playEnded() ? (
219
  <IconReplay />
 
290
  setMenuOpen={setMenuOpen}
291
  />
292
 
293
+ <ControlButton
294
+ tooltip={musicMode ? "Exit music mode" : "Enter music mode"}
295
+ onClick={() => {
296
+ setMusicMode(!musicMode)
297
+ if (!musicMode && pipEnabled) {
298
+ setPipEnabled(false)
299
+ }
300
+ }}
301
+ interaction={showControlsAction}
302
+ >
303
+ <IconMusic />
304
+ </ControlButton>
305
+
306
+ <ControlButton
307
+ tooltip={pipEnabled ? "Exit PiP" : "Enter PiP"}
308
+ onClick={() => {
309
+ setPipEnabled(!pipEnabled)
310
+ if (!pipEnabled && musicMode) {
311
+ setMusicMode(false)
312
+ }
313
+ }}
314
+ interaction={showControlsAction}
315
+ >
316
+ <IconPip />
317
+ </ControlButton>
318
+
319
  <ControlButton
320
  tooltip={fullscreen ? "Leave fullscreen" : "Enter fullscreen"}
321
  onClick={() => {
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)
@@ -128,6 +130,8 @@ const Player: FC<Props> = ({ roomId, socket, fullHeight }) => {
128
  const [connected, setConnected] = useState(false)
129
  const [unmuted, setUnmuted] = useState(false)
130
  const [fullscreen, setFullscreen] = useState(false)
 
 
131
  const fullscreenHandle = useFullScreenHandle()
132
  const player = useRef<ReactPlayer>(null)
133
 
@@ -193,6 +197,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)
@@ -222,7 +232,7 @@ const Player: FC<Props> = ({ roomId, socket, fullHeight }) => {
222
  _setPlaylist(update.playlist)
223
  }
224
  })
225
- }, [socket])
226
 
227
  useEffect(() => {
228
  if (ready) {
@@ -243,9 +253,22 @@ const Player: FC<Props> = ({ roomId, socket, fullHeight }) => {
243
  }
244
  }}
245
  >
 
 
 
 
 
 
 
 
 
 
 
246
  <ReactPlayer
247
  style={{
248
  maxHeight: fullscreen || fullHeight ? "100vh" : "calc(100vh - 210px)",
 
 
249
  }}
250
  ref={player}
251
  width={"100%"}
@@ -265,7 +288,7 @@ const Player: FC<Props> = ({ roomId, socket, fullHeight }) => {
265
  },
266
  }}
267
  url={currentSrc.src}
268
- pip={false}
269
  playing={!paused}
270
  controls={false}
271
  loop={loop}
@@ -419,6 +442,11 @@ 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)
 
130
  const [connected, setConnected] = useState(false)
131
  const [unmuted, setUnmuted] = useState(false)
132
  const [fullscreen, setFullscreen] = useState(false)
133
+ const [pipEnabled, setPipEnabled] = useState(false)
134
+ const [musicMode, setMusicMode] = useState(false)
135
  const fullscreenHandle = useFullScreenHandle()
136
  const player = useRef<ReactPlayer>(null)
137
 
 
197
  setDeltaServerTime((room.serverTime - new Date().getTime()) / 1000)
198
  }
199
 
200
+ // Update owner info
201
+ if (room.ownerId !== ownerId) {
202
+ setOwnerId(room.ownerId)
203
+ setIsOwner(socket.id === room.ownerId)
204
+ }
205
+
206
  const update = room.targetState
207
  if (update.lastSync !== lastSyncRef.current) {
208
  _setLastSync(update.lastSync)
 
232
  _setPlaylist(update.playlist)
233
  }
234
  })
235
+ }, [socket, ownerId])
236
 
237
  useEffect(() => {
238
  if (ready) {
 
253
  }
254
  }}
255
  >
256
+ {musicMode && (
257
+ <div className="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-primary-900 to-dark-900">
258
+ <div className="flex flex-col items-center gap-4 text-primary-300">
259
+ <svg className="w-32 h-32" fill="currentColor" viewBox="0 0 512 512">
260
+ <path d='M470.38 1.51L150.41 96A32 32 0 0 0 128 126.51v261.41A139 139 0 0 0 96 384c-53 0-96 28.66-96 64s43 64 96 64 96-28.66 96-64V214.32l256-75v184.61a138.4 138.4 0 0 0-32-3.93c-53 0-96 28.66-96 64s43 64 96 64 96-28.65 96-64V32a32 32 0 0 0-41.62-30.49z' />
261
+ </svg>
262
+ <div className="text-2xl font-semibold">Music Mode</div>
263
+ <div className="text-sm text-primary-400">Audio continues playing</div>
264
+ </div>
265
+ </div>
266
+ )}
267
  <ReactPlayer
268
  style={{
269
  maxHeight: fullscreen || fullHeight ? "100vh" : "calc(100vh - 210px)",
270
+ visibility: musicMode ? "hidden" : "visible",
271
+ height: musicMode ? "0px" : (fullscreen || fullHeight ? "100vh" : "calc((9 / 16) * 100vw)"),
272
  }}
273
  ref={player}
274
  width={"100%"}
 
288
  },
289
  }}
290
  url={currentSrc.src}
291
+ pip={pipEnabled}
292
  playing={!paused}
293
  controls={false}
294
  loop={loop}
 
442
  lastSync={lastSync}
443
  error={error}
444
  playAgain={() => socket?.emit("playAgain")}
445
+ isOwner={isOwner}
446
+ pipEnabled={pipEnabled}
447
+ setPipEnabled={setPipEnabled}
448
+ musicMode={musicMode}
449
+ setMusicMode={setMusicMode}
450
  />
451
 
452
  <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
@@ -11,8 +11,19 @@ import {
11
  setRoom,
12
  } from "../../lib/cache"
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 +310,28 @@ 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
 
@@ -320,10 +346,7 @@ const ioHandler = (_: NextApiRequest, res: NextApiResponse) => {
320
  if (!isUrl(url)) return log("addToPlaylist invalid url", url)
321
  log("add to playlist", url)
322
 
323
- room.targetState.playlist.items.push({
324
- src: [{ src: url, resolution: "" }],
325
- sub: [],
326
- })
327
 
328
  await broadcast(room)
329
  })
 
11
  setRoom,
12
  } from "../../lib/cache"
13
  import { createNewRoom, createNewUser, updateLastSync } from "../../lib/room"
14
+ import { Playlist, RoomState, UserState, ChatMessage, MediaElement } from "../../lib/types"
15
  import { isUrl } from "../../lib/utils"
16
+ import { getDefaultImg, getDefaultSrc } from "../../lib/env"
17
+
18
+ /**
19
+ * Helper function to create a MediaElement from a URL
20
+ * @param url - The media URL to wrap
21
+ * @returns MediaElement with a single source and no subtitles
22
+ */
23
+ const createMediaElement = (url: string): MediaElement => ({
24
+ src: [{ src: url, resolution: "" }],
25
+ sub: [],
26
+ })
27
 
28
  const ioHandler = (_: NextApiRequest, res: NextApiResponse) => {
29
  // @ts-ignore
 
310
  return
311
  }
312
 
313
+ // Remove default image/video from playlist if it's the only item
314
+ const defaultImg = getDefaultImg()
315
+ const defaultMedia = defaultImg || getDefaultSrc()
316
+
317
+ if (room.targetState.playlist.items.length === 1) {
318
+ const firstItem = room.targetState.playlist.items[0]
319
+ if (firstItem?.src?.[0]?.src === defaultMedia) {
320
+ // Remove the default item
321
+ room.targetState.playlist.items = []
322
+ log("Removed default media from playlist")
323
+ }
324
  }
325
+
326
+ // Add new video to playlist at position 0
327
+ const newMedia = createMediaElement(url)
328
+ room.targetState.playlist.items.unshift(newMedia)
329
+
330
+ room.targetState.playing = newMedia
331
+ room.targetState.playlist.currentIndex = 0
332
  room.targetState.progress = 0
333
  room.targetState.lastSync = new Date().getTime() / 1000
334
+ room.targetState.paused = false
335
  await broadcast(room)
336
  })
337
 
 
346
  if (!isUrl(url)) return log("addToPlaylist invalid url", url)
347
  log("add to playlist", url)
348
 
349
+ room.targetState.playlist.items.push(createMediaElement(url))
 
 
 
350
 
351
  await broadcast(room)
352
  })
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.
yarn.lock CHANGED
@@ -102,6 +102,11 @@
102
  resolved "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.0.tgz"
103
  integrity sha512-naldaJy4hSVhWBgEjfdBY85CAa4UO+W1nx6a1sWStHZ7EUfNiuBTTN2KUYT5dH1+p/xij1t2QSXfCiFJoC5S/Q==
104
 
 
 
 
 
 
105
  "@img/sharp-linux-x64@0.33.1":
106
  version "0.33.1"
107
  resolved "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.1.tgz"
@@ -109,6 +114,13 @@
109
  optionalDependencies:
110
  "@img/sharp-libvips-linux-x64" "1.0.0"
111
 
 
 
 
 
 
 
 
112
  "@jridgewell/gen-mapping@^0.3.2":
113
  version "0.3.3"
114
  resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz"
@@ -166,6 +178,11 @@
166
  resolved "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.10.tgz"
167
  integrity sha512-GXvajAWh2woTT0GKEDlkVhFNxhJS/XdDmrVHrPOA83pLzlGPQnixqxD8u3bBB9oATBKB//5e4vpACnx5Vaxdqg==
168
 
 
 
 
 
 
169
  "@nodelib/fs.scandir@2.1.5":
170
  version "2.1.5"
171
  resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
 
102
  resolved "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.0.tgz"
103
  integrity sha512-naldaJy4hSVhWBgEjfdBY85CAa4UO+W1nx6a1sWStHZ7EUfNiuBTTN2KUYT5dH1+p/xij1t2QSXfCiFJoC5S/Q==
104
 
105
+ "@img/sharp-libvips-linuxmusl-x64@1.0.0":
106
+ version "1.0.0"
107
+ resolved "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.0.tgz"
108
+ integrity sha512-FW8iK6rJrg+X2jKD0Ajhjv6y74lToIBEvkZhl42nZt563FfxkCYacrXZtd+q/sRQDypQLzY5WdLkVTbJoPyqNg==
109
+
110
  "@img/sharp-linux-x64@0.33.1":
111
  version "0.33.1"
112
  resolved "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.1.tgz"
 
114
  optionalDependencies:
115
  "@img/sharp-libvips-linux-x64" "1.0.0"
116
 
117
+ "@img/sharp-linuxmusl-x64@0.33.1":
118
+ version "0.33.1"
119
+ resolved "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.1.tgz"
120
+ integrity sha512-LOGKNu5w8uu1evVqUAUKTix2sQu1XDRIYbsi5Q0c/SrXhvJ4QyOx+GaajxmOg5PZSsSnCYPSmhjHHsRBx06/wQ==
121
+ optionalDependencies:
122
+ "@img/sharp-libvips-linuxmusl-x64" "1.0.0"
123
+
124
  "@jridgewell/gen-mapping@^0.3.2":
125
  version "0.3.3"
126
  resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz"
 
178
  resolved "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.10.tgz"
179
  integrity sha512-GXvajAWh2woTT0GKEDlkVhFNxhJS/XdDmrVHrPOA83pLzlGPQnixqxD8u3bBB9oATBKB//5e4vpACnx5Vaxdqg==
180
 
181
+ "@next/swc-linux-x64-musl@14.2.10":
182
+ version "14.2.10"
183
+ resolved "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.10.tgz"
184
+ integrity sha512-opFFN5B0SnO+HTz4Wq4HaylXGFV+iHrVxd3YvREUX9K+xfc4ePbRrxqOuPOFjtSuiVouwe6uLeDtabjEIbkmDA==
185
+
186
  "@nodelib/fs.scandir@2.1.5":
187
  version "2.1.5"
188
  resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"