Compare commits
	
		
			1 commit
		
	
	
		
			main
			...
			options-pa
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | bcd36e2269 | 
					 22 changed files with 2500 additions and 2324 deletions
				
			
		|  | @ -3,7 +3,7 @@ root = true | ||||||
| [*] | [*] | ||||||
| indent_style = space | indent_style = space | ||||||
| indent_size = 2 | indent_size = 2 | ||||||
| end_of_line = lf | end_of_line = crlf | ||||||
| charset = utf-8 | charset = utf-8 | ||||||
| trim_trailing_whitespace = false | trim_trailing_whitespace = false | ||||||
| insert_final_newline = true | insert_final_newline = true | ||||||
|  |  | ||||||
|  | @ -3,7 +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="/styles.css?v=4b61c4" /> |     <link rel="stylesheet" href="/styles.css?v=bfdcf2" /> | ||||||
|   </head> |   </head> | ||||||
| 
 | 
 | ||||||
|   <body> |   <body> | ||||||
|  | @ -47,6 +47,6 @@ | ||||||
|       </form> |       </form> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <script type="module" src="/create.mjs?v=4b61c4"></script> |     <script type="module" src="/create.mjs?v=bfdcf2"></script> | ||||||
|   </body> |   </body> | ||||||
| </html> | </html> | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { setupCreateSessionForm } from "./lib/create-session.mjs?v=4b61c4"; | import { setupCreateSessionForm } from "./lib/create-session.mjs?v=bfdcf2"; | ||||||
| 
 | 
 | ||||||
