Browse Source

feat(frontend): refine video player and tweak sources

Miguel Ángel Moreno 1 year ago
parent
commit
728b0993f5

+ 34 - 1
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",

+ 3 - 1
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",

+ 11 - 4
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;
 }

+ 29 - 17
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])})))

+ 4 - 14
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

+ 0 - 5
src/frontend/tubo/subs.cljs

@@ -70,11 +70,6 @@
  (fn [db _]
    (:stream db)))
 
-(rf/reg-sub
- :stream-format
- (fn [db _]
-   (:stream-format db)))
-
 (rf/reg-sub
  :playlist
  (fn [db _]

+ 46 - 48
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