forked from lavender/watch-party
		
	use plyr for video controls
This commit is contained in:
		
							parent
							
								
									e43184ab49
								
							
						
					
					
						commit
						1bd7071cec
					
				
					 16 changed files with 1416 additions and 1437 deletions
				
			
		|  | @ -3,6 +3,7 @@ | ||||||
|   <head> |   <head> | ||||||
|     <meta charset="utf-8" /> |     <meta charset="utf-8" /> | ||||||
|     <title>watch party :D</title> |     <title>watch party :D</title> | ||||||
|  |     <link rel="stylesheet" href="/lib/plyr-3.7.3.css" /> | ||||||
|     <link rel="stylesheet" href="/styles.css?v=bfdcf2" /> |     <link rel="stylesheet" href="/styles.css?v=bfdcf2" /> | ||||||
|   </head> |   </head> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ import { | ||||||
|   setDebounce, |   setDebounce, | ||||||
|   setVideoTime, |   setVideoTime, | ||||||
|   setPlaying, |   setPlaying, | ||||||
|  |   sync, | ||||||
| } from "./watch-session.mjs?v=bfdcf2"; | } from "./watch-session.mjs?v=bfdcf2"; | ||||||
| import { emojify, findEmojis } from "./emojis.mjs?v=bfdcf2"; | import { emojify, findEmojis } from "./emojis.mjs?v=bfdcf2"; | ||||||
| import { linkify } from "./links.mjs?v=bfdcf2"; | import { linkify } from "./links.mjs?v=bfdcf2"; | ||||||
|  | @ -164,14 +165,7 @@ const setupChatboxEvents = (socket) => { | ||||||
|             handled = true; |             handled = true; | ||||||
|             break; |             break; | ||||||
|           case "/sync": |           case "/sync": | ||||||
|             const sessionId = window.location.hash.slice(1); |             await sync(); | ||||||
|             const { current_time_ms, is_playing } = await fetch( |  | ||||||
|               `/sess/${sessionId}` |  | ||||||
|             ).then((r) => r.json()); |  | ||||||
| 
 |  | ||||||
|             setDebounce(); |  | ||||||
|             setPlaying(is_playing); |  | ||||||
|             setVideoTime(current_time_ms); |  | ||||||
| 
 | 
 | ||||||
|             const syncMessageContent = document.createElement("span"); |             const syncMessageContent = document.createElement("span"); | ||||||
|             syncMessageContent.appendChild( |             syncMessageContent.appendChild( | ||||||
|  |  | ||||||
|  | @ -54,11 +54,13 @@ const displayPostCreateMessage = () => { | ||||||
|   if (params.get("created") == "true") { |   if (params.get("created") == "true") { | ||||||
|     document.querySelector("#post-create-message").style["display"] = "block"; |     document.querySelector("#post-create-message").style["display"] = "block"; | ||||||
|     window.history.replaceState({}, document.title, `/${window.location.hash}`); |     window.history.replaceState({}, document.title, `/${window.location.hash}`); | ||||||
|  |     return true; | ||||||
|   } |   } | ||||||
|  |   return false; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const setupJoinSessionForm = () => { | export const setupJoinSessionForm = () => { | ||||||
|   displayPostCreateMessage(); |   const created = displayPostCreateMessage(); | ||||||
| 
 | 
 | ||||||
|   const form = document.querySelector("#join-session-form"); |   const form = document.querySelector("#join-session-form"); | ||||||
|   const nickname = form.querySelector("#join-session-nickname"); |   const nickname = form.querySelector("#join-session-nickname"); | ||||||
|  | @ -84,7 +86,7 @@ export const setupJoinSessionForm = () => { | ||||||
|       state().nickname = nickname.value; |       state().nickname = nickname.value; | ||||||
|       state().sessionId = sessionId.value; |       state().sessionId = sessionId.value; | ||||||
|       state().colour = colour.value.replace(/^#/, ""); |       state().colour = colour.value.replace(/^#/, ""); | ||||||
|       await joinSession(); |       await joinSession(created); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       alert(e.message); |       alert(e.message); | ||||||
|       button.disabled = false; |       button.disabled = false; | ||||||
|  |  | ||||||
|  | @ -77,4 +77,3 @@ export const pling = () => { | ||||||
|   thirdBeep.start(ctx.currentTime + thirdBeepOffset); |   thirdBeep.start(ctx.currentTime + thirdBeepOffset); | ||||||
|   thirdBeep.stop(ctx.currentTime + (thirdBeepOffset + duration)); |   thirdBeep.stop(ctx.currentTime + (thirdBeepOffset + duration)); | ||||||
| }; | }; | ||||||
| 
 |  | ||||||
|  |  | ||||||
							
								
								
									
										1
									
								
								frontend/lib/plyr-3.7.3.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/lib/plyr-3.7.3.css
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								frontend/lib/plyr-3.7.3.min.esm.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/lib/plyr-3.7.3.min.esm.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							|  | @ -1,110 +1,56 @@ | ||||||
| const loadVolume = () => { | import Plyr from "./plyr-3.7.3.min.esm.js"; | ||||||
|   try { |  | ||||||
|     const savedVolume = localStorage.getItem("watch-party-volume"); |  | ||||||
|     if (savedVolume != null && savedVolume != "") { |  | ||||||
|       return +savedVolume; |  | ||||||
|     } |  | ||||||
|   } catch (_err) { |  | ||||||
|     // Sometimes localStorage is blocked from use
 |  | ||||||
|   } |  | ||||||
|   // default
 |  | ||||||
|   return 0.5; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * @param {number} volume |  | ||||||
|  */ |  | ||||||
| const saveVolume = (volume) => { |  | ||||||
|   try { |  | ||||||
|     localStorage.setItem("watch-party-volume", volume); |  | ||||||
|   } catch (_err) { |  | ||||||
|     // see loadVolume
 |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const loadCaptionTrack = () => { |  | ||||||
|   try { |  | ||||||
|     const savedTrack = localStorage.getItem("watch-party-captions"); |  | ||||||
|     if (savedTrack != null && savedTrack != "") { |  | ||||||
|       return +savedTrack; |  | ||||||
|     } |  | ||||||
|   } catch (_err) { |  | ||||||
|     // Sometimes localStorage is blocked from use
 |  | ||||||
|   } |  | ||||||
|   // default
 |  | ||||||
|   return -1; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * @param {number} track |  | ||||||
|  */ |  | ||||||
| const saveCaptionsTrack = (track) => { |  | ||||||
|   try { |  | ||||||
|     localStorage.setItem("watch-party-captions", track); |  | ||||||
|   } catch (_err) { |  | ||||||
|     // see loadCaptionsTrack
 |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * @param {string} videoUrl |  * @param {string} videoUrl | ||||||
|  * @param {{name: string, url: string}[]} subtitles |  * @param {{name: string, url: string}[]} subtitles | ||||||
|  */ |  */ | ||||||
| const createVideoElement = (videoUrl, subtitles) => { | const createVideoElement = (videoUrl, subtitles, created) => { | ||||||
|   const oldVideo = document.getElementById("video"); |   const oldVideo = document.getElementById(".plyr"); | ||||||
|   if (oldVideo) { |   if (oldVideo) { | ||||||
|     oldVideo.remove(); |     oldVideo.remove(); | ||||||
|   } |   } | ||||||
|   const video = document.createElement("video"); |   const video = document.createElement("video"); | ||||||
|   video.id = "video"; |   video.id = "video"; | ||||||
|   video.controls = true; |  | ||||||
|   video.autoplay = false; |  | ||||||
|   video.volume = loadVolume(); |  | ||||||
|   video.crossOrigin = "anonymous"; |   video.crossOrigin = "anonymous"; | ||||||
| 
 | 
 | ||||||
|   video.addEventListener("volumechange", async () => { |  | ||||||
|     saveVolume(video.volume); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   const source = document.createElement("source"); |   const source = document.createElement("source"); | ||||||
|   source.src = videoUrl; |   source.src = videoUrl; | ||||||
| 
 | 
 | ||||||
|   video.appendChild(source); |   video.appendChild(source); | ||||||
| 
 | 
 | ||||||
|   const storedTrack = loadCaptionTrack(); |  | ||||||
|   let id = 0; |  | ||||||
|   for (const { name, url } of subtitles) { |   for (const { name, url } of subtitles) { | ||||||
|     const track = document.createElement("track"); |     const track = document.createElement("track"); | ||||||
|     track.label = name; |     track.label = name; | ||||||
|  |     track.srclang = "xx-" + name.toLowerCase(); | ||||||
|     track.src = url; |     track.src = url; | ||||||
|     track.kind = "captions"; |     track.kind = "captions"; | ||||||
| 
 |  | ||||||
|     if (id == storedTrack || storedTrack == -1) { |  | ||||||
|       track.default = true; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     video.appendChild(track); |     video.appendChild(track); | ||||||
|     id++; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   video.textTracks.addEventListener("change", async () => { |   const videoContainer = document.querySelector("#video-container"); | ||||||
|     let id = 0; |   videoContainer.style.display = "block"; | ||||||
|     for (const track of video.textTracks) { |   videoContainer.appendChild(video); | ||||||
|       if (track.mode != "disabled") { | 
 | ||||||
|         saveCaptionsTrack(id); |   const player = new Plyr(video, { | ||||||
|         return; |     clickToPlay: false, | ||||||
|       } |     settings: ["captions", "quality"], | ||||||
|       id++; |     autopause: false, | ||||||
|     } |  | ||||||
|     saveCaptionsTrack(-1); |  | ||||||
|   }); |   }); | ||||||
| 
 |   player.elements.controls.insertAdjacentHTML( | ||||||
|   // watch for attribute changes on the video object to detect hiding/showing of controls
 |     "afterbegin", | ||||||
|   // as far as i can tell this is the least hacky solutions to get control visibility change events
 |     `<button type="button" aria-pressed="false" class="plyr__controls__item plyr__control lock-controls"><svg aria-hidden="true" focusable="false" viewBox="0 0 24 24"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zM9 8V6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9z"></path></svg><span class="label--pressed plyr__sr-only">Unlock controls</span><span class="label--not-pressed plyr__sr-only">Lock controls</span></button>` | ||||||
|   const observer = new MutationObserver(async (mutations) => { |   ); | ||||||
|     for (const mutation of mutations) { |   const lockButton = player.elements.controls.children[0]; | ||||||
|       if (mutation.attributeName == "controls") { |   let controlsEnabled = created; | ||||||
|         if (video.controls) { |   const setControlsEnabled = (enabled) => { | ||||||
|  |     controlsEnabled = enabled; | ||||||
|  |     lockButton.setAttribute("aria-pressed", enabled); | ||||||
|  |     lockButton.classList.toggle("plyr__control--pressed", enabled); | ||||||
|  |     player.elements.buttons.play[0].disabled = | ||||||
|  |       player.elements.buttons.play[1].disabled = | ||||||
|  |       player.elements.inputs.seek.disabled = | ||||||
|  |         !enabled; | ||||||
|  |     if (!enabled) { | ||||||
|       // enable media button support
 |       // enable media button support
 | ||||||
|       navigator.mediaSession.setActionHandler("play", null); |       navigator.mediaSession.setActionHandler("play", null); | ||||||
|       navigator.mediaSession.setActionHandler("pause", null); |       navigator.mediaSession.setActionHandler("pause", null); | ||||||
|  | @ -125,13 +71,14 @@ const createVideoElement = (videoUrl, subtitles) => { | ||||||
|       navigator.mediaSession.setActionHandler("previoustrack", () => {}); |       navigator.mediaSession.setActionHandler("previoustrack", () => {}); | ||||||
|       navigator.mediaSession.setActionHandler("nexttrack", () => {}); |       navigator.mediaSession.setActionHandler("nexttrack", () => {}); | ||||||
|     } |     } | ||||||
|         return; |   }; | ||||||
|       } |   setControlsEnabled(controlsEnabled); | ||||||
|     } |   lockButton.addEventListener("click", () => | ||||||
|   }); |     setControlsEnabled(!controlsEnabled) | ||||||
|   observer.observe(video, { attributes: true }); |   ); | ||||||
|  |   window.__plyr = player; | ||||||
| 
 | 
 | ||||||
|   return video; |   return player; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -140,24 +87,26 @@ const createVideoElement = (videoUrl, subtitles) => { | ||||||
|  * @param {number} currentTime |  * @param {number} currentTime | ||||||
|  * @param {boolean} playing |  * @param {boolean} playing | ||||||
|  */ |  */ | ||||||
| export const setupVideo = async (videoUrl, subtitles, currentTime, playing) => { | export const setupVideo = async ( | ||||||
|  |   videoUrl, | ||||||
|  |   subtitles, | ||||||
|  |   currentTime, | ||||||
|  |   playing, | ||||||
|  |   created | ||||||
|  | ) => { | ||||||
|   document.querySelector("#pre-join-controls").style["display"] = "none"; |   document.querySelector("#pre-join-controls").style["display"] = "none"; | ||||||
|   const video = createVideoElement(videoUrl, subtitles); |   const player = createVideoElement(videoUrl, subtitles, created); | ||||||
|   const videoContainer = document.querySelector("#video-container"); |   player.currentTime = currentTime / 1000.0; | ||||||
|   videoContainer.style.display = "block"; |  | ||||||
|   videoContainer.appendChild(video); |  | ||||||
| 
 |  | ||||||
|   video.currentTime = currentTime / 1000.0; |  | ||||||
| 
 | 
 | ||||||
|   try { |   try { | ||||||
|     if (playing) { |     if (playing) { | ||||||
|       await video.play(); |       player.play(); | ||||||
|     } else { |     } else { | ||||||
|       video.pause(); |       player.pause(); | ||||||
|     } |     } | ||||||
|   } catch (err) { |   } catch (err) { | ||||||
|     // Auto-play is probably disabled, we should uhhhhhhh do something about it
 |     // Auto-play is probably disabled, we should uhhhhhhh do something about it
 | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   return video; |   return player; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -7,7 +7,7 @@ import { | ||||||
| } from "./chat.mjs?v=bfdcf2"; | } from "./chat.mjs?v=bfdcf2"; | ||||||
| import ReconnectingWebSocket from "./reconnecting-web-socket.mjs"; | import ReconnectingWebSocket from "./reconnecting-web-socket.mjs"; | ||||||
| import { state } from "./state.mjs"; | import { state } from "./state.mjs"; | ||||||
| 
 | let player; | ||||||
| /** | /** | ||||||
|  * @param {string} sessionId |  * @param {string} sessionId | ||||||
|  * @param {string} nickname |  * @param {string} nickname | ||||||
|  | @ -42,26 +42,18 @@ export const setDebounce = () => { | ||||||
|   }, 500); |   }, 500); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const setVideoTime = (time, video = null) => { | export const setVideoTime = (time) => { | ||||||
|   if (video == null) { |  | ||||||
|     video = document.querySelector("video"); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   const timeSecs = time / 1000.0; |   const timeSecs = time / 1000.0; | ||||||
|   if (Math.abs(video.currentTime - timeSecs) > 0.5) { |   if (Math.abs(player.currentTime - timeSecs) > 0.5) { | ||||||
|     video.currentTime = timeSecs; |     player.currentTime = timeSecs; | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const setPlaying = async (playing, video = null) => { | export const setPlaying = async (playing) => { | ||||||
|   if (video == null) { |  | ||||||
|     video = document.querySelector("video"); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   if (playing) { |   if (playing) { | ||||||
|     await video.play(); |     await player.play(); | ||||||
|   } else { |   } else { | ||||||
|     video.pause(); |     player.pause(); | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | @ -69,7 +61,7 @@ export const setPlaying = async (playing, video = null) => { | ||||||
|  * @param {HTMLVideoElement} video |  * @param {HTMLVideoElement} video | ||||||
|  * @param {ReconnectingWebSocket} socket |  * @param {ReconnectingWebSocket} socket | ||||||
|  */ |  */ | ||||||
| const setupIncomingEvents = (video, socket) => { | const setupIncomingEvents = (player, socket) => { | ||||||
|   socket.addEventListener("message", async (messageEvent) => { |   socket.addEventListener("message", async (messageEvent) => { | ||||||
|     try { |     try { | ||||||
|       const event = JSON.parse(messageEvent.data); |       const event = JSON.parse(messageEvent.data); | ||||||
|  | @ -79,16 +71,16 @@ const setupIncomingEvents = (video, socket) => { | ||||||
|             setDebounce(); |             setDebounce(); | ||||||
| 
 | 
 | ||||||
|             if (event.data.playing) { |             if (event.data.playing) { | ||||||
|               await video.play(); |               await player.play(); | ||||||
|             } else { |             } else { | ||||||
|               video.pause(); |               player.pause(); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             setVideoTime(event.data.time, video); |             setVideoTime(event.data.time); | ||||||
|             break; |             break; | ||||||
|           case "SetTime": |           case "SetTime": | ||||||
|             setDebounce(); |             setDebounce(); | ||||||
|             setVideoTime(event.data, video); |             setVideoTime(event.data); | ||||||
|             break; |             break; | ||||||
|           case "UpdateViewerList": |           case "UpdateViewerList": | ||||||
|             updateViewerList(event.data); |             updateViewerList(event.data); | ||||||
|  | @ -102,19 +94,19 @@ const setupIncomingEvents = (video, socket) => { | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * @param {HTMLVideoElement} video |  * @param {Plyr} player | ||||||
|  * @param {ReconnectingWebSocket} socket |  * @param {ReconnectingWebSocket} socket | ||||||
|  */ |  */ | ||||||
| const setupOutgoingEvents = (video, socket) => { | const setupOutgoingEvents = (player, socket) => { | ||||||
|   const currentVideoTime = () => (video.currentTime * 1000) | 0; |   const currentVideoTime = () => (player.currentTime * 1000) | 0; | ||||||
| 
 | 
 | ||||||
|   video.addEventListener("pause", async (event) => { |   player.on("pause", async () => { | ||||||
|     if (outgoingDebounce || !video.controls) { |     if (outgoingDebounce || player.elements.inputs.seek.disabled) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // don't send a pause event for the video ending
 |     // don't send a pause event for the video ending
 | ||||||
|     if (video.currentTime == video.duration) { |     if (player.currentTime == player.duration) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -129,8 +121,8 @@ const setupOutgoingEvents = (video, socket) => { | ||||||
|     ); |     ); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   video.addEventListener("play", (event) => { |   player.on("play", () => { | ||||||
|     if (outgoingDebounce || !video.controls) { |     if (outgoingDebounce || player.elements.inputs.seek.disabled) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -146,14 +138,14 @@ const setupOutgoingEvents = (video, socket) => { | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   let firstSeekComplete = false; |   let firstSeekComplete = false; | ||||||
|   video.addEventListener("seeked", async (event) => { |   player.on("seeked", async (event) => { | ||||||
|     if (!firstSeekComplete) { |     if (!firstSeekComplete) { | ||||||
|       // The first seeked event is performed by the browser when the video is loading
 |       // The first seeked event is performed by the browser when the video is loading
 | ||||||
|       firstSeekComplete = true; |       firstSeekComplete = true; | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (outgoingDebounce || !video.controls) { |     if (outgoingDebounce || player.elements.inputs.seek.disabled) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -168,7 +160,7 @@ const setupOutgoingEvents = (video, socket) => { | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const joinSession = async () => { | export const joinSession = async (created) => { | ||||||
|   if (state().activeSession) { |   if (state().activeSession) { | ||||||
|     if (state().activeSession === state().sessionId) { |     if (state().activeSession === state().sessionId) { | ||||||
|       // we are already in this session, dont rejoin
 |       // we are already in this session, dont rejoin
 | ||||||
|  | @ -221,30 +213,20 @@ export const joinSession = async () => { | ||||||
|   const socket = createWebSocket(); |   const socket = createWebSocket(); | ||||||
|   state().socket = socket; |   state().socket = socket; | ||||||
|   socket.addEventListener("open", async () => { |   socket.addEventListener("open", async () => { | ||||||
|     const video = await setupVideo( |     player = await setupVideo( | ||||||
|       video_url, |       video_url, | ||||||
|       subtitle_tracks, |       subtitle_tracks, | ||||||
|       current_time_ms, |       current_time_ms, | ||||||
|       is_playing |       is_playing, | ||||||
|  |       created | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     // TODO: Allow the user to set this somewhere
 |     player.on("canplay", () => { | ||||||
|     let defaultAllowControls = false; |       sync(); | ||||||
|     try { |     }); | ||||||
|       defaultAllowControls = localStorage.getItem( |  | ||||||
|         "watch-party-default-allow-controls" |  | ||||||
|       ); |  | ||||||
|     } catch (_err) {} |  | ||||||
| 
 | 
 | ||||||
|     // By default, we should disable video controls if the video is already playing.
 |     setupOutgoingEvents(player, socket); | ||||||
|     // This solves an issue where Safari users join and seek to 00:00:00 because of
 |     setupIncomingEvents(player, socket); | ||||||
|     // outgoing events.
 |  | ||||||
|     if (current_time_ms != 0 || !defaultAllowControls) { |  | ||||||
|       video.controls = false; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     setupOutgoingEvents(video, socket); |  | ||||||
|     setupIncomingEvents(video, socket); |  | ||||||
|     setupChat(socket); |     setupChat(socket); | ||||||
|   }); |   }); | ||||||
|   socket.addEventListener("reconnecting", (e) => { |   socket.addEventListener("reconnecting", (e) => { | ||||||
|  | @ -274,3 +256,15 @@ export const createSession = async (videoUrl, subtitleTracks) => { | ||||||
| 
 | 
 | ||||||
|   window.location = `/?created=true#${id}`; |   window.location = `/?created=true#${id}`; | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | export const sync = async () => { | ||||||
|  |   setDebounce(); | ||||||
|  |   await setPlaying(false); | ||||||
|  |   const { current_time_ms, is_playing } = await fetch( | ||||||
|  |     `/sess/${state().sessionId}` | ||||||
|  |   ).then((r) => r.json()); | ||||||
|  | 
 | ||||||
|  |   setDebounce(); | ||||||
|  |   setVideoTime(current_time_ms); | ||||||
|  |   if (is_playing) await setPlaying(is_playing); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | @ -25,6 +25,14 @@ | ||||||
|     ), |     ), | ||||||
|     linear-gradient(var(--bg), var(--bg)); |     linear-gradient(var(--bg), var(--bg)); | ||||||
|   --accent-transparent: rgba(var(--accent-rgb), 0.25); |   --accent-transparent: rgba(var(--accent-rgb), 0.25); | ||||||
|  |   --plyr-color-main: var(--accent); | ||||||
|  |   --plyr-control-radius: 6px; | ||||||
|  |   --plyr-menu-radius: 6px; | ||||||
|  |   --plyr-menu-background: var(--autocomplete-bg); | ||||||
|  |   --plyr-menu-color: var(--fg); | ||||||
|  |   --plyr-menu-arrow-color: var(--fg); | ||||||
|  |   --plyr-menu-back-border-color: var(--fg-transparent); | ||||||
|  |   --plyr-menu-back-border-shadow-color: transparent; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| html { | html { | ||||||
|  | @ -49,11 +57,41 @@ body { | ||||||
|   flex-direction: column; |   flex-direction: column; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| video { | .lock-controls.plyr__control--pressed svg { | ||||||
|   display: block; |   opacity: 0.5; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .plyr { | ||||||
|   width: 100%; |   width: 100%; | ||||||
|   height: 100%; |   height: 100%; | ||||||
|   object-fit: contain; | } | ||||||
|  | 
 | ||||||
|  | .plyr__menu__container { | ||||||
|  |   --plyr-video-control-background-hover: var(--fg-transparent); | ||||||
|  |   --plyr-video-control-color-hover: var(--fg); | ||||||
|  |   --plyr-control-radius: 4px; | ||||||
|  |   --plyr-control-spacing: calc(0.25rem / 0.7); | ||||||
|  |   --plyr-font-size-menu: 0.75rem; | ||||||
|  |   --plyr-menu-arrow-size: 0; | ||||||
|  |   margin-bottom: 0.48rem; | ||||||
|  |   max-height: 27vmin; | ||||||
|  |   clip-path: inset(0 0 0 0 round 4px); | ||||||
|  |   scrollbar-width: thin; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .plyr__menu__container .plyr__control[role="menuitemradio"]::after { | ||||||
|  |   left: 10px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .plyr__menu__container | ||||||
|  |   .plyr__control[role="menuitemradio"][aria-checked="true"].plyr__tab-focus::before, | ||||||
|  | .plyr__menu__container | ||||||
|  |   .plyr__control[role="menuitemradio"][aria-checked="true"]:hover::before { | ||||||
|  |   background: var(--accent); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | [data-plyr="language"] .plyr__menu__value { | ||||||
|  |   display: none; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #video-container { | #video-container { | ||||||
|  | @ -131,7 +169,7 @@ input[type="text"] { | ||||||
|   overflow-y: scroll; |   overflow-y: scroll; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| button { | button:not(.plyr button) { | ||||||
|   background-color: var(--accent); |   background-color: var(--accent); | ||||||
|   border: var(--accent); |   border: var(--accent); | ||||||
|   border-radius: 6px; |   border-radius: 6px; | ||||||
|  | @ -303,7 +341,7 @@ button.small-button { | ||||||
|   display: none; |   display: none; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .emoji-option { | .emoji-option:not(:root) { | ||||||
|   background: transparent; |   background: transparent; | ||||||
|   font-size: 0.75rem; |   font-size: 0.75rem; | ||||||
|   text-align: left; |   text-align: left; | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue