Progress on watch party logic using trystero

This commit is contained in:
WardPearce
2026-03-03 22:53:29 +13:00
parent 8df5f625ce
commit a52e6892cc
5 changed files with 155 additions and 8 deletions
@@ -0,0 +1,141 @@
<script lang="ts">
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 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()
});
type WatchPartyEvent = z.infer<typeof zWatchPartyEvent>;
let room: Room | 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) => {};
function actionReceiver(receiver: ActionReceiver<DataPayload>) {
receiver((data) => {
const dataParsed = zWatchPartyEvent.safeParse(data);
if (!dataParsed.success) return;
const player = $playerState;
if (!player?.playerElement) return;
const playerElement = player.playerElement;
switch (dataParsed.data.event) {
case 'play':
playerElement.play();
break;
case 'pause':
playerElement.pause();
break;
case 'seek':
playerElement.currentTime = dataParsed.data.currentTime;
break;
}
});
}
async function createRoom() {
await sodium.ready;
const roomId = sodium.to_base64(sodium.randombytes_buf(24));
room = joinRoom({ appId }, roomId);
const [sendAction, getAction] = room.makeAction('watchParty');
sendEvent = sendAction as unknown as SendEvent;
actionReceiver(getAction);
}
playerState.subscribe((player) => {
if (!player?.playerElement) return;
const playerElement = player.playerElement;
playerElement.addEventListener('play', () => {
if (!room) return;
sendEvent({
event: 'play',
videoId: player.data.video.videoId,
sent: new Date(),
currentTime: playerElement.currentTime
});
});
playerElement.addEventListener('playing', () => {
if (!room) return;
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
});
});
});
</script>
<article>
<h4>{$_('watchParty.header')}</h4>
<div class="space"></div>
{#if !room}
<button onclick={createRoom} class="surface-container-highest">
<span>{$_('watchParty.createRoom')}</span>
</button>
<button class="surface-container-high">
<span>{$_('watchParty.joinRoom')}</span>
</button>
{/if}
</article>
@@ -1 +0,0 @@
<script lang="ts"></script>
+4 -1
View File
@@ -86,7 +86,10 @@
"streams": "Streams"
},
"watchParty": {
"start": "Start a watch party"
"start": "Start a watch party",
"header": "Watch party",
"createRoom": "Create room",
"joinRoom": "Join room"
},
"subscriptions": {
"manageSubscriptions": "Manage subscriptions"
@@ -33,6 +33,7 @@
import Author from '$lib/components/Author.svelte';
import Toast from '$lib/components/Toast.svelte';
import { isOwnBackend } from '$lib/shared';
import WatchParty from '$lib/components/WatchParty.svelte';
let { children } = $props();
@@ -41,6 +42,7 @@
let mobileSearchShow = $state(false);
let notifications: Notification[] = $state([]);
let playerIsPip: boolean = $state(false);
let showWatchParty = $state(false);
let pages = $state(getPages());
invidiousAuthStore.subscribe(() => {
@@ -245,6 +247,10 @@
<Search on:searchCancelled={() => (mobileSearchShow = false)} />
</div>
{:else}
<button onclick={() => (showWatchParty = !showWatchParty)} class="circle large transparent">
<i>groups</i>
<div class="tooltip bottom">{$_('watchParty.start')}</div>
</button>
{#if $invidiousAuthStore && !isYTBackend()}
<button
class="circle large transparent"
@@ -317,6 +323,10 @@
</dialog>
<main id="main-content" tabindex="0" class="responsive max root">
{#if showWatchParty}
<WatchParty />
{/if}
{#if $playerState}
<div class="grid">
<div
@@ -297,12 +297,6 @@
{$_('transcript')}
</div>
</button>
<button class="surface-container-highest">
<i>sync</i>
<div class="tooltip">
{$_('watchParty.start')}
</div>
</button>
<Share
includePromptText={$_('player.share.includeTimestamp')}
shares={[