| const main = () => { | const main = () => { | ||||||
|   setupCreateSessionForm(); |   setupCreateSessionForm(); | ||||||
|  |  | ||||||
|  | @ -3,8 +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=4b61c4" /> |  | ||||||
|   </head> |   </head> | ||||||
| 
 | 
 | ||||||
|   <body> |   <body> | ||||||
|  | @ -54,6 +53,7 @@ | ||||||
| 
 | 
 | ||||||
|     <div id="video-container"></div> |     <div id="video-container"></div> | ||||||
|     <div id="chatbox-container"> |     <div id="chatbox-container"> | ||||||
|  |       <section id="viewing"> | ||||||
|         <div id="viewer-list"></div> |         <div id="viewer-list"></div> | ||||||
|         <div id="chatbox"></div> |         <div id="chatbox"></div> | ||||||
|         <form id="chatbox-send"> |         <form id="chatbox-send"> | ||||||
|  | @ -65,9 +65,43 @@ | ||||||
|           <div id="emoji-autocomplete"></div> |           <div id="emoji-autocomplete"></div> | ||||||
|           <!-- DO NOT ADD SPACING INSIDE THE TAG IT WILL BREAK THE CSS kthxbye --> |           <!-- DO NOT ADD SPACING INSIDE THE TAG IT WILL BREAK THE CSS kthxbye --> | ||||||
|         </form> |         </form> | ||||||
|  |       </section> | ||||||
|  |       <section id="options"> | ||||||
|  |         <h2>settings</h2> | ||||||
|  |         <hr /> | ||||||
|  |         <form id="options-form"> | ||||||
|  |           <label for="plingVolume" | ||||||
|  |             ><input | ||||||
|  |               type="range" | ||||||
|  |               min="0" | ||||||
|  |               max="100" | ||||||
|  |               value="100" | ||||||
|  |               id="plingVolume" | ||||||
|  |               onchange="handlePlingVolume(this)" | ||||||
|  |             /> | ||||||
|  |             ping volume</label | ||||||
|  |           > | ||||||
|  |           <label | ||||||
|  |           ><input | ||||||
|  |             id="playerControlsShown" | ||||||
|  |             type="checkbox" | ||||||
|  |             onchange="togglePlayerControlsShown(this)" | ||||||
|  |           />hide controls when loading video player</label | ||||||
|  |         > | ||||||
|  |         </form> | ||||||
|  |       </section> | ||||||
|  |       <div id="options-toggle"> | ||||||
|  |         <button | ||||||
|  |           aria-label="settings" | ||||||
|  |           id="options-icon" | ||||||
|  |           onclick="toggleOptionPane(event, this)" | ||||||
|  |         > | ||||||
|  |           ⚙️ | ||||||
|  |         </button> | ||||||
|  |       </div> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <script type="module" src="/main.mjs?v=4b61c4"></script> |     <script type="module" src="/main.mjs?v=bfdcf2"></script> | ||||||
|     <script> |     <script> | ||||||
|       const updateColourLabel = () => { |       const updateColourLabel = () => { | ||||||
|         const colour = document.querySelector("#join-session-colour").value; |         const colour = document.querySelector("#join-session-colour").value; | ||||||
|  |  | ||||||
|  | @ -2,12 +2,11 @@ import { | ||||||
|   setDebounce, |   setDebounce, | ||||||
|   setVideoTime, |   setVideoTime, | ||||||
|   setPlaying, |   setPlaying, | ||||||
|   sync, | } from "./watch-session.mjs?v=bfdcf2"; | ||||||
| } from "./watch-session.mjs?v=4b61c4"; | import { emojify, findEmojis } from "./emojis.mjs?v=bfdcf2"; | ||||||
| import { emojify, findEmojis } from "./emojis.mjs?v=4b61c4"; | import { linkify } from "./links.mjs?v=bfdcf2"; | ||||||
| import { linkify } from "./links.mjs?v=4b61c4"; | import { joinSession } from "./watch-session.mjs?v=bfdcf2"; | ||||||
| import { joinSession } from "./watch-session.mjs?v=4b61c4"; | import { pling } from "./pling.mjs?v=bfdcf2"; | ||||||
| import { pling } from "./pling.mjs?v=4b61c4"; |  | ||||||
| import { state } from "./state.mjs"; | import { state } from "./state.mjs"; | ||||||
| 
 | 
 | ||||||
| function setCaretPosition(elem, caretPos) { | function setCaretPosition(elem, caretPos) { | ||||||
|  | @ -165,7 +164,14 @@ const setupChatboxEvents = (socket) => { | ||||||
|             handled = true; |             handled = true; | ||||||
|             break; |             break; | ||||||
|           case "/sync": |           case "/sync": | ||||||
|             await sync(); |             const sessionId = window.location.hash.slice(1); | ||||||
|  |             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( | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { createSession } from "./watch-session.mjs?v=4b61c4"; | import { createSession } from "./watch-session.mjs?v=bfdcf2"; | ||||||
| 
 | 
 | ||||||
| export const setupCreateSessionForm = () => { | export const setupCreateSessionForm = () => { | ||||||
|   const form = document.querySelector("#create-session-form"); |   const form = document.querySelector("#create-session-form"); | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { joinSession } from "./watch-session.mjs?v=4b61c4"; | import { joinSession } from "./watch-session.mjs?v=bfdcf2"; | ||||||
| import { state } from "./state.mjs"; | import { state } from "./state.mjs"; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -54,13 +54,11 @@ 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 = () => { | ||||||
|   const created = displayPostCreateMessage(); |   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"); | ||||||
|  | @ -86,7 +84,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(created); |       await joinSession(); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       alert(e.message); |       alert(e.message); | ||||||
|       button.disabled = false; |       button.disabled = false; | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { joinSession } from "./watch-session.mjs?v=4b61c4"; | import { joinSession } from "./watch-session.mjs?v=bfdcf2"; | ||||||
| import { state } from "./state.mjs"; | import { state } from "./state.mjs"; | ||||||
| 
 | 
 | ||||||
| export async function linkify( | export async function linkify( | ||||||
|  |  | ||||||
							
								
								
									
										43
									
								
								frontend/lib/options-pane.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								frontend/lib/options-pane.mjs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,43 @@ | ||||||
|  | export const toggleOptionPane = (event, element) => { | ||||||
|  |   event.preventDefault(); | ||||||
|  |   // show options
 | ||||||
|  |   if ( | ||||||
|  |     !document.querySelector("#options").style.display || | ||||||
|  |     document.querySelector("#options").style.display === "none" | ||||||
|  |   ) { | ||||||
|  |     // using this to do any potential init logic for the fields too
 | ||||||
|  |     loadPlayerControlsShown(document.querySelector("#playerControlsShown")) | ||||||
|  |     loadPlingVolume(document.querySelector("#plingVolume")) | ||||||
|  | 
 | ||||||
|  |     element.innerText = "❌"; | ||||||
|  |     document.querySelector("#options").style.display = "block"; | ||||||
|  |     return (document.querySelector("#viewing").style.display = "none"); | ||||||
|  |   } | ||||||
|  |   // hide options
 | ||||||
|  |   element.innerText = "⚙️"; | ||||||
|  |   document.querySelector("#options").style.display = "none"; | ||||||
|  |   document.querySelector("#viewing").style.display = "block"; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const getPlayerControlsShown = () =>  localStorage.getItem("watch-party-default-allow-controls") || false | ||||||
|  | // delete from storage on false to prevent weird js boolean parsing (Boolean('false') === True)
 | ||||||
|  | const setPlayerControlShown = (boolean) => !boolean  | ||||||
|  | ? localStorage.removeItem("watch-party-default-allow-controls") | ||||||
|  | : localStorage.setItem("watch-party-default-allow-controls", boolean) | ||||||
|  | export const togglePlayerControlsShown = (element) => { | ||||||
|  |   const isShown = element.checked | ||||||
|  |   setPlayerControlShown(!isShown) | ||||||
|  | } | ||||||
|  | const loadPlayerControlsShown = (element) => { | ||||||
|  |   const isShown = getPlayerControlsShown() | ||||||
|  |   element.checked = !isShown | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const getPlingVolume = () =>  localStorage.getItem("watch-party-pling-volume") || 100 | ||||||
|  | const setPlingVolume = (value) => localStorage.setItem("watch-party-pling-volume", value) | ||||||
|  | export const handlePlingVolume = (element) => { | ||||||
|  |   setPlingVolume(element.value) | ||||||
|  | } | ||||||
|  | const loadPlingVolume = (element) => { | ||||||
|  |   element.value = getPlingVolume() | ||||||
|  | } | ||||||
|  | @ -1,5 +1,8 @@ | ||||||
| export const pling = () => { | export const pling = () => { | ||||||
|   const maxGain = 0.3; |   // technically volume 0 breaks it but its the wanted outcome i guess?
 | ||||||
|  |   const maxGain = | ||||||
|  |     (Number(localStorage.getItem("watch-party-pling-volume")) / 100 ?? 1) * 0.3; | ||||||
|  | 
 | ||||||
|   const duration = 0.22; |   const duration = 0.22; | ||||||
|   const fadeDuration = 0.1; |   const fadeDuration = 0.1; | ||||||
|   const secondBeepOffset = 0.05; |   const secondBeepOffset = 0.05; | ||||||
|  |  | ||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							|  | @ -1,56 +1,110 @@ | ||||||
| import Plyr from "./plyr-3.7.3.min.esm.js"; | const loadVolume = () => { | ||||||
|  |   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, created) => { | const createVideoElement = (videoUrl, subtitles) => { | ||||||
|   const oldVideo = document.getElementById(".plyr"); |   const oldVideo = document.getElementById("video"); | ||||||
|   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"; | ||||||
|     video.appendChild(track); | 
 | ||||||
|  |     if (id == storedTrack || storedTrack == -1) { | ||||||
|  |       track.default = true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|   const videoContainer = document.querySelector("#video-container"); |     video.appendChild(track); | ||||||
|   videoContainer.style.display = "block"; |     id++; | ||||||
|   videoContainer.appendChild(video); |   } | ||||||
| 
 | 
 | ||||||
|   const player = new Plyr(video, { |   video.textTracks.addEventListener("change", async () => { | ||||||
|     clickToPlay: false, |     let id = 0; | ||||||
|     settings: ["captions", "quality"], |     for (const track of video.textTracks) { | ||||||
|     autopause: false, |       if (track.mode != "disabled") { | ||||||
|  |         saveCaptionsTrack(id); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       id++; | ||||||
|  |     } | ||||||
|  |     saveCaptionsTrack(-1); | ||||||
|   }); |   }); | ||||||
|   player.elements.controls.insertAdjacentHTML( | 
 | ||||||
|     "afterbegin", |   // watch for attribute changes on the video object to detect hiding/showing of controls
 | ||||||
|     `<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>` |   // as far as i can tell this is the least hacky solutions to get control visibility change events
 | ||||||
|   ); |   const observer = new MutationObserver(async (mutations) => { | ||||||
|   const lockButton = player.elements.controls.children[0]; |     for (const mutation of mutations) { | ||||||
|   let controlsEnabled = created; |       if (mutation.attributeName == "controls") { | ||||||
|   const setControlsEnabled = (enabled) => { |         if (video.controls) { | ||||||
|     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); | ||||||
|  | @ -71,14 +125,13 @@ const createVideoElement = (videoUrl, subtitles, created) => { | ||||||
|           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 player; |   return video; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -87,26 +140,24 @@ const createVideoElement = (videoUrl, subtitles, created) => { | ||||||
|  * @param {number} currentTime |  * @param {number} currentTime | ||||||
|  * @param {boolean} playing |  * @param {boolean} playing | ||||||
|  */ |  */ | ||||||
| export const setupVideo = async ( | export const setupVideo = async (videoUrl, subtitles, currentTime, playing) => { | ||||||
|   videoUrl, |  | ||||||
|   subtitles, |  | ||||||
|   currentTime, |  | ||||||
|   playing, |  | ||||||
|   created |  | ||||||
| ) => { |  | ||||||
|   document.querySelector("#pre-join-controls").style["display"] = "none"; |   document.querySelector("#pre-join-controls").style["display"] = "none"; | ||||||
|   const player = createVideoElement(videoUrl, subtitles, created); |   const video = createVideoElement(videoUrl, subtitles); | ||||||
|   player.currentTime = currentTime / 1000.0; |   const videoContainer = document.querySelector("#video-container"); | ||||||
|  |   videoContainer.style.display = "block"; | ||||||
|  |   videoContainer.appendChild(video); | ||||||
|  | 
 | ||||||
|  |   video.currentTime = currentTime / 1000.0; | ||||||
| 
 | 
 | ||||||
|   try { |   try { | ||||||
|     if (playing) { |     if (playing) { | ||||||
|       player.play(); |       await video.play(); | ||||||
|     } else { |     } else { | ||||||
|       player.pause(); |       video.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 player; |   return video; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -1,13 +1,13 @@ | ||||||
| import { setupVideo } from "./video.mjs?v=4b61c4"; | import { setupVideo } from "./video.mjs?v=bfdcf2"; | ||||||
| import { | import { | ||||||
|   setupChat, |   setupChat, | ||||||
|   logEventToChat, |   logEventToChat, | ||||||
|   updateViewerList, |   updateViewerList, | ||||||
|   printChatMessage, |   printChatMessage, | ||||||
| } from "./chat.mjs?v=4b61c4"; | } 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,18 +42,26 @@ export const setDebounce = () => { | ||||||
|   }, 500); |   }, 500); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const setVideoTime = (time) => { | export const setVideoTime = (time, video = null) => { | ||||||
|  |   if (video == null) { | ||||||
|  |     video = document.querySelector("video"); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   const timeSecs = time / 1000.0; |   const timeSecs = time / 1000.0; | ||||||
|   if (Math.abs(player.currentTime - timeSecs) > 0.5) { |   if (Math.abs(video.currentTime - timeSecs) > 0.5) { | ||||||
|     player.currentTime = timeSecs; |     video.currentTime = timeSecs; | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const setPlaying = async (playing) => { | export const setPlaying = async (playing, video = null) => { | ||||||
|  |   if (video == null) { | ||||||
|  |     video = document.querySelector("video"); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   if (playing) { |   if (playing) { | ||||||
|     await player.play(); |     await video.play(); | ||||||
|   } else { |   } else { | ||||||
|     player.pause(); |     video.pause(); | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | @ -61,7 +69,7 @@ export const setPlaying = async (playing) => { | ||||||
|  * @param {HTMLVideoElement} video |  * @param {HTMLVideoElement} video | ||||||
|  * @param {ReconnectingWebSocket} socket |  * @param {ReconnectingWebSocket} socket | ||||||
|  */ |  */ | ||||||
| const setupIncomingEvents = (player, socket) => { | const setupIncomingEvents = (video, 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); | ||||||
|  | @ -71,16 +79,16 @@ const setupIncomingEvents = (player, socket) => { | ||||||
|             setDebounce(); |             setDebounce(); | ||||||
| 
 | 
 | ||||||
|             if (event.data.playing) { |             if (event.data.playing) { | ||||||
|               await player.play(); |               await video.play(); | ||||||
|             } else { |             } else { | ||||||
|               player.pause(); |               video.pause(); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             setVideoTime(event.data.time); |             setVideoTime(event.data.time, video); | ||||||
|             break; |             break; | ||||||
|           case "SetTime": |           case "SetTime": | ||||||
|             setDebounce(); |             setDebounce(); | ||||||
|             setVideoTime(event.data); |             setVideoTime(event.data, video); | ||||||
|             break; |             break; | ||||||
|           case "UpdateViewerList": |           case "UpdateViewerList": | ||||||
|             updateViewerList(event.data); |             updateViewerList(event.data); | ||||||
|  | @ -94,19 +102,19 @@ const setupIncomingEvents = (player, socket) => { | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * @param {Plyr} player |  * @param {HTMLVideoElement} video | ||||||
|  * @param {ReconnectingWebSocket} socket |  * @param {ReconnectingWebSocket} socket | ||||||
|  */ |  */ | ||||||
| const setupOutgoingEvents = (player, socket) => { | const setupOutgoingEvents = (video, socket) => { | ||||||
|   const currentVideoTime = () => (player.currentTime * 1000) | 0; |   const currentVideoTime = () => (video.currentTime * 1000) | 0; | ||||||
| 
 | 
 | ||||||
|   player.on("pause", async () => { |   video.addEventListener("pause", async (event) => { | ||||||
|     if (outgoingDebounce || player.elements.inputs.seek.disabled) { |     if (outgoingDebounce || !video.controls) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // don't send a pause event for the video ending
 |     // don't send a pause event for the video ending
 | ||||||
|     if (player.currentTime == player.duration) { |     if (video.currentTime == video.duration) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -121,8 +129,8 @@ const setupOutgoingEvents = (player, socket) => { | ||||||
|     ); |     ); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   player.on("play", () => { |   video.addEventListener("play", (event) => { | ||||||
|     if (outgoingDebounce || player.elements.inputs.seek.disabled) { |     if (outgoingDebounce || !video.controls) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -138,14 +146,14 @@ const setupOutgoingEvents = (player, socket) => { | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   let firstSeekComplete = false; |   let firstSeekComplete = false; | ||||||
|   player.on("seeked", async (event) => { |   video.addEventListener("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 || player.elements.inputs.seek.disabled) { |     if (outgoingDebounce || !video.controls) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -160,7 +168,7 @@ const setupOutgoingEvents = (player, socket) => { | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const joinSession = async (created) => { | export const joinSession = async () => { | ||||||
|   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
 | ||||||
|  | @ -213,20 +221,29 @@ export const joinSession = async (created) => { | ||||||
|   const socket = createWebSocket(); |   const socket = createWebSocket(); | ||||||
|   state().socket = socket; |   state().socket = socket; | ||||||
|   socket.addEventListener("open", async () => { |   socket.addEventListener("open", async () => { | ||||||
|     player = await setupVideo( |     const video = await setupVideo( | ||||||
|       video_url, |       video_url, | ||||||
|       subtitle_tracks, |       subtitle_tracks, | ||||||
|       current_time_ms, |       current_time_ms, | ||||||
|       is_playing, |       is_playing | ||||||
|       created |  | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     player.on("canplay", () => { |     let defaultAllowControls = false; | ||||||
|       sync(); |     try { | ||||||
|     }); |       defaultAllowControls = localStorage.getItem( | ||||||
|  |         "watch-party-default-allow-controls" | ||||||
|  |       ); | ||||||
|  |     } catch (_err) {} | ||||||
| 
 | 
 | ||||||
|     setupOutgoingEvents(player, socket); |     // By default, we should disable video controls if the video is already playing.
 | ||||||
|     setupIncomingEvents(player, socket); |     // This solves an issue where Safari users join and seek to 00:00:00 because of
 | ||||||
|  |     // 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) => { | ||||||
|  | @ -256,15 +273,3 @@ 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); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
|  | @ -1,7 +1,15 @@ | ||||||
| import { setupJoinSessionForm } from "./lib/join-session.mjs?v=4b61c4"; | import { setupJoinSessionForm } from "./lib/join-session.mjs?v=bfdcf2"; | ||||||
|  | import { | ||||||
|  |   toggleOptionPane, | ||||||
|  |   togglePlayerControlsShown, | ||||||
|  |   handlePlingVolume | ||||||
|  | } from "./lib/options-pane.mjs?v=bfdcf2"; | ||||||
| 
 | 
 | ||||||
| const main = () => { | const main = () => { | ||||||
|   setupJoinSessionForm(); |   setupJoinSessionForm(); | ||||||
|  |   window.toggleOptionPane = toggleOptionPane; | ||||||
|  |   window.togglePlayerControlsShown = togglePlayerControlsShown; | ||||||
|  |   window.handlePlingVolume = handlePlingVolume | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| if (document.readyState === "complete") { | if (document.readyState === "complete") { | ||||||
|  |  | ||||||
|  | @ -8,6 +8,8 @@ | ||||||
|   --bg-rgb: 28, 23, 36; |   --bg-rgb: 28, 23, 36; | ||||||
|   --fg-rgb: 234, 234, 248; |   --fg-rgb: 234, 234, 248; | ||||||
|   --accent-rgb: 181, 127, 220; |   --accent-rgb: 181, 127, 220; | ||||||
|  |   --accent-darker: rgb(95, 37, 136); /*50% darker*/ | ||||||
|  |   --accent-darkest: rgb(47, 19, 68); /*75% darker*/ | ||||||
|   --fg: rgb(var(--fg-rgb)); |   --fg: rgb(var(--fg-rgb)); | ||||||
|   --bg: rgb(var(--bg-rgb)); |   --bg: rgb(var(--bg-rgb)); | ||||||
|   --default-user-color: rgb(126, 208, 255); |   --default-user-color: rgb(126, 208, 255); | ||||||
|  | @ -25,14 +27,6 @@ | ||||||
|     ), |     ), | ||||||
|     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 { | ||||||
|  | @ -57,41 +51,11 @@ body { | ||||||
|   flex-direction: column; |   flex-direction: column; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .lock-controls.plyr__control--pressed svg { | video { | ||||||
|   opacity: 0.5; |   display: block; | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .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 { | ||||||
|  | @ -169,7 +133,7 @@ input[type="text"] { | ||||||
|   overflow-y: scroll; |   overflow-y: scroll; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| button:not(.plyr button) { | button { | ||||||
|   background-color: var(--accent); |   background-color: var(--accent); | ||||||
|   border: var(--accent); |   border: var(--accent); | ||||||
|   border-radius: 6px; |   border-radius: 6px; | ||||||
|  | @ -317,7 +281,6 @@ button.small-button { | ||||||
| 
 | 
 | ||||||
| #chatbox-send { | #chatbox-send { | ||||||
|   padding: 0 1em; |   padding: 0 1em; | ||||||
|   padding-bottom: 0.5em; |  | ||||||
|   position: relative; |   position: relative; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -341,7 +304,7 @@ button.small-button { | ||||||
|   display: none; |   display: none; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .emoji-option:not(:root) { | .emoji-option { | ||||||
|   background: transparent; |   background: transparent; | ||||||
|   font-size: 0.75rem; |   font-size: 0.75rem; | ||||||
|   text-align: left; |   text-align: left; | ||||||
|  | @ -397,6 +360,73 @@ button.small-button { | ||||||
|   cursor: pointer; |   cursor: pointer; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | #options-toggle { | ||||||
|  |   padding: 0 1em 1em; | ||||||
|  |   position: relative; | ||||||
|  |   text-align: right; | ||||||
|  |   margin-top: auto; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #options-toggle #options-icon { | ||||||
|  |   padding: 3px 10px; | ||||||
|  |   font-size: 1em; | ||||||
|  |   max-width: 3em; | ||||||
|  | 
 | ||||||
|  |   color: transparent; | ||||||
|  |   text-shadow: 0 0 0 white; | ||||||
|  |   border: none; | ||||||
|  |   box-shadow:0px 0px 0px 2px var(--accent-darkest) inset; | ||||||
|  |    | ||||||
|  |   transform-style: preserve-3d; | ||||||
|  |   transition: cubic-bezier(0, 0, 0.58, 1), cubic-bezier(0, 0, 0.58, 1); | ||||||
|  |   transition-duration: 150ms; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #options-toggle #options-icon::before { | ||||||
|  |   content: ""; | ||||||
|  | 
 | ||||||
|  |   position: absolute; | ||||||
|  |    | ||||||
|  |   width: 100%; | ||||||
|  |   height: 100%; | ||||||
|  |   top: 0; | ||||||
|  |   left: 0; | ||||||
|  |   right: 0; | ||||||
|  |   bottom: 0; | ||||||
|  | 
 | ||||||
|  |   background-color: var(--accent-darker); | ||||||
|  |   border-radius: inherit; | ||||||
|  |   border: none; | ||||||
|  |   box-shadow:0px 0px 0px 2px var(--accent-darkest) inset; | ||||||
|  | 
 | ||||||
|  |   transform: translate3d(0, 0.5em, -1em); | ||||||
|  |   transition: cubic-bezier(0, 0, 0.58, 1), cubic-bezier(0, 0, 0.58, 1); | ||||||
|  |   transition-duration: 150ms; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #options-toggle #options-icon:hover { | ||||||
|  |   transform: translate(0, 0.15em); | ||||||
|  |   background-color: rgb(173, 113, 216); /*5% darker accent*/ | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #options-toggle #options-icon:hover::before { | ||||||
|  |   transform: translate3d(0, 0.35em, -1em); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #options-toggle #options-icon:active { | ||||||
|  |   transform: translate(0em, 0.5em); | ||||||
|  |   background-color: rgb(165, 100, 213); /*10% darker accent*/ | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #options-toggle #options-icon:active::before { | ||||||
|  |   transform: translate3d(0, 0, -1em); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #options { | ||||||
|  |   display: none; /* default for sections is block */ | ||||||
|  |   padding: 01em; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| input[type="color"]::-moz-color-swatch { | input[type="color"]::-moz-color-swatch { | ||||||
|   border: none; |   border: none; | ||||||
|   margin: 0; |   margin: 0; | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue