Merge pull request #59 from WardPearce/update/watch-sync-parties
Update/watch sync parties
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user