diff options
author | metamuffin <metamuffin@disroot.org> | 2024-03-19 21:30:39 +0100 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2024-03-19 21:30:39 +0100 |
commit | a5b49a157d6b4cb52c8421fec4d65835b310f767 (patch) | |
tree | 4e0ab07743e73f5efbf9a65d64b027811c48aaeb | |
parent | d495f0f93c2d3b5c4c575f39b7d4b93731122b0f (diff) | |
parent | eec3cc456bd1424ad0eb4ed8d4e22954913f14a7 (diff) | |
download | jellything-a5b49a157d6b4cb52c8421fec4d65835b310f767.tar jellything-a5b49a157d6b4cb52c8421fec4d65835b310f767.tar.bz2 jellything-a5b49a157d6b4cb52c8421fec4d65835b310f767.tar.zst |
Merge branch 'master' of codeberg.org:metamuffin/jellything
-rw-r--r-- | import/src/infojson.rs | 2 | ||||
-rw-r--r-- | server/src/routes/ui/player.rs | 3 | ||||
-rw-r--r-- | server/src/routes/ui/sort.rs | 12 | ||||
-rw-r--r-- | web/script/player/mod.ts | 115 | ||||
-rw-r--r-- | web/script/player/player.ts | 21 | ||||
-rw-r--r-- | web/style/js-player.css | 44 |
6 files changed, 164 insertions, 33 deletions
diff --git a/import/src/infojson.rs b/import/src/infojson.rs index 14783f7..c83c91c 100644 --- a/import/src/infojson.rs +++ b/import/src/infojson.rs @@ -38,7 +38,7 @@ pub struct YVideo { pub uploader_id: Option<String>, pub uploader_url: Option<String>, pub upload_date: String, - pub availability: String, // "public" | "private" | "unlisted", + pub availability: Option<String>, // "public" | "private" | "unlisted", pub original_url: Option<String>, pub webpage_url_basename: String, pub webpage_url_domain: String, diff --git a/server/src/routes/ui/player.rs b/server/src/routes/ui/player.rs index 1ef0149..80386ad 100644 --- a/server/src/routes/ui/player.rs +++ b/server/src/routes/ui/player.rs @@ -39,6 +39,7 @@ pub struct PlayerConfig { pub v: Option<TrackID>, pub s: Option<TrackID>, pub t: Option<f64>, + pub kind: Option<PlayerKind>, } impl PlayerConfig { @@ -93,7 +94,7 @@ pub fn r_player<'a>( )))) }; - match sess.user.player_preference { + match conf.kind.unwrap_or(sess.user.player_preference) { PlayerKind::Browser => (), PlayerKind::Native => { return native_session("player"); diff --git a/server/src/routes/ui/sort.rs b/server/src/routes/ui/sort.rs index 2c5f0cc..542b7af 100644 --- a/server/src/routes/ui/sort.rs +++ b/server/src/routes/ui/sort.rs @@ -132,6 +132,7 @@ pub fn filter_and_sort_nodes( default_sort: (SortProperty, SortOrder), nodes: &mut Vec<(String, NodePublic, NodeUserData)>, ) { + let sort_prop = f.sort_by.unwrap_or(default_sort.0); nodes.retain(|(_id, node, udata)| { let mut o = true; if let Some(prop) = &f.filter_kind { @@ -158,16 +159,13 @@ pub fn filter_and_sort_nodes( } } } - if let Some(sort_by) = &f.sort_by { - match sort_by { - SortProperty::ReleaseDate => o &= node.release_date.is_some(), - SortProperty::Duration => o &= node.media.is_some(), - _ => (), - } + match sort_prop { + SortProperty::ReleaseDate => o &= node.release_date.is_some(), + SortProperty::Duration => o &= node.media.is_some(), + _ => (), } o }); - let sort_prop = f.sort_by.unwrap_or(default_sort.0); match sort_prop { SortProperty::Duration => { nodes.sort_by_key(|(_, n, _)| (n.media.as_ref().unwrap().duration * 1000.) as i64) diff --git a/web/script/player/mod.ts b/web/script/player/mod.ts index fa83a2b..3774c62 100644 --- a/web/script/player/mod.ts +++ b/web/script/player/mod.ts @@ -31,6 +31,12 @@ const MEDIA_KIND_ICONS: { [key in TrackKind]: [string, string] } = { subtitles: ["subtitles_off", "subtitles"], } +function toggle_fullscreen() { + if (document.fullscreenElement) document.exitFullscreen() + else document.documentElement.requestFullscreen() +} + + function initialize_player(el: HTMLElement, node_id: string) { el.innerHTML = "" // clear the body @@ -40,15 +46,41 @@ function initialize_player(el: HTMLElement, node_id: string) { const idle_inhibit = new OVar(false) const sync_state = new OVar<Playersync | undefined>(undefined) + let mute_saved_volume = 1; + const toggle_mute = () => { + if (player.volume.value == 0) { + logger.log("Unmuted."); + player.volume.value = mute_saved_volume + } + else { + logger.log("Muted."); + mute_saved_volume = player.volume.value + player.volume.value = 0. + } + } 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; const popups = e("div") + const step_track_kind = (kind: TrackKind) => { + // TODO cycle through all of them + const active = player.active_tracks.value.filter( + ts => get_track_kind(player.tracks![ts.track_index].kind) == kind) + if (active.length > 0) { + for (const t of active) player.set_track_enabled(t.track_index, false) + } else { + const all_kind = (player.tracks ?? []) + .map((track, index) => ({ index, track })) + .filter(({ track }) => get_track_kind(track.kind) == kind) + if (all_kind.length < 1) return logger.log(`No ${kind} tracks available`) + player.set_track_enabled(all_kind[0].index, true) + } + } + const track_select = (kind: TrackKind) => { const button = e("div", player.active_tracks.map(_ => { const active = player.active_tracks.value.filter( @@ -68,7 +100,6 @@ function initialize_player(el: HTMLElement, node_id: string) { if (all_kind.length < 1) return player.set_track_enabled(all_kind[0].index, true) } - } }) })) @@ -81,7 +112,13 @@ function initialize_player(el: HTMLElement, node_id: string) { slider.valueAsNumber = player.video.volume slider.onchange = () => player.video.volume = slider.valueAsNumber slider.onmousemove = () => player.video.volume = slider.valueAsNumber - return [e("div", { class: "jsp-controlgroup" }, e("label", "Volume", slider))] + return [e("div", { class: ["jsp-controlgroup", "jsp-volumecontrol"] }, + e("label", `Volume`), + e("span", { class: "jsp-volume" }, player.volume.map(v => + `${(v * 100).toFixed(2)}% | ${v == 0 ? "-∞" : (Math.log2(v) * 10).toFixed(2)}dB` as string + )), + slider + )] } new Popup(button, popups, () => @@ -90,10 +127,12 @@ function initialize_player(el: HTMLElement, node_id: string) { ...(kind == "audio" ? volume() : []), - player.active_tracks.map(_ => - e("ul", { class: "jsp-track-list" }, ...(player.tracks ?? []) + player.active_tracks.map(_ => { + const tracks_avail = (player.tracks ?? []) .map((track, index) => ({ index, track })) - .filter(({ track }) => get_track_kind(track.kind) == kind) + .filter(({ track }) => get_track_kind(track.kind) == kind); + if (!tracks_avail.length) return e("p", `No ${kind} tracks available.`) as HTMLElement; + return e("ul", { class: "jsp-track-list" }, ...tracks_avail .map(({ track, index }): HTMLElement => { const active = player.active_tracks.value.find(ts => ts.track_index == index) !== undefined const onclick = () => { @@ -106,7 +145,8 @@ function initialize_player(el: HTMLElement, node_id: string) { e("span", { class: "jsp-track-lang" }, `(${track.language})`) ) }) - )) + ) + }) ) ) return button @@ -119,7 +159,7 @@ function initialize_player(el: HTMLElement, node_id: string) { playersync_controls(sync_state, player), e("button", "Launch Native Player", { onclick: () => { - window.location.href = `jellynative://player-fullscreen/${window.location.protocol}//${window.location.host}/n/${encodeURIComponent(node_id)}/stream?format=hlsmaster` + window.location.href = `?kind=nativefullscreen` } }) )) @@ -136,6 +176,15 @@ function initialize_player(el: HTMLElement, node_id: string) { ), pri = e("div", { class: "jsp-pri" }, pri_current = e("div", { class: "jsp-pri-current" }), + player.chapters.map( + chapters => e("div", ...chapters.map(chap => e("div", { + class: "jsp-chapter", + style: { + left: pri_map(chap.time_start ?? 0), + width: pri_map((chap.time_end ?? player.duration.value) - (chap.time_start ?? 0)) + } + }, e("p", chap.labels[0][1])))) + ), player.active_tracks.map( tracks => e("div", ...tracks.map((t, i) => t.buffered.map( ranges => e("div", ...ranges.map( @@ -158,13 +207,7 @@ function initialize_player(el: HTMLElement, node_id: string) { track_select("subtitles") ), settings_popup(), - e("button", "fullscreen", { - class: "icon", - onclick() { - if (document.fullscreenElement) document.exitFullscreen() - else document.documentElement.requestFullscreen() - } - }) + e("button", "fullscreen", { class: "icon", onclick: toggle_fullscreen }) ) player.position.onchangeinit(p => pri_current.style.width = pri_map(p)) @@ -200,10 +243,21 @@ function initialize_player(el: HTMLElement, node_id: string) { const p = (ev.clientX - r.left) / (r.right - r.left) player.seek(p * player.duration.value) }) + + + document.body.addEventListener("keydown", k => { - if (k.ctrlKey) return + if (k.ctrlKey || k.altKey || k.metaKey) return if (k.code == "Period") player.pause(), player.frame_forward() - if (k.code == "Space") toggle_playing() + else if (k.code == "Space") toggle_playing() + else if (k.code == "KeyP") toggle_playing() + else if (k.code == "KeyF") toggle_fullscreen() + else if (k.code == "KeyQ") window.history.back() + else if (k.code == "KeyS") screenshot_video(player.video) + else if (k.code == "KeyJ") step_track_kind("subtitles") + else if (k.code == "KeyM") toggle_mute() + else if (k.key == "#") step_track_kind("audio") + else if (k.key == "_") step_track_kind("video") else if (k.code == "KeyV") show_stats.value = !show_stats.value else if (k.code == "ArrowLeft") player.seek(player.position.value - 5) else if (k.code == "ArrowRight") player.seek(player.position.value + 5) @@ -215,6 +269,25 @@ function initialize_player(el: HTMLElement, node_id: string) { send_player_progress(node_id, player) } +function screenshot_video(video: HTMLVideoElement) { + // TODO bug: video needs to be played to take a screenshot. if you have just seeked somewhere it wont work. + const canvas = document.createElement("canvas") + canvas.width = video.videoWidth + canvas.height = video.videoHeight + const context = canvas.getContext("2d")! + context.fillStyle = "#ff00ff" + context.fillRect(0, 0, video.videoWidth, video.videoHeight) + context.drawImage(video, 0, 0) + canvas.toBlob(blob => { + if (!blob) throw new Error("failed to create blob"); + const a = document.createElement("a"); + a.download = "screenshot.webp"; + a.href = window.URL.createObjectURL(blob) + a.click() + setTimeout(() => URL.revokeObjectURL(a.href), 0) + }, "image/webp", 0.95) +} + let sent_watched = false; function send_player_progress(node_id: string, player: Player) { let t = 0; @@ -237,9 +310,9 @@ function send_player_progress(node_id: string, player: Player) { function mouse_idle(e: HTMLElement, timeout: number): OVar<boolean> { let ct: number; const idle = new OVar(false) - // e.onmouseleave = () => { - // clearTimeout(ct) - // } + e.onmouseleave = () => { + clearTimeout(ct) + } e.onmousemove = () => { clearTimeout(ct) if (idle) { @@ -257,4 +330,4 @@ function show_profile(profile: EncodingProfile): string { if (profile.video) return `codec=${profile.video.codec} br=${show.metric(profile.video.bitrate, "b/s")} w=${profile.video.width} preset=${profile.video.preset}` if (profile.subtitles) return `codec=${profile.subtitles.codec}` return `???` -} +}
\ No newline at end of file diff --git a/web/script/player/player.ts b/web/script/player/player.ts index a5bccdb..3d35c49 100644 --- a/web/script/player/player.ts +++ b/web/script/player/player.ts @@ -9,7 +9,7 @@ import { NodePublic, NodeUserData, SourceTrack, TimeRange } from "./jhls.d.ts"; import { SegmentDownloader } from "./download.ts"; import { PlayerTrack } from "./track/mod.ts"; import { Logger } from "../jshelper/src/log.ts"; -import { WatchedState } from "./jhls.d.ts"; +import { WatchedState, Chapter } from "./jhls.d.ts"; import { get_track_kind } from "./mediacaps.ts"; import { create_track } from "./track/create.ts"; @@ -18,11 +18,13 @@ export class Player { public video = e("video") public media_source = new MediaSource(); public tracks?: SourceTrack[]; + public chapters = new OVar<Chapter[]>([]); public active_tracks = new OVar<PlayerTrack[]>([]); public downloader: SegmentDownloader = new SegmentDownloader(); public position = new OVar(0) public duration = new OVar(1) + public volume = new OVar(0) public playing = new OVar(false) public canplay = new OVar(false) public error = new OVar<string | undefined>(undefined) @@ -34,6 +36,17 @@ export class Player { } constructor(public node_id: string, public logger?: Logger<string>) { + this.volume.value = this.video.volume + let skip_change = false; + this.volume.onchange(v => { + if (!skip_change) this.video.volume = v + skip_change = false + }) + this.video.onvolumechange = () => { + skip_change = true; + this.volume.value = this.video.volume + } + this.video.onloadedmetadata = () => { } this.video.ondurationchange = () => { } this.video.ontimeupdate = () => { @@ -101,9 +114,11 @@ export class Player { if (userdata.error) return this.set_pers("server error: " + metadata.error) this.set_pers() + //! bad code: assignment order is important because chapter callbacks use duration + this.duration.value = metadata.media!.duration + this.chapters.value = metadata.media!.chapters this.tracks = metadata.media!.tracks - this.duration.value = metadata.media!.duration this.video.src = URL.createObjectURL(this.media_source) this.media_source.addEventListener("sourceopen", async () => { this.set_pers("Downloading track indecies...") @@ -129,7 +144,7 @@ export class Player { this.set_pers() }) } - + async update(newt?: number) { await Promise.all(this.active_tracks.value.map(t => t.update(newt ?? this.video.currentTime))) } diff --git a/web/style/js-player.css b/web/style/js-player.css index 1d205a8..2dedb9c 100644 --- a/web/style/js-player.css +++ b/web/style/js-player.css @@ -207,3 +207,47 @@ ul.jsp-track-list { ul.jsp-track-list li.active { background-color: #047a0073; } + +.jsp-volumecontrol input { + appearance: none; + width: 100%; + height: 24px; + background-color: black; + opacity: 0.5; + outline: none; +} +.jsp-volumecontrol input:hover { + opacity: 1; +} +.jsp-volumecontrol input::-webkit-slider-thumb, +.jsp-volumecontrol input::-moz-range-thumb { + width: 24px; + height: 24px; + border-radius: 0px; + background-color: #06ad00; + cursor: ew-resize; + border: 0px solid transparent; +} + +.jsp-volume { + display: inline-block; + margin-left: 2em; + font-family: monospace; + font-size: large; + width: 20em; +} + +.jsp-chapter { + position: absolute; + height: var(--csize); + padding-left: 2px; + border-left: 2px solid rgba(255, 161, 55, 0.548); +} +.jsp-chapter p { + font-size: small; + text-overflow: ellipsis; + overflow: visible; + overflow: hidden; + white-space: nowrap; + width: 100%; +} |