More watch progress

This commit is contained in:
WardPearce
2026-03-03 23:27:41 +13:00
parent a52e6892cc
commit 586d9fe601
2 changed files with 82 additions and 62 deletions
@@ -1,27 +1,36 @@
<script lang="ts">
import { goto, pushState } from '$app/navigation';
import { page } from '$app/state';
import { _ } from '$lib/i18n';
import { playerState } from '$lib/store';
import sodium from 'libsodium-wrappers-sumo';
import { joinRoom, type ActionReceiver, type DataPayload, type Room } from 'trystero';
import { onDestroy, onMount } from 'svelte';
import { SvelteURLSearchParams } from 'svelte/reactivity';
import { joinRoom, type ActionReceiver, type DataPayload, type Room } from 'trystero/mqtt';
import z from 'zod';
const zWatchPartyEvent = z.object({
event: z.union([z.literal('pause'), z.literal('play'), z.literal('seek')]),
videoId: z.regex(/^[a-zA-Z0-9_-]{11}$/),
sent: z.date(),
currentTime: z.number()
currentTime: z.number().min(0)
});
type WatchPartyEvent = z.infer<typeof zWatchPartyEvent>;
let room: Room | undefined = $state();
let roomId: string | undefined = $state();
const appId = 'materialious_';
type SendEvent = (message: WatchPartyEvent) => void;
// If room not in use message just get sent nowhere.
let sendEvent: SendEvent = (message: WatchPartyEvent) => {};
let sendEvent: SendEvent = () => {};
onMount(() => initalRoom());
onDestroy(() => room?.leave());
function actionReceiver(receiver: ActionReceiver<DataPayload>) {
receiver((data) => {
@@ -33,27 +42,34 @@
const playerElement = player.playerElement;
const currentTime = playerElement.currentTime;
const sentTime = dataParsed.data.sent.getTime();
const timeDifference = Math.abs(currentTime - sentTime);
if (timeDifference > 5000) return;
const currentTimeWithDifference = Math.max(
0,
Math.min(dataParsed.data.currentTime + timeDifference, playerElement.duration)
);
switch (dataParsed.data.event) {
case 'play':
playerElement.play();
playerElement.currentTime = currentTimeWithDifference;
break;
case 'pause':
playerElement.pause();
playerElement.currentTime = currentTimeWithDifference;
break;
case 'seek':
playerElement.currentTime = dataParsed.data.currentTime;
playerElement.currentTime = currentTimeWithDifference;
break;
}
});
}
async function createRoom() {
await sodium.ready;
const roomId = sodium.to_base64(sodium.randombytes_buf(24));
room = joinRoom({ appId }, roomId);
function setupRoom(room: Room) {
const [sendAction, getAction] = room.makeAction('watchParty');
sendEvent = sendAction as unknown as SendEvent;
@@ -61,81 +77,82 @@
actionReceiver(getAction);
}
async function createRoom() {
await sodium.ready;
roomId = sodium.to_base64(sodium.randombytes_buf(24));
room = joinRoom({ appId }, roomId);
setupRoom(room);
const currentSearchParams = new SvelteURLSearchParams(window.location.search);
currentSearchParams.set('room', roomId);
pushState(`?${currentSearchParams.toString()}`, { replaceState: false }); // eslint-disable-line svelte/no-navigation-without-resolve
}
function initalRoom() {
const givenRoomId = page.url.searchParams.get('room');
if (!givenRoomId) return;
room = joinRoom({ appId }, givenRoomId);
setupRoom(room);
}
playerState.subscribe((player) => {
if (!player?.playerElement) return;
const playerElement = player.playerElement;
playerElement.addEventListener('play', () => {
if (!room) return;
function sendPlayerEvent(event: 'play' | 'pause' | 'seek') {
if (!room || !player) return;
sendEvent({
event: 'play',
event,
videoId: player.data.video.videoId,
sent: new Date(),
currentTime: playerElement.currentTime
});
});
}
let initalPlaying = true;
playerElement.addEventListener('play', () => sendPlayerEvent('play'));
playerElement.addEventListener('playing', () => {
if (!room) return;
if (!initalPlaying) return;
initalPlaying = true;
sendEvent({
event: 'play',
videoId: player.data.video.videoId,
sent: new Date(),
currentTime: playerElement.currentTime
});
});
playerElement.addEventListener('seeked', () => {
sendEvent({
event: 'seek',
videoId: player.data.video.videoId,
sent: new Date(),
currentTime: playerElement.currentTime
});
});
playerElement.addEventListener('pause', () => {
sendEvent({
event: 'pause',
videoId: player.data.video.videoId,
sent: new Date(),
currentTime: playerElement.currentTime
});
});
playerElement.addEventListener('waiting', () => {
sendEvent({
event: 'pause',
videoId: player.data.video.videoId,
sent: new Date(),
currentTime: playerElement.currentTime
});
});
playerElement.addEventListener('error', () => {
sendEvent({
event: 'pause',
videoId: player.data.video.videoId,
sent: new Date(),
currentTime: playerElement.currentTime
});
sendPlayerEvent('play');
});
playerElement.addEventListener('seeked', () => sendPlayerEvent('seek'));
playerElement.addEventListener('pause', () => sendPlayerEvent('pause'));
playerElement.addEventListener('waiting', () => sendPlayerEvent('pause'));
playerElement.addEventListener('error', () => sendPlayerEvent('pause'));
});
</script>
<article>
<h4>{$_('watchParty.header')}</h4>
<div class="space"></div>
{#if !room}
<div class="space"></div>
<button onclick={createRoom} class="surface-container-highest">
<span>{$_('watchParty.createRoom')}</span>
</button>
<button class="surface-container-high">
<span>{$_('watchParty.joinRoom')}</span>
{:else}
<div class="space"></div>
<button
onclick={() => {
room?.leave();
room = undefined;
}}
class="surface-container-highest"
>
{$_('watchParty.leaveRoom')}
</button>
{/if}
</article>
+4 -1
View File
@@ -89,7 +89,10 @@
"start": "Start a watch party",
"header": "Watch party",
"createRoom": "Create room",
"joinRoom": "Join room"
"joinRoom": "Join room",
"roomID": "Room ID",
"shareRoom": "Share room",
"leaveRoom": "Leave room"
},
"subscriptions": {
"manageSubscriptions": "Manage subscriptions"