aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiguel Ángel Moreno <mail@migalmoreno.com>2024-01-28 03:09:31 +0100
committerMiguel Ángel Moreno <mail@migalmoreno.com>2024-01-28 03:09:31 +0100
commit728b0993f5d343c9c5946ce5dc103253048a82e3 (patch)
treef522ba01d9425953faf0d6b23bc30b4b5e9268d3
parent80ba76375e824839fe5b19c64fad6d40f98410b0 (diff)
feat(frontend): refine video player and tweak sources
-rw-r--r--package-lock.json35
-rw-r--r--package.json4
-rw-r--r--resources/src/css/tubo.scss15
-rw-r--r--src/frontend/tubo/components/video_player.cljs46
-rw-r--r--src/frontend/tubo/events.cljs18
-rw-r--r--src/frontend/tubo/subs.cljs5
-rw-r--r--src/frontend/tubo/views/stream.cljs94
7 files changed, 127 insertions, 90 deletions
diff --git a/package-lock.json b/package-lock.json
index 70f5eb3..7b314f7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,11 +6,13 @@
"": {
"dependencies": {
"@fortawesome/fontawesome-free": "^6.4.2",
+ "@silvermine/videojs-quality-selector": "^1.3.1",
"buffer": "^6.0.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"timeago.js": "^4.0.2",
- "video.js": "^8.5.2"
+ "video.js": "^8.5.2",
+ "videojs-mobile-ui": "^1.1.1"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.4",
@@ -201,6 +203,17 @@
"node": ">= 8"
}
},
+ "node_modules/@silvermine/videojs-quality-selector": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@silvermine/videojs-quality-selector/-/videojs-quality-selector-1.3.1.tgz",
+ "integrity": "sha512-uo6gs2HVG2TD0bpZAl0AT6RkDXzk9PnAxtmmW5zXexa2uJvkdFT64QvJoMlEUd2FUUwqYqqAuWGFDJdBh5+KcQ==",
+ "dependencies": {
+ "underscore": "1.13.1"
+ },
+ "peerDependencies": {
+ "video.js": ">=6.0.0"
+ }
+ },
"node_modules/@tailwindcss/forms": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.4.tgz",
@@ -3852,6 +3865,11 @@
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"dev": true
},
+ "node_modules/underscore": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz",
+ "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g=="
+ },
"node_modules/update-browserslist-db": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
@@ -3943,6 +3961,21 @@
"resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-4.1.0.tgz",
"integrity": "sha512-X1LuPfLZPisPLrANIAKCknZbZu5obVM/ylfd1CN+SsCmPZQ3UMDPcvLTpPBJxcBuTpHQq2MO1QCFt7p8spnZ/w=="
},
+ "node_modules/videojs-mobile-ui": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/videojs-mobile-ui/-/videojs-mobile-ui-1.1.1.tgz",
+ "integrity": "sha512-q7vx74++bqu2763Tc/GG4qFcMt42emC8uXe/z+zFVpBIiysgAf89AgorE6m30YHWtVJWgbRIyzFVYNOxCk9qow==",
+ "dependencies": {
+ "global": "^4.4.0"
+ },
+ "engines": {
+ "node": ">=14",
+ "npm": ">=6"
+ },
+ "peerDependencies": {
+ "video.js": "^8"
+ }
+ },
"node_modules/videojs-vtt.js": {
"version": "0.15.5",
"resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz",
diff --git a/package.json b/package.json
index 0306059..9ce6894 100644
--- a/package.json
+++ b/package.json
@@ -6,11 +6,13 @@
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.4.2",
+ "@silvermine/videojs-quality-selector": "^1.3.1",
"buffer": "^6.0.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"timeago.js": "^4.0.2",
- "video.js": "^8.5.2"
+ "video.js": "^8.5.2",
+ "videojs-mobile-ui": "^1.1.1"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.4",
diff --git a/resources/src/css/tubo.scss b/resources/src/css/tubo.scss
index 5d795f8..91d33fa 100644
--- a/resources/src/css/tubo.scss
+++ b/resources/src/css/tubo.scss
@@ -8,11 +8,18 @@
@import "tailwindcss/components";
@import "tailwindcss/utilities";
@import "video.js/dist/video-js.css";
+@import "@silvermine/videojs-quality-selector/dist/css/quality-selector.css";
-video[poster] {
+.vjs-tubo .vjs-poster img {
object-fit: cover;
}
-.vjs-poster {
- background-size: cover !important;
- background-position: inherit;
+
+.vjs-tubo .vjs-control-bar {
+ background: none;
+}
+
+.vjs-tubo .vjs-big-play-button, .vjs-tubo.vjs-paused .vjs-big-play-button {
+ background: none;
+ font-size: 120px;
+ border: none;
}
diff --git a/src/frontend/tubo/components/video_player.cljs b/src/frontend/tubo/components/video_player.cljs
index c94579a..1825f50 100644
--- a/src/frontend/tubo/components/video_player.cljs
+++ b/src/frontend/tubo/components/video_player.cljs
@@ -1,27 +1,39 @@
(ns tubo.components.video-player
(:require
+ [re-frame.core :as rf]
[reagent.core :as r]
[reagent.dom :as rdom]
- ["video.js" :as videojs]))
+ ["video.js" :as videojs]
+ ["videojs-mobile-ui"]
+ ["@silvermine/videojs-quality-selector" :as VideojsQualitySelector]))
(defn player
- [options url]
- (let [!player (atom nil)]
+ [options]
+ (let [!player (atom nil)
+ service-color @(rf/subscribe [:service-color])
+ {:keys [theme]} @(rf/subscribe [:settings])]
(r/create-class
{:display-name "VideoPlayer"
:component-did-mount
- (fn [this]
- (reset! !player (videojs (rdom/dom-node this) (clj->js options))))
- :component-did-update
- (fn [this [_ prev-argv prev-more]]
- (when (and @!player (not= prev-more (first (r/children this))))
- (.src @!player (apply array (map #(js-obj "type" % "src" (first (r/children this)))
- (map #(get % "type") (get options "sources")))))
- (.ready @!player #(.play @!player))))
- :component-will-unmount
- (fn [_]
- (when @!player
- (.dispose @!player)))
+ (fn [^videojs/VideoJsPlayer this]
+ (let [set-bg-color! #(set! (.. (.$ (.getChild ^videojs/VideoJsPlayer @!player "ControlBar") %)
+ -style
+ -background)
+ service-color)]
+ (VideojsQualitySelector videojs)
+ (reset! !player (videojs (rdom/dom-node this) (clj->js options)))
+ (set-bg-color! ".vjs-play-progress")
+ (set-bg-color! ".vjs-volume-level")
+ (set-bg-color! ".vjs-slider-bar")
+ (.ready @!player #(.mobileUi ^videojs/VideoJsPlayer @!player))
+ (.on @!player "play" (fn []
+ (.audioPosterMode
+ @!player
+ (clojure.string/includes?
+ (:label (first (filter #(= (:src %) (.src @!player))
+ (:sources options))))
+ "audio-only"))))))
+ :component-will-unmount #(when @!player (.dispose @!player))
:reagent-render
- (fn [options url]
- [:video-js.vjs-default-skin.vjs-big-play-centered.bottom-0.object-cover.min-h-full.max-h-full.min-w-full.focus:ring-transparent])})))
+ (fn [options]
+ [:video-js.vjs-tubo.vjs-default-skin.vjs-big-play-centered.vjs-show-big-play-button-on-pause])})))
diff --git a/src/frontend/tubo/events.cljs b/src/frontend/tubo/events.cljs
index 90282ed..a7d5269 100644
--- a/src/frontend/tubo/events.cljs
+++ b/src/frontend/tubo/events.cljs
@@ -621,20 +621,10 @@
(rf/reg-event-fx
::get-stream-page
(fn [{:keys [db]} [_ uri]]
- {:db (assoc db :show-page-loading true)
- :fx [[:dispatch [::fetch-stream-page uri]]]}))
-
-(rf/reg-event-db
- ::change-stream-format
- (fn [{:keys [stream] :as db} [_ format-id]]
- (let [{:keys [audio-streams video-streams]} stream]
- (if format-id
- (assoc db :stream-format
- (first (filter #(= format-id (:id %))
- (apply conj audio-streams video-streams))))
- (assoc db :stream-format (if (empty? video-streams)
- (first audio-streams)
- (last video-streams)))))))
+ (assoc
+ (api/get-request (str "/api/streams/" (js/encodeURIComponent uri))
+ [::load-stream-page] [::bad-response])
+ :db (assoc db :show-page-loading true))))
(rf/reg-event-fx
::load-channel
diff --git a/src/frontend/tubo/subs.cljs b/src/frontend/tubo/subs.cljs
index 0ca48f0..d30d69c 100644
--- a/src/frontend/tubo/subs.cljs
+++ b/src/frontend/tubo/subs.cljs
@@ -71,11 +71,6 @@
(:stream db)))
(rf/reg-sub
- :stream-format
- (fn [db _]
- (:stream-format db)))
-
-(rf/reg-sub
:playlist
(fn [db _]
(:playlist db)))
diff --git a/src/frontend/tubo/views/stream.cljs b/src/frontend/tubo/views/stream.cljs
index b609511..3b97f42 100644
--- a/src/frontend/tubo/views/stream.cljs
+++ b/src/frontend/tubo/views/stream.cljs
@@ -17,59 +17,57 @@
uploader-url upload-date related-streams
thumbnail-url show-comments-loading comments-page
show-comments show-related show-description service-id]
- :as stream} @(rf/subscribe [:stream])
- {show-comments? :show-comments show-related? :show-related
- show-description? :show-description} @(rf/subscribe [:settings])
+ :as stream} @(rf/subscribe [:stream])
+ {show-comments? :show-comments
+ show-related? :show-related
+ show-description? :show-description}
+ @(rf/subscribe [:settings])
available-streams (apply conj audio-streams video-streams)
- {:keys [content id] :as stream-format} @(rf/subscribe [:stream-format])
- page-loading? @(rf/subscribe [:show-page-loading])
- service-color @(rf/subscribe [:service-color])
- bookmarks @(rf/subscribe [:bookmarks])]
+ page-loading? @(rf/subscribe [:show-page-loading])
+ service-color @(rf/subscribe [:service-color])
+ bookmarks @(rf/subscribe [:bookmarks])
+ sources (reverse (map (fn [{:keys [content format resolution averageBitrate]}]
+ {:src content
+ :type "video/mp4"
+ :label (str (or resolution "audio-only") " "
+ format
+ (when-not resolution
+ (str " " averageBitrate "kbit/s")))})
+ available-streams))
+ player-elements ["playToggle" "progressControl"
+ "volumePanel" "playbackRateMenuButton"
+ "QualitySelector" "fullscreenToggle"]]
[layout/content-container
[:div.flex.justify-center.relative
{:class "h-[300px] md:h-[450px] lg:h-[600px]"}
- (when stream-format
- [player/player {"sources" [{"src" content "type" "video/mp4"}
- {"src" content "type" "video/webm"}]
- "poster" thumbnail-url
- "controls" true
- "responsive" true
- "fill" true}
- content])]
- [:div.overflow-x-hidden
- [:div.flex.flex.w-full.my-4.justify-center
- [:button.sm:px-2.py-1.text-sm.sm:text-base.text-neutral-600.dark:text-neutral-300
- {:on-click #(rf/dispatch [::events/switch-to-audio-player stream service-color])}
- [:i.fa-solid.fa-headphones]
- [:span.mx-3 "Background"]]
- (if (some #(= (:url %) url) bookmarks)
- [:button.sm:px-2.py-1.text-sm.sm:text-base.text-neutral-600.dark:text-neutral-300
- {:on-click #(rf/dispatch [::events/remove-from-bookmarks stream])}
- [:i.fa-solid.fa-bookmark]
- [:span.mx-3 "Bookmarked"]]
- [:button.sm:px-2.py-1.text-sm.sm:text-base.text-neutral-600.dark:text-neutral-300
- {:on-click #(rf/dispatch [::events/add-to-bookmarks stream])}
- [:i.fa-regular.fa-bookmark]
- [:span.mx-3 "Bookmark"]])
- [:button.sm:px-2.py-1.text-sm.sm:text-base.text-neutral-600.dark:text-neutral-300
- [:a.block.sm:inline-block {:href url}
- [:i.fa-solid.fa-external-link-alt]
- [:span.mx-3 "Original"]]]
- (when stream-format
- [:div.relative.flex.flex-col.items-center.justify-center.text-neutral-600.dark:text-neutral-300
- [:select.border-none.focus:ring-transparent.dark:bg-blend-color-dodge.pr-8.w-full.text-ellipsis.text-sm.sm:text-base
- {:on-change #(rf/dispatch [::events/change-stream-format (.. % -target -value)])
- :value id
- :style {:background "transparent"}}
- (when available-streams
- (for [[i {:keys [id format resolution averageBitrate]}] (map-indexed vector available-streams)]
- [:option.dark:bg-neutral-900.border-none {:value id :key i}
- (str (or resolution "audio-only") " " format (when-not resolution (str " " averageBitrate "kbit/s")))]))]
- [:div.flex.absolute.min-h-full.top-0.right-4.items-center.justify-end
- [:i.fa-solid.fa-caret-down]]])]
+ [player/player
+ {:sources sources
+ :poster thumbnail-url
+ :controls true
+ :controlBar {:children player-elements}
+ :preload "metadata"
+ :responsive true
+ :fill true
+ :playbackRates [0.5 1 1.5 2]}]]
+ [:div
[:div.flex.flex-col
- [:div.min-w-full.pb-3
- [:h1.text-2xl.font-extrabold.line-clamp-1 name]]
+ [:div.flex.items-center.justify-between.pt-4
+ [:div.flex-auto
+ [:h1.text-lg.sm:text-2xl.font-extrabold.line-clamp-1 name]]
+ [:div.flex.flex-auto.justify-end.items-center.my-3.gap-x-5
+ [:button
+ {:on-click #(rf/dispatch [::events/switch-to-audio-player stream service-color])}
+ [:i.fa-solid.fa-headphones]]
+ [:button
+ [:a.block.sm:inline-block {:href url :target "__blank"}
+ [:i.fa-solid.fa-external-link-alt]]]
+ (if (some #(= (:url %) url) bookmarks)
+ [:button
+ {:on-click #(rf/dispatch [::events/remove-from-bookmarks stream])}
+ [:i.fa-solid.fa-bookmark {:style {:color service-color}}]]
+ [:button
+ {:on-click #(rf/dispatch [::events/add-to-bookmarks stream])}
+ [:i.fa-regular.fa-bookmark]])]]
[:div.flex.justify-between.py-2.flex-nowrap
[:div.flex.items-center
[layout/uploader-avatar uploader-avatar uploader-name