Started watch sync feature

This commit is contained in:
WardPearce
2024-04-05 01:17:58 +13:00
parent 01998123c9
commit 318f812771
6 changed files with 313 additions and 7 deletions
+1
View File
@@ -12,6 +12,7 @@
![Preview of homepage](./previews/home-preview.png)
# Features
- Watch sync parties!
- Sponsorblock built-in.
- Return YouTube dislikes built-in.
- DeArrow built-in (With local processing fallback).
+186
View File
@@ -11,6 +11,7 @@
"beercss": "^3.5.1",
"human-number": "^2.0.4",
"media-icons": "^0.10.0",
"peerjs": "^1.5.2",
"sponsorblock-api": "^0.2.4",
"svelte-infinite-loading": "^1.3.8",
"svelte-persisted-store": "^0.9.1",
@@ -1984,6 +1985,78 @@
"node": ">=6.9.0"
}
},
"node_modules/@cbor-extract/cbor-extract-darwin-arm64": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz",
"integrity": "sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@cbor-extract/cbor-extract-darwin-x64": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.0.tgz",
"integrity": "sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@cbor-extract/cbor-extract-linux-arm": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.0.tgz",
"integrity": "sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
]
},
"node_modules/@cbor-extract/cbor-extract-linux-arm64": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.0.tgz",
"integrity": "sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
]
},
"node_modules/@cbor-extract/cbor-extract-linux-x64": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.0.tgz",
"integrity": "sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
]
},
"node_modules/@cbor-extract/cbor-extract-win32-x64": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz",
"integrity": "sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
]
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
@@ -2544,6 +2617,14 @@
"resolved": "https://registry.npmjs.org/@material/material-color-utilities/-/material-color-utilities-0.2.7.tgz",
"integrity": "sha512-0FCeqG6WvK4/Cc06F/xXMd/pv4FeisI0c1tUpBbfhA2n9Y8eZEv4Karjbmf2ZqQCPUWMrGp8A571tCjizxoTiQ=="
},
"node_modules/@msgpack/msgpack": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz",
"integrity": "sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==",
"engines": {
"node": ">= 10"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -3695,6 +3776,35 @@
],
"peer": true
},
"node_modules/cbor-extract": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/cbor-extract/-/cbor-extract-2.2.0.tgz",
"integrity": "sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
"node-gyp-build-optional-packages": "5.1.1"
},
"bin": {
"download-cbor-prebuilds": "bin/download-prebuilds.js"
},
"optionalDependencies": {
"@cbor-extract/cbor-extract-darwin-arm64": "2.2.0",
"@cbor-extract/cbor-extract-darwin-x64": "2.2.0",
"@cbor-extract/cbor-extract-linux-arm": "2.2.0",
"@cbor-extract/cbor-extract-linux-arm64": "2.2.0",
"@cbor-extract/cbor-extract-linux-x64": "2.2.0",
"@cbor-extract/cbor-extract-win32-x64": "2.2.0"
}
},
"node_modules/cbor-x": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/cbor-x/-/cbor-x-1.5.4.tgz",
"integrity": "sha512-PVKILDn+Rf6MRhhcyzGXi5eizn1i0i3F8Fe6UMMxXBnWkalq9+C5+VTmlIjAYM4iF2IYF2N+zToqAfYOp+3rfw==",
"optionalDependencies": {
"cbor-extract": "^2.1.1"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -4039,6 +4149,15 @@
"node": ">=8"
}
},
"node_modules/detect-libc": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
"optional": true,
"engines": {
"node": ">=8"
}
},
"node_modules/devalue": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.2.tgz",
@@ -4536,6 +4655,11 @@
"node": ">=0.10.0"
}
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -5957,6 +6081,20 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
"node_modules/node-gyp-build-optional-packages": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz",
"integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==",
"optional": true,
"dependencies": {
"detect-libc": "^2.0.1"
},
"bin": {
"node-gyp-build-optional-packages": "bin.js",
"node-gyp-build-optional-packages-optional": "optional.js",
"node-gyp-build-optional-packages-test": "build-test.js"
}
},
"node_modules/node-releases": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
@@ -6122,6 +6260,37 @@
"node": ">=8"
}
},
"node_modules/peerjs": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/peerjs/-/peerjs-1.5.2.tgz",
"integrity": "sha512-pPrtNwPyWJHRPxy2y+rHcdlrG8UwUBB1nl+3Yj6r7FLwcbBpcB2NvGNvLvcrxAVGGGX9fsdA5VT5zBKTZcm1DQ==",
"dependencies": {
"@msgpack/msgpack": "^2.8.0",
"cbor-x": "1.5.4",
"eventemitter3": "^4.0.7",
"peerjs-js-binarypack": "^2.1.0",
"webrtc-adapter": "^8.0.0"
},
"engines": {
"node": ">= 14"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/peer"
}
},
"node_modules/peerjs-js-binarypack": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/peerjs-js-binarypack/-/peerjs-js-binarypack-2.1.0.tgz",
"integrity": "sha512-YIwCC+pTzp3Bi8jPI9UFKO0t0SLo6xALnHkiNt/iUFmUUZG0fEEmEyFKvjsDKweiFitzHRyhuh6NvyJZ4nNxMg==",
"engines": {
"node": ">= 14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/peer"
}
},
"node_modules/periscopic": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz",
@@ -6692,6 +6861,11 @@
"rimraf": "bin.js"
}
},
"node_modules/sdp": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.0.tgz",
"integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw=="
},
"node_modules/semver": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
@@ -7784,6 +7958,18 @@
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.1.tgz",
"integrity": "sha512-poXpCylU7ExuvZK8z+On3kX+S8o/2dQ/SVYueKA0D4WEMXROXgY8Ez50/bQEUmvoSMMrWcrJqCHuhAbsiwg7Dg=="
},
"node_modules/webrtc-adapter": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-8.2.3.tgz",
"integrity": "sha512-gnmRz++suzmvxtp3ehQts6s2JtAGPuDPjA1F3a9ckNpG1kYdYuHWYpazoAnL9FS5/B21tKlhkorbdCXat0+4xQ==",
"dependencies": {
"sdp": "^3.2.0"
},
"engines": {
"node": ">=6.0.0",
"npm": ">=3.10.0"
}
},
"node_modules/whatwg-url": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz",
+1
View File
@@ -38,6 +38,7 @@
"beercss": "^3.5.1",
"human-number": "^2.0.4",
"media-icons": "^0.10.0",
"peerjs": "^1.5.2",
"sponsorblock-api": "^0.2.4",
"svelte-infinite-loading": "^1.3.8",
"svelte-persisted-store": "^0.9.1",
+1 -1
View File
@@ -24,8 +24,8 @@
export let currentTime: number = 0;
export let audioMode = false;
export let playlistVideos: PlaylistPageVideo[] | null = null;
export let player: MediaPlayerElement;
let player: MediaPlayerElement;
let src: PlayerSrc = [];
let categoryBeingSkipped = '';
+5
View File
@@ -0,0 +1,5 @@
export interface PlayerEvent {
type: "pause" | "seek" | "change-video" | "play";
time?: number;
videoId?: string;
}
@@ -13,8 +13,12 @@
import Player from '$lib/Player.svelte';
import Thumbnail from '$lib/Thumbnail.svelte';
import { cleanNumber, numberWithCommas } from '$lib/misc.js';
import { onMount } from 'svelte';
import type { PlayerEvent } from '$lib/player';
import type { DataConnection } from 'peerjs';
import Peer from 'peerjs';
import { onDestroy, onMount } from 'svelte';
import { get } from 'svelte/store';
import type { MediaPlayerElement } from 'vidstack/elements';
import { activePage, auth, playerListenByDefault } from '../../../store';
export let data;
@@ -23,10 +27,83 @@
activePage.set(null);
const currentUrl = new URL(location.href);
let playlistVideos: PlaylistPageVideo[] = [];
let playlist: PlaylistPage | null = null;
let audioMode = get(playerListenByDefault);
let currentTime: number;
let seekTo: (time: number) => void;
let player: MediaPlayerElement;
let peer: Peer | null = null;
function playerSyncEvents(conn: DataConnection) {
conn.on('data', (data) => {
const events = data as PlayerEvent[];
events.forEach((event) => {
if (event.type === 'pause') {
player.pause();
} else if (event.type === 'play') {
player.play();
} else if (event.type === 'seek' && event.time) {
player.currentTime = event.time;
}
});
});
player.addEventListener('pause', () => {
conn.send([
{
type: 'pause'
},
{
type: 'seek',
time: player.currentTime
}
] as PlayerEvent[]);
});
player.addEventListener('playing', () => {
conn.send([
{
type: 'play'
},
{
type: 'seek',
time: player.currentTime
}
] as PlayerEvent[]);
});
player.addEventListener('waiting', () => {
conn.send([
{
type: 'pause'
},
{
type: 'seek',
time: player.currentTime
}
] as PlayerEvent[]);
});
}
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);
playerSyncEvents(conn);
});
}
if (!data.playlistId) return;
for (let page = 1; page < Infinity; page++) {
@@ -54,6 +131,34 @@
}
});
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);
@@ -83,17 +188,13 @@
data.subscribed = !data.subscribed;
}
let audioMode = get(playerListenByDefault);
let currentTime: number;
let seekTo: (time: number) => void;
</script>
{#if data}
<div class="grid">
<div class="s12 m12 l10">
{#key data.video.videoId}
<Player {data} {audioMode} {playlistVideos} bind:seekTo bind:currentTime />
<Player {data} {audioMode} {playlistVideos} bind:seekTo bind:currentTime bind:player />
{/key}
<h5>{data.video.title}</h5>
@@ -150,6 +251,18 @@
</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)}