copilot-swe-agent[bot] ArnavSingh76533 commited on
Commit
7f6fc57
·
1 Parent(s): 094792a

Implement all four player fixes: playlist reorder, fullscreen mobile, PiP, tap-to-pause

Browse files

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

components/player/Controls.tsx CHANGED
@@ -44,7 +44,6 @@ interface Props extends PlayerState {
44
  }
45
 
46
  let interaction = false
47
- let doubleClick = false
48
  let interactionTime = 0
49
  let lastMouseMove = 0
50
 
@@ -90,17 +89,10 @@ const Controls: FC<Props> = ({
90
 
91
  setTimeout(() => {
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
-
104
  interaction = false
105
  }
106
  }, 400)
@@ -117,6 +109,28 @@ const Controls: FC<Props> = ({
117
  return paused && progress > duration - SYNC_DELTA
118
  }
119
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  const mouseMoved = (touch: boolean | null = null) => {
121
  lastMouseMove = new Date().getTime()
122
 
@@ -137,7 +151,8 @@ const Controls: FC<Props> = ({
137
  <InteractionHandler
138
  className={classNames(
139
  "absolute top-0 left-0 w-full h-full transition-opacity flex flex-col",
140
- show ? "opacity-100" : "opacity-0"
 
141
  )}
142
  onMove={(_, touch) => {
143
  setShowControls(!touch)
@@ -156,11 +171,12 @@ const Controls: FC<Props> = ({
156
  }
157
  onClick={(_, touch) => {
158
  if (interaction) {
159
- doubleClick = true
160
  interaction = false
161
  console.log("Toggled fullscreen")
162
  setFullscreen(!fullscreen)
163
  } else if (touch) {
 
164
  setShowControls(!showControls)
165
  setMenuOpen(false)
166
  }
@@ -305,10 +321,47 @@ const Controls: FC<Props> = ({
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}
 
44
  }
45
 
46
  let interaction = false
 
47
  let interactionTime = 0
48
  let lastMouseMove = 0
49
 
 
89
 
90
  setTimeout(() => {
91
  if (new Date().getTime() - interactionTime > 350) {
92
+ // Reset interaction flag
93
+ // Double-click detection works by checking if interaction is still true
94
+ // when the second click happens (within 400ms)
95
+ // We don't toggle pause anymore - only show/hide controls on single tap
 
 
 
 
 
 
 
96
  interaction = false
97
  }
98
  }, 400)
 
109
  return paused && progress > duration - SYNC_DELTA
110
  }
111
 
112
+ const openPipFallback = () => {
113
+ // Open a small popup window as PiP fallback
114
+ const width = 480
115
+ const height = 270
116
+ const left = window.screen.width - width - 20
117
+ const top = window.screen.height - height - 100
118
+
119
+ const pipWindow = window.open(
120
+ `/embed/${roomId}`,
121
+ 'PiP Player',
122
+ `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=no,status=no,toolbar=no,menubar=no,location=no`
123
+ )
124
+
125
+ if (pipWindow) {
126
+ pipWindow.focus()
127
+ console.log("Opened PiP fallback window")
128
+ } else {
129
+ console.error("Failed to open PiP fallback window - popup may be blocked")
130
+ alert("Please allow popups to use Picture-in-Picture mode")
131
+ }
132
+ }
133
+
134
  const mouseMoved = (touch: boolean | null = null) => {
135
  lastMouseMove = new Date().getTime()
136
 
 
151
  <InteractionHandler
152
  className={classNames(
153
  "absolute top-0 left-0 w-full h-full transition-opacity flex flex-col",
154
+ show ? "opacity-100" : "opacity-0",
155
+ fullscreen ? "controls-fullscreen" : ""
156
  )}
157
  onMove={(_, touch) => {
158
  setShowControls(!touch)
 
171
  }
172
  onClick={(_, touch) => {
173
  if (interaction) {
174
+ // Second click detected within timeout - toggle fullscreen
175
  interaction = false
176
  console.log("Toggled fullscreen")
177
  setFullscreen(!fullscreen)
178
  } else if (touch) {
179
+ // Single touch - toggle controls visibility
180
  setShowControls(!showControls)
181
  setMenuOpen(false)
182
  }
 
321
 
322
  <ControlButton
323
  tooltip={pipEnabled ? "Exit PiP" : "Enter PiP"}
324
+ onClick={async () => {
325
+ if (pipEnabled) {
326
+ // Exit PiP
327
+ setPipEnabled(false)
328
+ if (document.pictureInPictureElement) {
329
+ try {
330
+ await document.exitPictureInPicture()
331
+ } catch (err) {
332
+ console.warn("Failed to exit PiP:", err)
333
+ }
334
+ }
335
+ } else {
336
+ // Try to enter PiP
337
+ const isYouTube = currentSrc.src.includes('youtube.com') || currentSrc.src.includes('youtu.be')
338
+
339
+ if (isYouTube) {
340
+ // For YouTube, use ReactPlayer's pip prop
341
+ setPipEnabled(true)
342
+ if (!pipEnabled && musicMode) {
343
+ setMusicMode(false)
344
+ }
345
+ } else {
346
+ // For file sources, try native PiP API
347
+ const videoElement = document.querySelector('video')
348
+
349
+ if (videoElement && 'requestPictureInPicture' in videoElement && document.pictureInPictureEnabled) {
350
+ try {
351
+ await videoElement.requestPictureInPicture()
352
+ setPipEnabled(true)
353
+ if (musicMode) {
354
+ setMusicMode(false)
355
+ }
356
+ } catch (err) {
357
+ console.warn("Native PiP failed, trying fallback:", err)
358
+ openPipFallback()
359
+ }
360
+ } else {
361
+ // Fallback: open popup window
362
+ openPipFallback()
363
+ }
364
+ }
365
  }
366
  }}
367
  interaction={showControlsAction}
components/player/Player.tsx CHANGED
@@ -265,14 +265,14 @@ const Player: FC<Props> = ({ roomId, socket, fullHeight }) => {
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%"}
275
- height={fullscreen || fullHeight ? "100vh" : "calc((9 / 16) * 100vw)"}
276
  config={{
277
  youtube: {
278
  playerVars: {
 
265
  </div>
266
  )}
267
  <ReactPlayer
268
+ className={fullscreen || fullHeight ? "video-fullscreen" : "video-normal"}
269
  style={{
 
270
  visibility: musicMode ? "hidden" : "visible",
271
+ height: musicMode ? "0px" : undefined,
272
  }}
273
  ref={player}
274
  width={"100%"}
275
+ height={fullscreen || fullHeight ? "100dvh" : "calc((9 / 16) * 100vw)"}
276
  config={{
277
  youtube: {
278
  playerVars: {
pages/api/socketio.ts CHANGED
@@ -197,20 +197,29 @@ const ioHandler = (_: NextApiRequest, res: NextApiResponse) => {
197
  log("playback ended")
198
 
199
  if (room.targetState.loop) {
 
200
  room.targetState.progress = 0
201
  room.targetState.paused = false
202
  } else if (
203
  room.targetState.playlist.currentIndex + 1 <
204
  room.targetState.playlist.items.length
205
  ) {
 
206
  room.targetState.playing =
207
  room.targetState.playlist.items[
208
  room.targetState.playlist.currentIndex + 1
209
  ]
210
- room.targetState.playlist.currentIndex += 1
 
 
 
 
 
211
  room.targetState.progress = 0
212
  room.targetState.paused = false
 
213
  } else {
 
214
  room.targetState.progress =
215
  room.users.find((user) => user.socketIds[0] === socket.id)?.player
216
  .progress || 0
 
197
  log("playback ended")
198
 
199
  if (room.targetState.loop) {
200
+ // Loop mode: restart the current video without modifying playlist
201
  room.targetState.progress = 0
202
  room.targetState.paused = false
203
  } else if (
204
  room.targetState.playlist.currentIndex + 1 <
205
  room.targetState.playlist.items.length
206
  ) {
207
+ // Auto-advance to next item: play next video and remove finished one
208
  room.targetState.playing =
209
  room.targetState.playlist.items[
210
  room.targetState.playlist.currentIndex + 1
211
  ]
212
+
213
+ // Remove the finished item from playlist (shift remaining items left)
214
+ room.targetState.playlist.items.splice(room.targetState.playlist.currentIndex, 1)
215
+
216
+ // Reset currentIndex to 0 since items have shifted
217
+ room.targetState.playlist.currentIndex = 0
218
  room.targetState.progress = 0
219
  room.targetState.paused = false
220
+ log("Removed finished item from playlist, shifted remaining items")
221
  } else {
222
+ // Last item finished: pause at end
223
  room.targetState.progress =
224
  room.users.find((user) => user.socketIds[0] === socket.id)?.player
225
  .progress || 0
pages/global.css CHANGED
@@ -72,3 +72,47 @@ input:focus, textarea:focus {
72
  html {
73
  scroll-behavior: smooth;
74
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  html {
73
  scroll-behavior: smooth;
74
  }
75
+
76
+ /* Fullscreen video player styles for mobile */
77
+ .video-fullscreen {
78
+ height: 100dvh !important;
79
+ max-height: 100dvh !important;
80
+ width: 100vw !important;
81
+ }
82
+
83
+ /* Support for older browsers that don't support dvh */
84
+ @supports not (height: 100dvh) {
85
+ .video-fullscreen {
86
+ height: 100vh !important;
87
+ max-height: 100vh !important;
88
+ }
89
+ }
90
+
91
+ /* Safe area handling for iOS notches */
92
+ .video-fullscreen {
93
+ padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
94
+ }
95
+
96
+ /* Non-fullscreen video styles */
97
+ .video-normal {
98
+ height: calc((9 / 16) * 100vw);
99
+ max-height: calc(100vh - 210px);
100
+ max-height: calc(100dvh - 210px);
101
+ }
102
+
103
+ @supports not (height: 100dvh) {
104
+ .video-normal {
105
+ max-height: calc(100vh - 210px);
106
+ }
107
+ }
108
+
109
+ /* Controls overlay improvements for fullscreen */
110
+ .controls-fullscreen {
111
+ position: absolute;
112
+ top: 0;
113
+ left: 0;
114
+ right: 0;
115
+ bottom: 0;
116
+ z-index: 10;
117
+ padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
118
+ }