aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2024-03-19 21:30:39 +0100
committermetamuffin <metamuffin@disroot.org>2024-03-19 21:30:39 +0100
commita5b49a157d6b4cb52c8421fec4d65835b310f767 (patch)
tree4e0ab07743e73f5efbf9a65d64b027811c48aaeb
parentd495f0f93c2d3b5c4c575f39b7d4b93731122b0f (diff)
parenteec3cc456bd1424ad0eb4ed8d4e22954913f14a7 (diff)
downloadjellything-a5b49a157d6b4cb52c8421fec4d65835b310f767.tar
jellything-a5b49a157d6b4cb52c8421fec4d65835b310f767.tar.bz2
jellything-a5b49a157d6b4cb52c8421fec4d65835b310f767.tar.zst
Merge branch 'master' of codeberg.org:metamuffin/jellything
-rw-r--r--import/src/infojson.rs2
-rw-r--r--server/src/routes/ui/player.rs3
-rw-r--r--server/src/routes/ui/sort.rs12
-rw-r--r--web/script/player/mod.ts115
-rw-r--r--web/script/player/player.ts21
-rw-r--r--web/style/js-player.css44
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%;
+}