Improved watch sync

This commit is contained in:
WardPearce
2024-04-06 17:57:18 +13:00
parent 26903aa7b7
commit d0c8c02a91
4 changed files with 121 additions and 92 deletions
+23 -8
View File
@@ -1,22 +1,27 @@
<script lang="ts">
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { get } from 'svelte/store';
import { deArrowEnabled, playerSavePlaybackPosition } from '../store';
import {
deArrowEnabled,
playerSavePlaybackPosition,
syncPartyConnections,
syncPartyPeer
} from '../store';
import { getDeArrow, getThumbnail, getVideo } from './Api';
import type { Notification, PlaylistPageVideo, Video, VideoBase } from './Api/model';
import { cleanNumber, proxyVideoUrl, truncate, videoLength } from './misc';
import type { PlayerEvents } from './player';
export let video: VideoBase | Video | Notification | PlaylistPageVideo;
export let playlistId: string = '';
export let onClick: (input: string) => void = (input: string) => {};
let watchUrl = `/watch/${video.videoId}` + (playlistId ? `?playlist=${playlistId}` : '');
const syncId = $page.url.searchParams.get('sync');
if (syncId) {
watchUrl += (playlistId ? '&' : '?') + `sync=${syncId}`;
}
syncPartyPeer.subscribe((peer) => {
if (peer) {
watchUrl += (playlistId ? '&' : '?') + `sync=${peer.id}`;
}
});
let loading = true;
let loaded = false;
@@ -111,14 +116,24 @@
failed = true;
};
});
function syncChangeVideo() {
if ($syncPartyConnections) {
$syncPartyConnections.forEach((conn) => {
conn.send({
events: [{ type: 'change-video', videoId: video.videoId }]
} as PlayerEvents);
});
}
}
</script>
<a
class="wave"
style="width: 100%; overflow: hidden;min-height:100px;"
href={watchUrl}
on:click={() => onClick(video.videoId)}
data-sveltekit-preload-data="off"
on:click={syncChangeVideo}
>
{#if loading}
<progress class="circle"></progress>
+72 -1
View File
@@ -6,9 +6,12 @@
import PageLoading from '$lib/PageLoading.svelte';
import Search from '$lib/Search.svelte';
import Thumbnail from '$lib/Thumbnail.svelte';
import type { PlayerEvents } from '$lib/player';
import 'beercss';
import 'material-dynamic-colors';
import { onMount } from 'svelte';
import type { DataConnection } from 'peerjs';
import Peer from 'peerjs';
import { onDestroy, onMount } from 'svelte';
import { get } from 'svelte/store';
import { pwaInfo } from 'virtual:pwa-info';
import {
@@ -30,6 +33,8 @@
sponsorBlock,
sponsorBlockCategories,
sponsorBlockUrl,
syncPartyConnections,
syncPartyPeer,
themeColor
} from '../store';
@@ -97,6 +102,47 @@
{ name: 'Filler Tangent/Jokes', category: 'filler' }
];
function changeVideoEvent(conn: DataConnection) {
conn.on('data', (data) => {
const events = data as PlayerEvents;
const currentUrl = new URL(location.href);
events.events.forEach((event) => {
if (event.type === 'change-video' && event.videoId) {
currentUrl.pathname = `/watch/${event.videoId}`;
goto(currentUrl);
}
});
});
}
async function startWatchSync() {
const currentUrl = new URL(location.href);
if ($syncPartyPeer) {
$syncPartyPeer.disconnect();
syncPartyPeer.set(null);
currentUrl.searchParams.delete('sync');
window.history.pushState({ path: currentUrl.toString() }, '', currentUrl.toString());
return;
}
const peerId = crypto.randomUUID();
currentUrl.searchParams.set('sync', peerId);
window.history.pushState({ path: currentUrl.toString() }, '', currentUrl.toString());
$syncPartyPeer = new Peer(peerId);
if ($syncPartyPeer) {
$syncPartyPeer.on('connection', (conn) => {
conn.on('open', () => {
changeVideoEvent(conn);
syncPartyConnections.set([...($syncPartyConnections || []), conn]);
});
});
}
}
function toggleSponsor(category: string) {
if (sponsorCategoriesList.includes(category)) {
sponsorBlockCategories.set(sponsorCategoriesList.filter((value) => value !== category));
@@ -182,6 +228,27 @@
if (isLoggedIn) {
loadNotifications().catch(() => auth.set(null));
}
const currentUrl = new URL(location.href);
const syncId = currentUrl.searchParams.get('sync');
if (syncId) {
$syncPartyPeer = new Peer(crypto.randomUUID());
$syncPartyPeer.on('open', () => {
if (!$syncPartyPeer) return;
const conn = $syncPartyPeer.connect(syncId);
syncPartyConnections.set([...($syncPartyConnections || []), conn]);
changeVideoEvent(conn);
});
}
});
onDestroy(() => {
if ($syncPartyPeer) {
$syncPartyPeer.disconnect();
$syncPartyPeer = null;
}
});
$: webManifestLink = pwaInfo ? pwaInfo.webManifest.linkTag : '';
@@ -227,6 +294,10 @@
<i>code</i>
<div class="tooltip bottom">Star us on Github!</div>
</a>
<button on:click={startWatchSync} class="circle large transparent">
<i class:primary-text={$syncPartyPeer}>group</i>
<div class="tooltip bottom">Start sync party</div>
</button>
{#if isLoggedIn}
<button class="circle large transparent" data-ui="#dialog-notifications"
><i>notifications</i>
@@ -1,5 +1,4 @@
<script lang="ts">
import { goto } from '$app/navigation';
import {
addPlaylistVideo,
deleteUnsubscribe,
@@ -16,11 +15,16 @@
import { cleanNumber, numberWithCommas } from '$lib/misc.js';
import type { PlayerEvents } from '$lib/player';
import type { DataConnection } from 'peerjs';
import Peer from 'peerjs';
import { onDestroy, onMount } from 'svelte';
import { onMount } from 'svelte';
import { get } from 'svelte/store';
import type { MediaPlayerElement } from 'vidstack/elements';
import { activePage, auth, playerListenByDefault } from '../../../store';
import {
activePage,
auth,
playerListenByDefault,
syncPartyConnections,
syncPartyPeer
} from '../../../store';
export let data;
@@ -28,8 +32,6 @@
activePage.set(null);
const currentUrl = new URL(location.href);
let playlistVideos: PlaylistPageVideo[] = [];
let playlist: PlaylistPage | null = null;
@@ -38,28 +40,12 @@
let seekTo: (time: number) => void;
let player: MediaPlayerElement;
let peer: Peer | null = null;
let peerConnection: DataConnection | null = null;
function onVideoChange(videoId: string) {
if (!peerConnection) return;
peerConnection.send({
events: [
{
type: 'change-video',
videoId: videoId
}
]
} as PlayerEvents);
}
function playerSyncEvents(conn: DataConnection) {
peerConnection = conn;
conn.on('data', (data) => {
const events = data as PlayerEvents;
if (!player) return;
events.events.forEach((event) => {
if (event.type === 'pause') {
player.pause();
@@ -67,9 +53,6 @@
player.play();
} else if (event.type === 'seek' && event.time) {
player.currentTime = event.time;
} else if (event.type === 'change-video' && event.videoId) {
currentUrl.pathname = `/watch/${event.videoId}`;
goto(currentUrl);
}
});
});
@@ -148,15 +131,14 @@
});
}
syncPartyConnections.subscribe((connections) => {
if (!connections || !player) return;
playerSyncEvents(connections[connections.length - 1]);
});
onMount(async () => {
const syncId = currentUrl.searchParams.get('sync');
if (syncId) {
peer = new Peer(crypto.randomUUID());
peer.on('open', () => {
if (!peer) return;
const conn = peer.connect(syncId);
if ($syncPartyConnections) {
$syncPartyConnections.forEach((conn) => {
playerSyncEvents(conn);
});
}
@@ -188,34 +170,6 @@
}
});
onDestroy(() => {
if (peer) {
peer.disconnect();
peer = null;
}
});
async function startWatchSync() {
if (peer) {
peer.disconnect();
peer = null;
return;
}
const peerId = crypto.randomUUID();
currentUrl.searchParams.set('sync', peerId);
window.history.pushState({ path: currentUrl.toString() }, '', currentUrl.toString());
peer = new Peer(peerId);
peer.on('connection', (conn) => {
conn.on('open', () => {
playerSyncEvents(conn);
});
});
}
async function addVideoToPlaylist(playlistId: string) {
await addPlaylistVideo(playlistId, data.video.videoId);
@@ -255,7 +209,7 @@
{data}
{audioMode}
{playlistVideos}
isSyncing={peer !== null}
isSyncing={$syncPartyPeer !== null}
bind:seekTo
bind:currentTime
bind:player
@@ -316,18 +270,6 @@
</div>
<div class="s12 m12 l4 video-actions">
{#if data.video.hlsUrl}
<button disabled>
<i>group</i>
<span>Watch sync</span>
<div class="tooltip">Sync not available for live streams</div>
</button>
{:else}
<button on:click={startWatchSync} class:border={!peer}>
<i>group</i>
<span>Watch sync</span>
</button>
{/if}
<button
class="no-margin"
on:click={() => (audioMode = !audioMode)}
@@ -454,7 +396,7 @@
{#each data.video.recommendedVideos as recommendedVideo}
<article class="no-padding">
{#key recommendedVideo.videoId}
<Thumbnail onClick={onVideoChange} video={recommendedVideo} />
<Thumbnail video={recommendedVideo} />
{/key}
</article>
{/each}
@@ -477,11 +419,7 @@
id={playlistVideo.videoId}
class:border={playlistVideo.videoId === data.video.videoId}
>
<Thumbnail
onClick={onVideoChange}
video={playlistVideo}
playlistId={data.playlistId}
/>
<Thumbnail video={playlistVideo} playlistId={data.playlistId} />
</article>
{/each}
</article>
+6 -1
View File
@@ -1,3 +1,5 @@
import type Peer from 'peerjs';
import type { DataConnection } from 'peerjs';
import { persisted } from 'svelte-persisted-store';
import { writable, type Writable } from 'svelte/store';
@@ -26,4 +28,7 @@ export const sponsorBlockCategories: Writable<string[]> = persisted("sponsorBloc
export const deArrowInstance = persisted("deArrowInstance", import.meta.env.VITE_DEFAULT_DEARROW_INSTANCE);
export const deArrowEnabled = persisted("deArrowEnabled", false);
export const deArrowThumbnailInstance = persisted("deArrowThumbnailInstance", import.meta.env.VITE_DEFAULT_DEARROW_THUMBNAIL_INSTANCE);
export const deArrowThumbnailInstance = persisted("deArrowThumbnailInstance", import.meta.env.VITE_DEFAULT_DEARROW_THUMBNAIL_INSTANCE);
export const syncPartyPeer: Writable<Peer | null> = writable(null);
export const syncPartyConnections: Writable<DataConnection[] | null> = writable();