diff options
Diffstat (limited to 'web')
-rw-r--r-- | web/script/player/mod.ts | 33 | ||||
-rw-r--r-- | web/script/player/sync.ts | 92 | ||||
-rw-r--r-- | web/style/js-player.css | 38 |
3 files changed, 157 insertions, 6 deletions
diff --git a/web/script/player/mod.ts b/web/script/player/mod.ts index fdb4e4a..6aca36c 100644 --- a/web/script/player/mod.ts +++ b/web/script/player/mod.ts @@ -11,6 +11,7 @@ import { EncodingProfile } from "./jhls.d.ts"; import { TrackKind, get_track_kind } from "./mediacaps.ts"; import { Player } from "./player.ts"; import { Popup } from "./popup.ts"; +import { Playersync } from "./sync.ts" globalThis.addEventListener("DOMContentLoaded", () => { if (document.body.classList.contains("player")) { @@ -35,10 +36,12 @@ function initialize_player(el: HTMLElement, node_id: string) { const logger = new Logger<string>(s => e("p", s)) const player = new Player(node_id, logger) const show_stats = new OVar(false); + const sync_state = new OVar<Playersync | undefined>(undefined) const toggle_playing = () => player.playing.value ? player.pause() : player.play() const pri_map = (v: number) => (v / player.duration.value * 100) + "%" + let pri_current: HTMLElement; let pri: HTMLElement; @@ -49,12 +52,14 @@ function initialize_player(el: HTMLElement, node_id: string) { const button = e("button", MEDIA_KIND_ICONS[kind][+enabled], { class: "icon", onclick: () => { + // sync_state.value = new Playersync(player, logger, "test") enabled = !enabled button.textContent = MEDIA_KIND_ICONS[kind][+enabled] } }) new Popup(button, popups, () => e("div", { class: "jsp-track-select-popup" }, + e("h3", `${kind[0].toUpperCase()}${kind.substring(1)}`), ...(player.tracks ?? []) .map((track, index) => ({ index, track })) .filter(({ track }) => get_track_kind(track.kind) == kind) @@ -74,6 +79,33 @@ function initialize_player(el: HTMLElement, node_id: string) { ) return button } + const settings_popup = () => { + const button = e("button", "settings", { class: "icon" }) + let channelname: HTMLInputElement; + new Popup(button, popups, () => e("div", { class: "jsp-settings-popup" }, + e("h2", "Settings"), + e("div", { class: "jsp-playersync-controls" }, + e("h3", "Playersync"), + sync_state.map(sync => sync + ? e("div", + e("span", "Sync enabled."), + e("button", "Disable", { + onclick: () => { sync_state.value?.destroy(); sync_state.value = undefined } + })) + : e("div", + channelname = e("input", { type: "text" }), + e("button", "Sync!", { + onclick: () => { + if (!channelname.value.length) return + sync_state.value?.destroy() + sync_state.value = new Playersync(player, logger, channelname.value) + } + })) + ) + ) + )) + return button; + } const controls = e("div", { class: "jsp-controls" }, player.playing.map(playing => @@ -105,6 +137,7 @@ function initialize_player(el: HTMLElement, node_id: string) { track_select("audio"), track_select("subtitles") ), + settings_popup(), e("button", "fullscreen", { class: "icon", onclick() { diff --git a/web/script/player/sync.ts b/web/script/player/sync.ts new file mode 100644 index 0000000..a2029ea --- /dev/null +++ b/web/script/player/sync.ts @@ -0,0 +1,92 @@ +import { Logger } from "../jshelper/src/log.ts"; +import { Player } from "./player.ts" + +function get_username() { + return document.querySelector("nav .account .username")?.textContent ?? "Unknown User" +} + +interface Packet { + time?: number, + playing?: boolean, + join?: string, + leave?: string, +} + +export class Playersync { + private ws: WebSocket + private on_destroy: (() => void)[] = [] + + private cancel_pers: undefined | (() => void) + set_pers(s?: string) { + if (this.cancel_pers) this.cancel_pers(), this.cancel_pers = undefined + if (s) this.cancel_pers = this.logger?.log_persistent(s) + } + + constructor(private player: Player, private logger: Logger<string>, private channel_name: string) { + this.set_pers("Playersync enabling...") + + let [localpart, remotepart] = channel_name.split(":") + if (!remotepart?.length) remotepart = window.location.host + + this.ws = new WebSocket(`${window.location.protocol.endsWith("s:") ? "wss" : "ws"}://${remotepart}/playersync/${encodeURIComponent(localpart)}`) + this.on_destroy.push(() => this.ws.close()) + + this.ws.onopen = () => { + this.set_pers() + this.logger.log(`Playersync connected.`) + this.send({ join: get_username() }) + } + this.ws.onerror = () => { + this.set_pers(`Playersync websocket error.`) + } + this.ws.onclose = () => { + this.set_pers(`Playersync websocket closed.`) + } + + let last_time = 0; + this.ws.onmessage = ev => { + const packet: Packet = JSON.parse(ev.data) + console.log("playersync recv", packet); + if (packet.time !== undefined) { + this.player.seek(packet.time) + last_time = packet.time + } + if (packet.playing === true) this.player.play() + if (packet.playing === false) this.player.pause() + if (packet.join) this.logger.log(`${packet.join} joined.`) + if (packet.leave) this.logger.log(`${packet.join} left.`) + } + + let cb: () => void + + player.video.addEventListener("play", cb = () => { + this.send({ playing: true }) + }) + this.on_destroy.push(() => player.video.removeEventListener("play", cb)) + + player.video.addEventListener("pause", cb = () => { + this.send({ playing: false }) + }) + this.on_destroy.push(() => player.video.removeEventListener("pause", cb)) + + player.video.addEventListener("seeking", cb = () => { + const time = this.player.video.currentTime + if (Math.abs(last_time - time) < 0.01) return + this.send({ time: this.player.video.currentTime }) + }) + this.on_destroy.push(() => player.video.removeEventListener("seeking", cb)) + } + + destroy() { + this.set_pers() + this.logger.log("Playersync disabled.") + this.on_destroy.forEach(f => f()) + this.send({ leave: get_username() }) + } + + send(p: Packet) { + console.log("playersync send", p); + this.ws.send(JSON.stringify(p)) + } +} + diff --git a/web/style/js-player.css b/web/style/js-player.css index 40455ed..8f047f1 100644 --- a/web/style/js-player.css +++ b/web/style/js-player.css @@ -52,10 +52,6 @@ .jsp-track-state:hover { background-color: rgba(113, 113, 113, 0.333); } -.jsp-track-select-popup { - background-color: #303a; - padding: 1em; -} .jsp-pri { position: relative; @@ -149,14 +145,16 @@ bottom: var(--csize); right: 0px; animation-name: popup-in; + animation-delay: 180ms; animation-duration: 100ms; - animation-fill-mode: forwards; + animation-fill-mode: both; animation-timing-function: ease-out; } .jsp-popup-out { animation-name: popup-out; + animation-delay: 0ms; animation-duration: 100ms; - animation-fill-mode: backwards; + animation-fill-mode: both; animation-timing-function: ease-in; } @keyframes popup-in { @@ -179,3 +177,31 @@ opacity: 0; } } + +.jsp-settings-popup { + padding: 1em; + min-width: 14em; + background-color: rgba(45, 24, 104, 0.548); +} +.jsp-track-select-popup { + min-width: 14em; + background-color: #303a; + padding: 1em; +} +.jsp-settings-popup h2, +.jsp-settings-popup h3 { + margin-top: 0.1em; + margin-bottom: 0.1em; +} + +.jsp-playersync-controls button { + background-color: black; + border: 2px solid var(--accent-light); + font-size: medium; + padding: 0.3em; + border-radius: 7px; +} +.jsp-playersync-controls { + padding: 1em; + background-color: #0005; +} |