Merge pull request #59 from WardPearce/update/watch-sync-parties

Update/watch sync parties
This commit is contained in:
Ward
2024-04-06 18:27:27 +13:00
committed by GitHub
4 changed files with 177 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>
+125 -2
View File
@@ -1,14 +1,17 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { navigating } from '$app/stores';
import { navigating, page } from '$app/stores';
import { getFeed } from '$lib/Api/index';
import Logo from '$lib/Logo.svelte';
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,60 @@
{ 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', () => {
if ($page.url.pathname.startsWith('/watch')) {
const paths = $page.url.pathname.split('/');
if (paths.length > 2) {
conn.send({
events: [
{
type: 'change-video',
videoId: paths[2]
}
]
} as PlayerEvents);
}
}
changeVideoEvent(conn);
syncPartyConnections.set([...($syncPartyConnections || []), conn]);
});
});
}
}
function toggleSponsor(category: string) {
if (sponsorCategoriesList.includes(category)) {
sponsorBlockCategories.set(sponsorCategoriesList.filter((value) => value !== category));
@@ -182,6 +241,32 @@
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);
});
$syncPartyPeer.on('disconnected', () => {
syncPartyPeer.set(null);
syncPartyConnections.set(null);
});
}
});
onDestroy(() => {
if ($syncPartyPeer) {
$syncPartyPeer.disconnect();
$syncPartyPeer = null;
}
});
$: webManifestLink = pwaInfo ? pwaInfo.webManifest.linkTag : '';
@@ -227,6 +312,10 @@
<i>code</i>
<div class="tooltip bottom">Star us on Github!</div>
</a>
<button data-ui="#sync-party" class="circle large transparent">
<i class:primary-text={$syncPartyPeer}>group</i>
<div class="tooltip bottom">Sync party</div>
</button>
{#if isLoggedIn}
<button class="circle large transparent" data-ui="#dialog-notifications"
><i>notifications</i>
@@ -574,6 +663,40 @@
{/each}
</dialog>
<dialog id="sync-party">
{#if $syncPartyPeer}
<nav>
<div class="field label border">
<input
name="sync-share"
readonly
value={`${import.meta.env.VITE_DEFAULT_FRONTEND_URL}?sync=${$syncPartyPeer.id}`}
type="text"
/>
<label for="sync-share">Share URL</label>
</div>
<button
on:click={async () => {
await navigator.clipboard.writeText(
`${import.meta.env.VITE_DEFAULT_FRONTEND_URL}?sync=${$syncPartyPeer.id}`
);
}}
class="square round"
>
<i>content_copy</i>
</button>
</nav>
{/if}
<div class="space"></div>
<button on:click={startWatchSync} data-ui={`${$syncPartyPeer ? '#sync-party' : ''}`}
>{#if $syncPartyPeer}
End
{:else}
Start
{/if} sync party
</button>
</dialog>
<main class="responsive max root">
{#if $navigating}
<PageLoading />
@@ -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,16 @@
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.send({
events: [{ type: 'seek', time: player.currentTime }]
} as PlayerEvents);
conn.on('data', (data) => {
const events = data as PlayerEvents;
if (!player) return;
events.events.forEach((event) => {
if (event.type === 'pause') {
player.pause();
@@ -67,9 +57,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 +135,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 +174,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 +213,7 @@
{data}
{audioMode}
{playlistVideos}
isSyncing={peer !== null}
isSyncing={$syncPartyPeer !== null}
bind:seekTo
bind:currentTime
bind:player
@@ -316,18 +274,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 +400,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 +423,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();