commit 36d9211dce9d8e26a5f17863f86fd65b5455abec Author: sup39 Date: Mon Jan 22 04:07:17 2024 +0900 feat: implement play/pause/seek sync diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..abb6c4b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2024 sup39 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3b203ce --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# sup-ytsync +A tampermonkey script to sync Youtube video progress with others via MQTT diff --git a/index.js b/index.js new file mode 100644 index 0000000..3565d9c --- /dev/null +++ b/index.js @@ -0,0 +1,218 @@ +// ==UserScript== +// @name sup-ytsync +// @namespace http://tampermonkey.net/ +// @version 0.1.0 +// @description sup39 +// @author sup39 +// @match https://www.youtube.com/watch* +// @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com +// @grant none +// ==/UserScript== + +(() => { + 'use strict'; + + const url = '...'; + const username = '...'; + const password = '...'; + const roomId = '...'; + const clientId = `ytsync-${username}-${+new Date()}`; + // topic = ${topicPrefixBase}/${roomId}/${videoId}/${commander.clientId}/${command} + const topicPrefixBase = 'sup-ytsync'; + + const mqttScriptUrl = 'https://unpkg.com/mqtt@5.3.4/dist/mqtt.min.js'; + + /** + * @param {HTMLVideoElement} element + * @param {VideoCommandBroadcaster} commandBroadcaster + */ + function Video(element, commandBroadcaster) { + /** @type {Set} */ + const queuedCommands = new Set(); + + for (const [eventName, command] of (/**@type{const}*/([ + ['play', 'play'], + ['pause', 'pause'], + ['seeked', 'seek'], + ]))) { + element.addEventListener(eventName, () => { + // ignore queued command + if (queuedCommands.delete(command)) return; + // ignore seeked event when playing + if (eventName === 'seeked' && !element.paused) return; + // callback + const {currentTime} = element; + commandBroadcaster(command, {currentTime}); + }); + } + + return { + /** @param {number} t */ + seek(t) { + queuedCommands.add('seek'); + element.currentTime = t; + }, + play() { + queuedCommands.add('play'); + element.play(); + }, + pause() { + queuedCommands.add('pause'); + element.pause(); + }, + get isPaused() { + return element.paused; + }, + }; + } + + // FIXME + const Buffer = /** @type{Buffer}*//**@type{any}*/(Uint8Array); + + const videoId = new URLSearchParams(window.location.search).get('v'); + if (videoId == null) return; + + const videoElement = document.querySelector('video'); + if (videoElement == null) return; + + /** @returns {Promise} */ + const loadMqttScript = () => new Promise(resolve => { + if (typeof mqtt === 'object') return resolve(); + document.head.appendChild( + Object.assign(document.createElement('script'), { + src: mqttScriptUrl, + async: true, + }), + ).addEventListener('load', () => resolve()); + }); + + const startYtsync = () => loadMqttScript().then(() => { + const topicPrefix = `${topicPrefixBase}/${roomId}/${videoId}/`; + + console.info('Connecting to MQTT server:', url); + const client = mqtt.connect(url, {username, password, clientId}); + client.on('error', e => console.warn('Error occurs on MQTT connection:', e)); + + /** @param {DataView} body */ + function parseTime(body) { + if (body.byteLength !== 8) { + throw new Error('Invalid payload'); + } + return body.getFloat64(0); + } + + client.on('connect', () => { + console.info('Connected to MQTT server:', url); + + const video = Video(videoElement, (command, state) => { + const {currentTime: t} = state; + console.debug(`Broadcast video sync command [${command}]`, t); + const payload = new Buffer(8); + new DataView(payload.buffer).setFloat64(0, t); + client.publish(`${topicPrefix}${clientId}/${command}`, payload); + }); + + client.on('message', (/**@type{string}*/topic, /**@type{Uint8Array}*/payload) => { + if (!topic.startsWith(topicPrefix)) { + return console.warn('Unexpected MQTT message', {topic, payload}); + } + const [commander, command=''] = topic.slice(topicPrefix.length).split('/'); + // ignore command sent by self + if (commander === clientId) return; + + /** DataView of payload */ + const body = new DataView( + payload.buffer, + payload.byteOffset, + payload.byteLength, + ); + + try { + if (command === 'play') { + const t = parseTime(body); + console.debug(`Received command [${command}]`, t); + video.seek(t); + video.play(); + } else if (command === 'pause') { + const t = parseTime(body); + console.debug(`Received command [${command}]`, t); + video.seek(t); + video.pause(); + } else if (command === 'seek') { + /** + * (seek while playing) pause -> play -> seeked + * => no need to update current time + * (seek when paused) pause -> seeked + * => need to update current time + */ + if (video.isPaused) { + const t = parseTime(body); + console.debug(`Received command [${command}]`, t); + video.seek(t); + } + } else { + return console.warn('Unknown command', {command, payload}); + } + } catch (e) { + console.warn(e, {topic, payload}); + } + }); + + client.subscribe(topicPrefix+'+/+'); + }); + }); + + // Based on https://stackoverflow.com/a/61511955 by Yong Wang (2020) + /** + * @param {string} selector + * @returns {Promise} + */ + const untilElementLoaded = selector => new Promise(resolve => { + const elm = document.querySelector(selector); + if (elm != null) return elm; + + const observer = new MutationObserver(() => { + const elm = document.querySelector(selector); + if (elm != null) { + observer.disconnect(); + resolve(elm); + } + }); + + observer.observe(document.body, { + childList: true, + subtree: true + }); + }); + + // TODO better UI + untilElementLoaded('#title h1').then(elm => elm.appendChild( + Object.assign(document.createElement('button'), { + textContent: 'Start sup-ytsync' + }), + ).addEventListener('click', startYtsync, {once: true})); +})(); + +/** + * The MIT License (MIT) + * + * Copyright (c) 2024 sup39 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..5ebfc59 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "checkJs": true, + "strict": true + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..bff4386 --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "sup-ytsync", + "version": "0.1.0", + "description": "A tampermonkey script to sync Youtube video progress with others via MQTT", + "main": "index.js", + "author": "sup39", + "license": "MIT", + "devDependencies": { + "mqtt": "^5.3.4" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..59f6133 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,353 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +devDependencies: + mqtt: + specifier: ^5.3.4 + version: 5.3.4 + +packages: + + /@babel/runtime@7.23.8: + resolution: {integrity: sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.1 + dev: true + + /@types/node@20.11.5: + resolution: {integrity: sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==} + dependencies: + undici-types: 5.26.5 + dev: true + + /@types/readable-stream@4.0.10: + resolution: {integrity: sha512-AbUKBjcC8SHmImNi4yK2bbjogQlkFSg7shZCcicxPQapniOlajG8GCc39lvXzCWX4lLRRs7DM3VAeSlqmEVZUA==} + dependencies: + '@types/node': 20.11.5 + safe-buffer: 5.1.2 + dev: true + + /@types/ws@8.5.10: + resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} + dependencies: + '@types/node': 20.11.5 + dev: true + + /abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + dependencies: + event-target-shim: 5.0.1 + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: true + + /bl@6.0.10: + resolution: {integrity: sha512-F14DFhDZfxtVm2FY0k9kG2lWAwzZkO9+jX3Ytuoy/V0E1/5LBuBzzQHXAjqpxXEDIpmTPZZf5GVIGPQcLxFpaA==} + dependencies: + buffer: 6.0.3 + inherits: 2.0.4 + readable-stream: 4.5.2 + dev: true + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true + + /buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: true + + /commist@3.2.0: + resolution: {integrity: sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==} + dev: true + + /concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + dev: true + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + dev: true + + /events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + dev: true + + /fast-unique-numbers@8.0.13: + resolution: {integrity: sha512-7OnTFAVPefgw2eBJ1xj2PGGR9FwYzSUso9decayHgCDX4sJkHLdcsYTytTg+tYv+wKF3U8gJuSBz2jJpQV4u/g==} + engines: {node: '>=16.1.0'} + dependencies: + '@babel/runtime': 7.23.8 + tslib: 2.6.2 + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.6 + once: 1.4.0 + dev: true + + /help-me@4.2.0: + resolution: {integrity: sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==} + dependencies: + glob: 8.1.0 + readable-stream: 3.6.2 + dev: true + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /js-sdsl@4.3.0: + resolution: {integrity: sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==} + dev: true + + /lru-cache@10.1.0: + resolution: {integrity: sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==} + engines: {node: 14 || >=16.14} + dev: true + + /minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: true + + /mqtt-packet@9.0.0: + resolution: {integrity: sha512-8v+HkX+fwbodsWAZIZTI074XIoxVBOmPeggQuDFCGg1SqNcC+uoRMWu7J6QlJPqIUIJXmjNYYHxBBLr1Y/Df4w==} + dependencies: + bl: 6.0.10 + debug: 4.3.4 + process-nextick-args: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: true + + /mqtt@5.3.4: + resolution: {integrity: sha512-nyhr2bnFtyiv68jV3yfR6eQtGcGs/jr2l3ETKXYc0amttsasXa1KgvETHRNRjfeDt/yc68IqoEjFzKkHpoQUPQ==} + engines: {node: '>=16.0.0'} + hasBin: true + dependencies: + '@types/readable-stream': 4.0.10 + '@types/ws': 8.5.10 + commist: 3.2.0 + concat-stream: 2.0.0 + debug: 4.3.4 + help-me: 4.2.0 + lru-cache: 10.1.0 + minimist: 1.2.8 + mqtt-packet: 9.0.0 + number-allocator: 1.0.14 + readable-stream: 4.5.2 + reinterval: 1.1.0 + rfdc: 1.3.1 + split2: 4.2.0 + worker-timers: 7.1.1 + ws: 8.16.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /number-allocator@1.0.14: + resolution: {integrity: sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==} + dependencies: + debug: 4.3.4 + js-sdsl: 4.3.0 + transitivePeerDependencies: + - supports-color + dev: true + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + dev: true + + /process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + dev: true + + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: true + + /readable-stream@4.5.2: + resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + dev: true + + /regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + dev: true + + /reinterval@1.1.0: + resolution: {integrity: sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==} + dev: true + + /rfdc@1.3.1: + resolution: {integrity: sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==} + dev: true + + /safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + dev: true + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: true + + /split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + dev: true + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + dev: true + + /typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + dev: true + + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: true + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: true + + /worker-timers-broker@6.1.1: + resolution: {integrity: sha512-CTlDnkXAewtYvw5gOwVIc6UuIPcNHJrqWxBMhZbCWOmadvl20nPs9beAsXlaTEwW3G2KBpuKiSgkhBkhl3mxDA==} + dependencies: + '@babel/runtime': 7.23.8 + fast-unique-numbers: 8.0.13 + tslib: 2.6.2 + worker-timers-worker: 7.0.65 + dev: true + + /worker-timers-worker@7.0.65: + resolution: {integrity: sha512-Dl4nGONr8A8Fr+vQnH7Ee+o2iB480S1fBcyJYqnMyMwGRVyQZLZU+o91vbMvU1vHqiryRQmjXzzMYlh86wx+YQ==} + dependencies: + '@babel/runtime': 7.23.8 + tslib: 2.6.2 + dev: true + + /worker-timers@7.1.1: + resolution: {integrity: sha512-axtq83GwPqYwkQmQmei2abQ9cT7oSwmLw4lQCZ9VmMH9g4t4kuEF1Gw+tdnIJGHCiZ2QPDnr/+307bYx6tynLA==} + dependencies: + '@babel/runtime': 7.23.8 + tslib: 2.6.2 + worker-timers-broker: 6.1.1 + worker-timers-worker: 7.0.65 + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /ws@8.16.0: + resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: true diff --git a/types.d.ts b/types.d.ts new file mode 100644 index 0000000..f7fa450 --- /dev/null +++ b/types.d.ts @@ -0,0 +1,13 @@ +import MQTT from 'mqtt'; +declare global { + const mqtt: typeof MQTT; + + type VideoCommand = 'play' | 'pause' | 'seek'; + type VideoState = { + currentTime: number + }; + type VideoCommandBroadcaster = ( + command: VideoCommand, + state: VideoState + ) => void; +}