diff --git a/README.md b/README.md index 42eeda6d..6f66a57b 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@  # Features +- Watch sync parties! - Sponsorblock built-in. - Return YouTube dislikes built-in. - DeArrow built-in (With local processing fallback). diff --git a/materialious/package-lock.json b/materialious/package-lock.json index 8e58d9f7..faf149fa 100644 --- a/materialious/package-lock.json +++ b/materialious/package-lock.json @@ -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", diff --git a/materialious/package.json b/materialious/package.json index bc11d80b..1cb976b3 100644 --- a/materialious/package.json +++ b/materialious/package.json @@ -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", diff --git a/materialious/src/lib/Player.svelte b/materialious/src/lib/Player.svelte index a916803a..2077c71d 100644 --- a/materialious/src/lib/Player.svelte +++ b/materialious/src/lib/Player.svelte @@ -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 = ''; diff --git a/materialious/src/lib/player.ts b/materialious/src/lib/player.ts new file mode 100644 index 00000000..f42651bf --- /dev/null +++ b/materialious/src/lib/player.ts @@ -0,0 +1,5 @@ +export interface PlayerEvent { + type: "pause" | "seek" | "change-video" | "play"; + time?: number; + videoId?: string; +} \ No newline at end of file diff --git a/materialious/src/routes/watch/[slug]/+page.svelte b/materialious/src/routes/watch/[slug]/+page.svelte index 872739bc..ff793149 100644 --- a/materialious/src/routes/watch/[slug]/+page.svelte +++ b/materialious/src/routes/watch/[slug]/+page.svelte @@ -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; {#if data}