diff options
-rw-r--r-- | package-lock.json | 35 | ||||
-rw-r--r-- | package.json | 4 | ||||
-rw-r--r-- | resources/src/css/tubo.scss | 15 | ||||
-rw-r--r-- | src/frontend/tubo/components/video_player.cljs | 46 | ||||
-rw-r--r-- | src/frontend/tubo/events.cljs | 18 | ||||
-rw-r--r-- | src/frontend/tubo/subs.cljs | 5 | ||||
-rw-r--r-- | src/frontend/tubo/views/stream.cljs | 94 |
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 |