Removed legacy peerjs sync party

This commit is contained in:
WardPearce
2026-03-03 20:00:59 +13:00
parent 87883da133
commit d4c13dd4c3
10 changed files with 3 additions and 551 deletions
-1
View File
@@ -29,7 +29,6 @@
- Android TV support.
- Support for disabling certificate validation for homelab users.
- Sync your watch progress between Invidious sessions.
- Watch sync parties.
- Sponsorblock built-in.
- [Return YouTube Dislike](https://github.com/Anarios/return-youtube-dislike) (RYD) built-in.
- [DeArrow](https://github.com/ajayyy/DeArrow) built-in.
-67
View File
@@ -47,7 +47,6 @@
"melt": "^0.44.0",
"mousetrap": "^1.6.5",
"mysql2": "^3.18.2",
"peerjs": "^1.5.5",
"pg": "^8.19.0",
"pg-hstore": "^2.3.4",
"sequelize": "^6.37.7",
@@ -3529,15 +3528,6 @@
"integrity": "sha512-ztmtTd6xwnuh2/xu+Vb01btgV8SQWYCaK56CkRK8gEkWe5TuDyBcYJ0wgkMRn+2VcE9KUmhvkz+N9GHrqw/C0g==",
"license": "Apache-2.0"
},
"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==",
"license": "ISC",
"engines": {
"node": ">= 10"
}
},
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
@@ -8070,12 +8060,6 @@
"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==",
"license": "MIT"
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -12107,38 +12091,6 @@
"node": ">=4"
}
},
"node_modules/peerjs": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/peerjs/-/peerjs-1.5.5.tgz",
"integrity": "sha512-viMUCPDL6CSfOu0ZqVcFqbWRXNHIbv2lPqNbrBIjbFYrflebOjItJ4hPfhjnuUCstqciHVu9vVJ7jFqqKi/EuQ==",
"license": "MIT",
"dependencies": {
"@msgpack/msgpack": "^2.8.0",
"eventemitter3": "^4.0.7",
"peerjs-js-binarypack": "^2.1.0",
"webrtc-adapter": "^9.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==",
"license": "MIT",
"engines": {
"node": ">= 14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/peer"
}
},
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
@@ -13664,12 +13616,6 @@
"node": ">=v12.22.7"
}
},
"node_modules/sdp": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.1.tgz",
"integrity": "sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw==",
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
@@ -15763,19 +15709,6 @@
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/webrtc-adapter": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.3.tgz",
"integrity": "sha512-5fALBcroIl31OeXAdd1YUntxiZl1eHlZZWzNg3U4Fn+J9/cGL3eT80YlrsWGvj2ojuz1rZr2OXkgCzIxAZ7vRQ==",
"license": "BSD-3-Clause",
"dependencies": {
"sdp": "^3.2.0"
},
"engines": {
"node": ">=6.0.0",
"npm": ">=3.10.0"
}
},
"node_modules/whatwg-mimetype": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
-1
View File
@@ -85,7 +85,6 @@
"melt": "^0.44.0",
"mousetrap": "^1.6.5",
"mysql2": "^3.18.2",
"peerjs": "^1.5.5",
"pg": "^8.19.0",
"pg-hstore": "^2.3.4",
"sequelize": "^6.37.7",
@@ -1,236 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { page } from '$app/stores';
import {
determinePeerJsInstance,
peerJs,
removeWindowQueryFlag,
setWindowQueryFlag,
shareURL
} from '$lib/misc';
import type { PlayerEvents } from '$lib/player/index';
import type { DataConnection } from 'peerjs';
import { onDestroy, onMount } from 'svelte';
import { _ } from '$lib/i18n';
import { get } from 'svelte/store';
import { syncPartyConnectionsStore, syncPartyPeerStore } from '../store';
import z from 'zod';
import { Buffer } from 'buffer';
import { Capacitor } from '@capacitor/core';
import { addToast } from './Toast.svelte';
const SyncInfo = z.object({
host: z.string(),
path: z.string(),
port: z.number(),
syncId: z.string()
});
let syncPartyManualInput: string = $state('');
const syncOrigin = Capacitor.isNativePlatform() ? 'https://sync.materialio.us' : location.origin;
function changeVideoEvent(conn: DataConnection) {
conn.on('data', (data) => {
const events = data as PlayerEvents;
const currentUrl = get(page).url;
const blockedPages = ['subscriptions', 'playlists', 'history', 'subscriptions/manage'];
events.events.forEach((event) => {
if (event.type === 'change-video' && event.videoId) {
currentUrl.pathname = resolve(`/watch/[videoId]`, { videoId: event.videoId });
// eslint-disable-next-line svelte/no-navigation-without-resolve
goto(currentUrl);
} else if (event.type === 'goto' && event.path && event.path !== $page.url.pathname) {
if (blockedPages.includes(event.path.replace('/', ''))) {
addToast({
data: {
text: $_('syncParty.userOnPrivatePage'),
icon: 'shield_person'
}
});
} else {
currentUrl.pathname = event.path;
// eslint-disable-next-line svelte/no-navigation-without-resolve
goto(currentUrl);
}
}
});
});
}
function createShareBase64(): string {
return Buffer.from(
JSON.stringify({
...determinePeerJsInstance(),
syncId: $syncPartyPeerStore?.id
}),
'utf-8'
).toString('base64');
}
async function startWatchSync() {
if ($syncPartyPeerStore) {
$syncPartyPeerStore.destroy();
syncPartyPeerStore.set(null);
removeWindowQueryFlag('sync');
return;
}
const peerId = crypto.randomUUID();
setWindowQueryFlag('sync', peerId);
$syncPartyPeerStore = await peerJs(peerId);
if ($syncPartyPeerStore) {
$syncPartyPeerStore.on('connection', (conn) => {
conn.on('open', async () => {
addToast({
data: {
text: $_('syncParty.userJoined'),
icon: 'person_add'
}
});
conn.send({
events: [
{
type: 'goto',
path: $page.url.pathname
}
]
} as PlayerEvents);
changeVideoEvent(conn);
syncPartyConnectionsStore.set([...($syncPartyConnectionsStore || []), conn]);
});
conn.on('close', async () => {
addToast({
data: {
text: $_('syncParty.userLeft'),
icon: 'person_remove'
}
});
});
});
}
}
page.subscribe((newPage) => {
if (!newPage.url.pathname.startsWith(resolve('/watch', {})) && $syncPartyPeerStore) {
$syncPartyConnectionsStore?.forEach((conn) => {
conn.send({
events: [
{
type: 'goto',
path: newPage.url.pathname
}
]
} as PlayerEvents);
});
}
});
async function joinSyncParty(syncInfo: string) {
const parseInfo = SyncInfo.safeParse(
JSON.parse(Buffer.from(syncInfo, 'base64').toString('utf-8'))
);
if (parseInfo.error) {
return;
}
$syncPartyPeerStore = await peerJs(crypto.randomUUID(), parseInfo.data);
$syncPartyPeerStore.on('open', () => {
if (!$syncPartyPeerStore) return;
const conn = $syncPartyPeerStore.connect(parseInfo.data.syncId);
syncPartyConnectionsStore.set([...($syncPartyConnectionsStore || []), conn]);
changeVideoEvent(conn);
});
$syncPartyPeerStore.on('disconnected', () => {
syncPartyPeerStore.set(null);
syncPartyConnectionsStore.set(null);
});
}
onMount(async () => {
const currentUrl = get(page).url;
const syncInfo = currentUrl.searchParams.get('sync');
if (syncInfo) {
await joinSyncParty(syncInfo);
}
});
onDestroy(() => {
if ($syncPartyPeerStore) {
$syncPartyPeerStore.destroy();
$syncPartyPeerStore = null;
}
});
</script>
<dialog id="sync-party" class="padding">
<h6 class="no-margin">{$_('layout.syncParty')}</h6>
<p class="no-margin">{$_('layout.syncPartyWarning')}</p>
{#if $syncPartyPeerStore}
<nav>
<div class="field label border max">
<input
name="sync-share"
readonly
value={`${syncOrigin}${resolve(`/?sync=${createShareBase64()}`, {})}`}
type="text"
/>
<label class="active" for="sync-share">{$_('layout.shareURL')}</label>
</div>
<button
onclick={async () => {
await shareURL(`${syncOrigin}${resolve(`/?sync=${createShareBase64()}`, {})}`);
}}
class="round"
>
<i>content_copy</i>
</button>
</nav>
{/if}
<div class="space"></div>
<button
class="no-margin"
onclick={startWatchSync}
data-ui={`${$syncPartyPeerStore ? '#sync-party' : ''}`}
>{#if $syncPartyPeerStore}
{$_('layout.endSyncParty')}
{:else}
{$_('layout.startSyncParty')}
{/if}
</button>
{#if !$syncPartyPeerStore}
<div class="space"></div>
<hr />
<div class="space"></div>
<h6 class="no-margin">{$_('layout.joinSyncParty')}</h6>
<form
onsubmit={async (event) => {
event.preventDefault();
try {
await joinSyncParty(new URL(syncPartyManualInput).searchParams.get('sync') || '');
} catch {
// Continue regardless of error
}
}}
>
<div class="field label border max">
<input bind:value={syncPartyManualInput} type="text" name="sync-input" />
<label for="sync-input">{$_('layout.shareURL')}</label>
</div>
</form>
{/if}
</dialog>
@@ -15,15 +15,12 @@
VideoBase,
VideoWatchHistory
} from '$lib/api/model';
import type { PlayerEvents } from '$lib/player';
import {
deArrowEnabledStore,
isAndroidTvStore,
playerSavePlaybackPositionStore,
playerState,
rawMasterKeyStore,
syncPartyConnectionsStore,
syncPartyPeerStore
rawMasterKeyStore
} from '$lib/store';
import { relativeTimestamp } from '$lib/time';
import { queueGetWatchHistory } from '$lib/api/backend/historyPool';
@@ -49,12 +46,6 @@
let beenWatched: boolean = $state(false);
syncPartyPeerStore.subscribe((peer) => {
if (peer) {
watchUrl.searchParams.set('sync', peer.id);
}
});
let progress: string | undefined = $state();
if (get(playerSavePlaybackPositionStore)) {
try {
@@ -136,22 +127,6 @@
function onVideoSelected() {
playerState.set(undefined);
if ($syncPartyConnectionsStore) {
const events = {
events: [{ type: 'change-video', videoId: video.videoId }]
} as PlayerEvents;
if (playlistId) {
events.events.unshift({
type: 'playlist',
playlistId: playlistId
});
}
$syncPartyConnectionsStore.forEach((conn) => {
conn.send(events);
});
}
}
</script>
+1 -42
View File
@@ -1,7 +1,6 @@
import { goto, pushState } from '$app/navigation';
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import he from 'he';
import type Peer from 'peerjs';
import { get } from 'svelte/store';
import { env } from '$env/dynamic/public';
import {
@@ -11,7 +10,6 @@ import {
feedCacheStore,
invidiousInstanceStore,
interfaceAndroidUseNativeShare,
isAndroidTvStore,
playlistCacheStore,
rawMasterKeyStore,
searchCacheStore
@@ -26,7 +24,6 @@ import type {
VideoBase,
VideoWatchHistory
} from './api/model';
import { page } from '$app/state';
import { Capacitor } from '@capacitor/core';
import { Share } from '@capacitor/share';
import { Clipboard } from '@capacitor/clipboard';
@@ -68,44 +65,6 @@ export function unsafeRandomItem(array: any[]): any {
return array[Math.floor(Math.random() * array.length)];
}
export function setWindowQueryFlag(key: string, value: string) {
page.url.searchParams.set(key, value);
// eslint-disable-next-line svelte/no-navigation-without-resolve
pushState(page.url, page.state);
}
export function removeWindowQueryFlag(key: string) {
page.url.searchParams.delete(key);
// eslint-disable-next-line svelte/no-navigation-without-resolve
pushState(page.url, page.state);
}
let PeerInstance: typeof Peer;
export interface PeerInstance {
host: string;
path: string;
port: number;
}
export function determinePeerJsInstance(): PeerInstance {
return {
host: getPublicEnv('DEFAULT_PEERJS_HOST') || '0.peerjs.com',
path: getPublicEnv('DEFAULT_PEERJS_PATH') || '/',
port: getPublicEnv('DEFAULT_PEERJS_PORT') ? Number(getPublicEnv('DEFAULT_PEERJS_PORT')) : 443
};
}
export async function peerJs(
id: string,
instance: PeerInstance = determinePeerJsInstance()
): Promise<Peer> {
// https://github.com/peers/peerjs/issues/819
if (typeof PeerInstance === 'undefined') {
PeerInstance = (await import('peerjs')).Peer;
}
return new PeerInstance(id, instance);
}
export async function shareURL(url: string) {
if (
Capacitor.getPlatform() === 'android' &&
+1 -25
View File
@@ -7,8 +7,7 @@ import {
playerDefaultQualityStore,
playerPlaylistHistory,
playerState,
playlistSettingsStore,
syncPartyConnectionsStore
playlistSettingsStore
} from '$lib/store';
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
@@ -17,18 +16,6 @@ import { unsafeRandomItem } from '$lib/misc';
import type shaka from 'shaka-player/dist/shaka-player.ui';
import ISO6391 from 'iso-639-1';
export interface PlayerEvent {
type: 'pause' | 'seek' | 'change-video' | 'play' | 'playlist' | 'goto';
path?: string;
time?: number;
videoId?: string;
playlistId?: string;
}
export interface PlayerEvents {
events: PlayerEvent[];
}
export const playbackRates = [
0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3, 3.25, 3.5, 3.75, 4
];
@@ -92,17 +79,6 @@ export async function goToNextVideo(video: VideoPlay, playlistId: string | null)
}
if (typeof goToVideo !== 'undefined') {
get(syncPartyConnectionsStore)?.forEach((conn) => {
if (typeof goToVideo === 'undefined') return;
conn.send({
events: [
{ type: 'change-video', videoId: goToVideo.videoId },
{ type: 'playlist', playlistId: playlistId }
]
} as PlayerEvents);
});
goto(
resolve(`/${isAndroidTv ? 'tv' : 'watch'}/[videoId]?playlist=${playlistId}`, {
videoId: goToVideo.videoId
-5
View File
@@ -1,6 +1,4 @@
import { Capacitor } from '@capacitor/core';
import type Peer from 'peerjs';
import type { DataConnection } from 'peerjs';
import { writable, type Writable } from 'svelte/store';
import { Preferences } from '@capacitor/preferences';
import {
@@ -330,9 +328,6 @@ export const watchHistoryEnabledStore: Writable<boolean> = persist(
'watchHistoryEnabled'
);
export const syncPartyPeerStore: Writable<Peer | null> = writable(null);
export const syncPartyConnectionsStore: Writable<DataConnection[] | null> = writable();
export const playlistSettingsStore: Writable<Record<string, { shuffle: boolean; loop: boolean }>> =
writable({});
@@ -9,7 +9,6 @@
import PageLoading from '$lib/components/PageLoading.svelte';
import Search from '$lib/components/Search.svelte';
import Settings from '$lib/components/settings/Settings.svelte';
import SyncParty from '$lib/components/SyncParty.svelte';
import Thumbnail from '$lib/components/thumbnail/VideoThumbnail.svelte';
import Player from '$lib/components/player/Player.svelte';
import { getPages } from '$lib/navPages';
@@ -21,7 +20,6 @@
playerState,
playertheatreModeIsActive,
rawMasterKeyStore,
syncPartyPeerStore,
themeColorStore,
backendInUseStore,
hideSearchStore
@@ -252,10 +250,6 @@
<Search on:searchCancelled={() => (mobileSearchShow = false)} />
</div>
{:else}
<button data-ui="#sync-party" class="circle large transparent">
<i class:primary-text={$syncPartyPeerStore}>group</i>
<div class="tooltip bottom">{$_('layout.syncParty')}</div>
</button>
{#if $invidiousAuthStore && !isYTBackend()}
<button
class="circle large transparent"
@@ -386,7 +380,6 @@
{@render children?.()}
{/if}
<SyncParty />
<Toast />
</main>
</div>
@@ -22,15 +22,12 @@
playerTheatreModeByDefaultStore,
playertheatreModeIsActive,
playlistCacheStore,
syncPartyConnectionsStore,
type PlayerState
} from '$lib/store';
import ui from 'beercss';
import type { DataConnection } from 'peerjs';
import { onDestroy, onMount, tick } from 'svelte';
import { _ } from '$lib/i18n';
import { get } from 'svelte/store';
import { loadEntirePlaylist } from '$lib/playlist';
import Author from '$lib/components/Author.svelte';
import Description from '$lib/components/watch/Description.svelte';
import LikesDislikes from '$lib/components/watch/LikesDislikes.svelte';
@@ -41,7 +38,6 @@
import { page } from '$app/state';
import Share from '$lib/components/Share.svelte';
import Playlist from '$lib/components/watch/Playlist.svelte';
import type { PlayerEvents } from '$lib/player/index';
let { data = $bindable() } = $props();
@@ -81,137 +77,6 @@
}
});
function playerSyncEvents(conn: DataConnection) {
if (playerElement) {
conn.send({
events: [{ type: 'seek', time: playerElement.currentTime }]
} as PlayerEvents);
}
if (data.playlistId) {
conn.send({
events: [
{
type: 'playlist',
playlistId: data.playlistId
}
]
} as PlayerEvents);
}
conn.on('data', (rawData) => {
const events = rawData as PlayerEvents;
events.events.forEach(async (event) => {
if (!playerElement) return;
if (event.type === 'pause') {
playerElement.pause();
} else if (event.type === 'play') {
playerElement.play();
} else if (event.type === 'seek' && event.time) {
const timeDiff = playerElement.currentTime - event.time;
if (timeDiff > 5 || timeDiff < -5) {
playerElement.currentTime = event.time;
}
} else if (
event.type === 'playlist' &&
event.playlistId &&
event.playlistId !== data.playlistId
) {
data.playlistId = event.playlistId;
await loadEntirePlaylist(event.playlistId);
goToCurrentPlaylistItem();
}
});
});
if (!playerElement) return;
playerElement.addEventListener('error', () => {
if (!playerElement) return;
conn.send({
events: [
{
type: 'seek',
time: playerElement.currentTime
},
{
type: 'play'
}
]
} as PlayerEvents);
});
playerElement.addEventListener('pause', () => {
conn.send({
events: [
{
type: 'pause'
}
]
} as PlayerEvents);
});
playerElement.addEventListener('playing', () => {
if (!playerElement) return;
conn.send({
events: [
{
type: 'seek',
time: playerElement.currentTime
},
{
type: 'play'
}
]
} as PlayerEvents);
});
playerElement.addEventListener('play', () => {
if (!playerElement) return;
conn.send({
events: [
{
type: 'seek',
time: playerElement.currentTime
},
{
type: 'play'
}
]
} as PlayerEvents);
});
playerElement.addEventListener('waiting', () => {
conn.send({
events: [
{
type: 'pause'
}
]
} as PlayerEvents);
});
playerElement.addEventListener('seeked', () => {
if (!playerElement) return;
conn.send({
events: [
{
type: 'seek',
time: playerElement.currentTime
}
]
} as PlayerEvents);
});
}
syncPartyConnectionsStore.subscribe((connections) => {
if (!connections || !playerElement) return;
playerSyncEvents(connections[connections.length - 1]);
});
async function load(state: PlayerState) {
playerElement = state.playerElement;
@@ -224,12 +89,6 @@
expandSummery('chapter-section');
}
if ($syncPartyConnectionsStore) {
$syncPartyConnectionsStore.forEach((conn) => {
playerSyncEvents(conn);
});
}
if (playerElement) {
playerElement.addEventListener('timeupdate', () => {
if (!playerElement) return;