Browse Source

fix(frontend): recompute audio stream sources on stream load

Previously audio stream sources were added to their corresponding items when
enqueued, but after a while source URLs would become invalid and fail playing.
Miguel Ángel Moreno 1 year ago
parent
commit
783ad76b21

+ 120 - 123
src/frontend/tubo/components/audio_player.cljs

@@ -9,126 +9,123 @@
 
 (defn player
   []
-  (let [!autoplay? (r/atom true)]
-    (fn []
-      (let [media-queue @(rf/subscribe [:media-queue])
-            media-queue-pos @(rf/subscribe [:media-queue-pos])
-            {:keys [uploader-name uploader-url thumbnail-url
-                    name stream url service-color] :as current-stream} @(rf/subscribe [:media-queue-stream])
-            show-audio-player? @(rf/subscribe [:show-audio-player])
-            show-audio-player-loading? @(rf/subscribe [:show-audio-player-loading])
-            show-media-queue? @(rf/subscribe [:show-media-queue])
-            is-window-visible @(rf/subscribe [:is-window-visible])
-            loop-file? @(rf/subscribe [:loop-file])
-            loop-playlist? @(rf/subscribe [:loop-playlist])
-            volume-level @(rf/subscribe [:volume-level])
-            muted? @(rf/subscribe [:muted])
-            !elapsed-time @(rf/subscribe [:elapsed-time])
-            !player @(rf/subscribe [:player])]
-        (when show-audio-player?
-          [:div.sticky.bottom-0.z-40.bg-white.dark:bg-neutral-900.p-3.sm:p-5.absolute.box-border.m-0
-           {:style {:borderTop (str "2px solid " service-color) :display (when show-media-queue? "none")}}
-           [:div.flex.items-center.justify-between
-            [:div.flex.items-center
-             [:div {:style {:height "40px" :width "70px" :maxWidth "70px" :minWidth "70px"}}
-              [:img.min-h-full.max-h-full.object-cover.min-w-full.max-w-full.w-full {:src thumbnail-url}]]
-             [:div.flex.flex-col.px-2
-              [:a.text-xs.line-clamp-1
-               {:href (rfe/href :tubo.routes/stream nil {:url url})} name]
-              [:a.text-xs.pt-2.text-neutral-600.dark:text-neutral-300.line-clamp-1
-               {:href (rfe/href :tubo.routes/channel nil {:url uploader-url})} uploader-name]]
-             [:audio
-              {:src                stream
-               :ref #(reset! !player %)
-               :loop               loop-file?
-               :on-time-update     #(when (and @!player (> (.-readyState @!player) 0))
-                                      (reset! !elapsed-time (.-currentTime @!player)))
-               :on-loaded-data #(when (and @!player (> (.-readyState @!player) 0))
-                                  (rf/dispatch [::events/start-playback @!player])
-                                  (set! (.-currentTime @!player) @!elapsed-time))
-               :on-ended           #(when (and @!player (> (.-readyState @!player) 0))
-                                      (let [idx (if (< (+ media-queue-pos 1) (count media-queue))
-                                                  (+ media-queue-pos 1)
-                                                  (if loop-playlist? 0 media-queue-pos))]
-                                        (rf/dispatch [::events/change-media-queue-pos idx])
-                                        (reset! !elapsed-time 0)
-                                        (when (and (not is-window-visible) loop-playlist?)
-                                          (set! (.-src @!player) (:stream (nth media-queue idx)))
-                                          (rf/dispatch [::events/start-playback @!player]))))}]]
-            [:div.flex
-             [:button:focus:ring-transparent.mx-2.cursor-pointer
-               {:on-click #(rf/dispatch [::events/toggle-media-queue])}
-               [:i.fa-solid.fa-list]]
-             [:button.hidden.ml:block.focus:outline-none.mx-2
-              {:class    (when-not (and media-queue (not= media-queue-pos 0))
-                           "opacity-50 cursor-auto")
-               :on-click (when (and media-queue (not= media-queue-pos 0))
-                           #(do
-                              (rf/dispatch [::events/change-media-queue-pos
-                                            (- media-queue-pos 1)])
-                              (reset! !elapsed-time 0)))}
-              [:i.fa-solid.fa-backward-step]]
-             [:button.hidden.ml:block.focus:outline-none.mx-2
-              {:on-click #(set! (.-currentTime @!player) (- @!elapsed-time 5))}
-              [:i.fa-solid.fa-backward]]
-             [:button.focus:outline-none.mx-2
-              {:on-click #(rf/dispatch [::events/start-playback @!player])}
-              (if @!player
-                (if show-audio-player-loading?
-                  [loading/loading-icon service-color "text-1xl"]
-                  (if (.-paused @!player)
-                    [:i.fa-solid.fa-play]
-                    [:i.fa-solid.fa-pause]))
-                [:i.fa-solid.fa-play])]
-             [:button.hidden.ml:block.focus:outline-none.mx-2
-              {:on-click #(set! (.-currentTime @!player) (+ @!elapsed-time 5))}
-              [:i.fa-solid.fa-forward]]
-             [:button.hidden.ml:block.focus:ring-transparent.mx-2
-              {:class    (when-not (and media-queue (< (+ media-queue-pos 1) (count media-queue)))
-                           "opacity-50 cursor-auto")
-               :on-click (when (and media-queue (< (+ media-queue-pos 1) (count media-queue)))
-                           #(do
-                              (rf/dispatch [::events/change-media-queue-pos
-                                            (+ media-queue-pos 1)])
-                              (reset! !elapsed-time 0)))}
-              [:i.fa-solid.fa-forward-step]]
-             [:div.flex.items-center
-              [:span.hidden.ml:block.mx-2 (if @!elapsed-time (util/format-duration @!elapsed-time) "00:00")]
-              [:input.hidden.ml:block.mx-2.w-20.ml:w-56.bg-gray-200.rounded-lg.cursor-pointer.focus:outline-none.h-1
-               {:type      "range"
-                :on-input  #(reset! !elapsed-time (.. % -target -value))
-                :on-change #(and @!player (> (.-readyState @!player) 0)
-                                 (set! (.-currentTime @!player) @!elapsed-time))
-                :style     {:accentColor service-color}
-                :max       (if (and @!player (> (.-readyState @!player) 0))
-                             (.floor js/Math (.-duration @!player))
-                             100)
-                :value     @!elapsed-time}]
-              [:span.hidden.ml:block.mx-2 (if (and @!player (> (.-readyState @!player) 0))
-                            (util/format-duration (.-duration @!player))
-                            "00:00")]
-              [:button.hidden.ml:flex.focus:ring-transparent.mx-2
-               {:on-click #(rf/dispatch [::events/toggle-loop-file])}
-               [:i.fa-solid.fa-repeat
-                {:style {:color (when loop-file? service-color)}}]]
-              [:button.hidden.ml:flex.focus:ring-transparent.mx-2
-               {:on-click #(rf/dispatch [::events/toggle-loop-playlist])}
-               [:i.fa-solid.fa-retweet
-                {:style {:color (when loop-playlist? service-color)}}]]
-              [:div.hidden.ml:flex.items-center
-               [:button.focus:outline-none.mx-2
-                {:on-click #(rf/dispatch [::events/toggle-mute @!player])}
-                (if (or (and @!player muted?))
-                  [:i.fa-solid.fa-volume-xmark]
-                  [:i.fa-solid.fa-volume-low])]
-               [:input.w-20.bg-gray-200.rounded-lg.cursor-pointer.focus:outline-none.h-1.range-sm.mx-2
-                {:type "range"
-                 :on-input #(rf/dispatch [::events/change-volume-level (.. % -target -value) @!player])
-                 :style {:accentColor service-color}
-                 :max 100
-                 :value volume-level}]]]
-             [:div.mx-2
-              [:i.fa-solid.fa-close.cursor-pointer
-               {:on-click (fn []
-                            (rf/dispatch [::events/toggle-audio-player])
-                            (.pause @!player))}]]]]])))))
+  (let [media-queue                @(rf/subscribe [:media-queue])
+        media-queue-pos            @(rf/subscribe [:media-queue-pos])
+        {:keys
+         [uploader-name uploader-url thumbnail-url
+          name stream url service-color] :as current-stream}
+        @(rf/subscribe [:media-queue-stream])
+        show-audio-player?         @(rf/subscribe [:show-audio-player])
+        show-audio-player-loading? @(rf/subscribe [:show-audio-player-loading])
+        show-media-queue?          @(rf/subscribe [:show-media-queue])
+        is-window-visible          @(rf/subscribe [:is-window-visible])
+        loop-file?                 @(rf/subscribe [:loop-file])
+        loop-playlist?             @(rf/subscribe [:loop-playlist])
+        volume-level               @(rf/subscribe [:volume-level])
+        muted?                     @(rf/subscribe [:muted])
+        !elapsed-time              @(rf/subscribe [:elapsed-time])
+        !player                    @(rf/subscribe [:player])
+        player-ready?              (and @!player (> (.-readyState @!player) 0))]
+    (when show-audio-player?
+      [:div.sticky.bottom-0.z-40.bg-white.dark:bg-neutral-900.p-3.sm:p-5.absolute.box-border.m-0
+       {:style {:borderTop (str "2px solid " service-color) :display (when show-media-queue? "none")}}
+       [:div.flex.items-center.justify-between
+        [:div.flex.items-center
+         [:div {:style {:height "40px" :width "70px" :maxWidth "70px" :minWidth "70px"}}
+          [:img.min-h-full.max-h-full.object-cover.min-w-full.max-w-full.w-full {:src thumbnail-url}]]
+         [:div.flex.flex-col.px-2
+          [:a.text-xs.line-clamp-1
+           {:href (rfe/href :tubo.routes/stream nil {:url url})} name]
+          [:a.text-xs.pt-2.text-neutral-600.dark:text-neutral-300.line-clamp-1
+           {:href (rfe/href :tubo.routes/channel nil {:url uploader-url})} uploader-name]]
+         [:audio
+          {:src            stream
+           :ref            #(reset! !player %)
+           :loop           loop-file?
+           :on-time-update #(when player-ready?
+                              (reset! !elapsed-time (.-currentTime @!player)))
+           :on-loaded-data #(do (.play @!player)
+                                (set! (.-currentTime @!player) @!elapsed-time))
+           :on-ended       #(when player-ready?
+                              (let [idx (if (< (+ media-queue-pos 1) (count media-queue))
+                                          (+ media-queue-pos 1)
+                                          (if loop-playlist? 0 media-queue-pos))]
+                                (rf/dispatch [::events/change-media-queue-pos idx])
+                                (when (and (not is-window-visible) loop-playlist?)
+                                  (set! (.-src @!player) (:stream (nth media-queue idx)))
+                                  (.play @!player))))}]]
+        [:div.flex
+         [:button:focus:ring-transparent.mx-2.cursor-pointer
+          {:on-click #(rf/dispatch [::events/toggle-media-queue])}
+          [:i.fa-solid.fa-list]]
+         [:button.hidden.ml:block.focus:outline-none.mx-2
+          {:class    (when-not (and media-queue (not= media-queue-pos 0))
+                       "opacity-50 cursor-auto")
+           :on-click #(when (and media-queue (not= media-queue-pos 0))
+                        (rf/dispatch [::events/change-media-queue-pos
+                                      (- media-queue-pos 1)]))}
+          [:i.fa-solid.fa-backward-step]]
+         [:button.hidden.ml:block.focus:outline-none.mx-2
+          {:on-click #(set! (.-currentTime @!player) (- @!elapsed-time 5))}
+          [:i.fa-solid.fa-backward]]
+         [:button.focus:outline-none.mx-2
+          {:on-click #(if (.-paused @!player)
+                        (.play @!player)
+                        (.pause @!player))}
+          (if @!player
+            (if show-audio-player-loading?
+              [loading/loading-icon service-color "text-1xl"]
+              (if (.-paused @!player)
+                [:i.fa-solid.fa-play]
+                [:i.fa-solid.fa-pause]))
+            [:i.fa-solid.fa-play])]
+         [:button.hidden.ml:block.focus:outline-none.mx-2
+          {:on-click #(set! (.-currentTime @!player) (+ @!elapsed-time 5))}
+          [:i.fa-solid.fa-forward]]
+         [:button.hidden.ml:block.focus:ring-transparent.mx-2
+          {:class    (when-not (and media-queue (< (+ media-queue-pos 1) (count media-queue)))
+                       "opacity-50 cursor-auto")
+           :on-click #(when (and media-queue (< (+ media-queue-pos 1) (count media-queue)))
+                        (rf/dispatch [::events/change-media-queue-pos
+                                      (+ media-queue-pos 1)]))}
+          [:i.fa-solid.fa-forward-step]]
+         [:div.flex.items-center
+          [:span.hidden.ml:block.mx-2 (if @!elapsed-time (util/format-duration @!elapsed-time) "00:00")]
+          [:input.hidden.ml:block.mx-2.w-20.ml:w-56.bg-gray-200.rounded-lg.cursor-pointer.focus:outline-none.h-1
+           {:type      "range"
+            :on-input  #(reset! !elapsed-time (.. % -target -value))
+            :on-change #(and @!player (> (.-readyState @!player) 0)
+                             (set! (.-currentTime @!player) @!elapsed-time))
+            :style     {:accentColor service-color}
+            :max       (if (and @!player (> (.-readyState @!player) 0))
+                         (.floor js/Math (.-duration @!player))
+                         100)
+            :value     @!elapsed-time}]
+          [:span.hidden.ml:block.mx-2
+           (if player-ready? (util/format-duration (.-duration @!player)) "00:00")]
+          [:button.hidden.ml:flex.focus:ring-transparent.mx-2
+           {:on-click #(rf/dispatch [::events/toggle-loop-file])}
+           [:i.fa-solid.fa-repeat
+            {:style {:color (when loop-file? service-color)}}]]
+          [:button.hidden.ml:flex.focus:ring-transparent.mx-2
+           {:on-click #(rf/dispatch [::events/toggle-loop-playlist])}
+           [:i.fa-solid.fa-retweet
+            {:style {:color (when loop-playlist? service-color)}}]]
+          [:div.hidden.ml:flex.items-center
+           [:button.focus:outline-none.mx-2
+            {:on-click #(rf/dispatch [::events/toggle-mute @!player])}
+            (if (or (and @!player muted?))
+              [:i.fa-solid.fa-volume-xmark]
+              [:i.fa-solid.fa-volume-low])]
+           [:input.w-20.bg-gray-200.rounded-lg.cursor-pointer.focus:outline-none.h-1.range-sm.mx-2
+            {:type     "range"
+             :on-input #(rf/dispatch [::events/change-volume-level (.. % -target -value) @!player])
+             :style    {:accentColor service-color}
+             :max      100
+             :value    volume-level}]]]
+         [:div.mx-2
+          [:i.fa-solid.fa-close.cursor-pointer
+           {:on-click (fn []
+                        (rf/dispatch [::events/toggle-audio-player])
+                        (.pause @!player)
+                        (set! (.-currentTime @!player) 0))}]]]]])))

+ 23 - 25
src/frontend/tubo/components/play_queue.cljs

@@ -3,12 +3,14 @@
    [re-frame.core :as rf]
    [reitit.frontend.easy :as rfe]
    [tubo.components.items :as items]
+   [tubo.components.loading :as loading]
    [tubo.events :as events]
    [tubo.util :as util]))
 
 (defn queue
   []
   (let [show-media-queue @(rf/subscribe [:show-media-queue])
+        show-audio-player-loading? @(rf/subscribe [:show-audio-player-loading])
         media-queue @(rf/subscribe [:media-queue])
         media-queue-pos @(rf/subscribe [:media-queue-pos])
         {:keys [uploader-name uploader-url
@@ -16,7 +18,8 @@
         !elapsed-time @(rf/subscribe [:elapsed-time])
         !player @(rf/subscribe [:player])
         loop-file? @(rf/subscribe [:loop-file])
-        loop-playlist? @(rf/subscribe [:loop-playlist])]
+        loop-playlist? @(rf/subscribe [:loop-playlist])
+        player-ready? (and @!player (> (.-readyState @!player) 0))]
     (when (and show-media-queue media-queue)
       [:div.fixed.flex.flex-col.items-center.px-5.py-2.min-w-full.w-full.z-30
        {:style {:minHeight "calc(100vh - 56px)" :height "calc(100vh - 56px)"}
@@ -40,9 +43,7 @@
             [:div.flex.w-full.h-24.rounded.px-2.cursor-pointer.my-2
              {:key      i
               :class    (when (= i media-queue-pos) "bg-[#f0f0f0] dark:bg-stone-800")
-              :on-click #(do
-                           (rf/dispatch [::events/change-media-queue-pos i])
-                           (reset! !elapsed-time 0))}
+              :on-click #(rf/dispatch [::events/change-media-queue-pos i])}
              [:div.w-56
               [items/thumbnail thumbnail-url nil url name duration {:classes "h-24"}]]
              [:div.flex.flex-col.px-4.py-2.w-full
@@ -63,14 +64,14 @@
          [:input.mx-2.bg-gray-200.rounded-lg.cursor-pointer.focus:outline-none.w-full.h-1
           {:type      "range"
            :on-input  #(reset! !elapsed-time (.. % -target -value))
-           :on-change #(and @!player (> (.-readyState @!player) 0)
-                            (set! (.-currentTime @!player) @!elapsed-time))
+           :on-change #(when player-ready?
+                         (set! (.-currentTime @!player) @!elapsed-time))
            :style     {:accentColor service-color}
-           :max       (if (and @!player (> (.-readyState @!player) 0))
+           :max       (if player-ready?
                         (.floor js/Math (.-duration @!player))
                         100)
            :value     @!elapsed-time}]
-         [:span (if (and @!player (> (.-readyState @!player) 0))
+         [:span (if player-ready?
                   (util/format-duration (.-duration @!player))
                   "00:00")]]
         [:div.flex.justify-center.items-center
@@ -81,34 +82,31 @@
          [:button.focus:outline-none.mx-2.text-xl
           {:class    (when-not (and media-queue (not= media-queue-pos 0))
                        "opacity-50 cursor-auto")
-           :on-click (when (and media-queue (not= media-queue-pos 0))
-                       #(do
-                          (rf/dispatch [::events/change-media-queue-pos
-                                        (- media-queue-pos 1)])
-                          (reset! !elapsed-time 0)))}
+           :on-click #(when (and media-queue (not= media-queue-pos 0))
+                        (rf/dispatch [::events/change-media-queue-pos
+                                      (- media-queue-pos 1)]))}
           [:i.fa-solid.fa-backward-step]]
          [:button.focus:outline-none.mx-2.text-xl
           {:on-click #(set! (.-currentTime @!player) (- @!elapsed-time 5))}
           [:i.fa-solid.fa-backward]]
          [:button.focus:outline-none.mx-2.text-3xl
-          {:on-click #(when-let [player @!player]
-                        (if (.-paused player)
-                          (.play player)
-                          (.pause player)))}
-          (if (and @!player (.-paused @!player))
-            [:i.fa-solid.fa-play]
-            [:i.fa-solid.fa-pause])]
+          {:on-click #(if (.-paused @!player)
+                        (.play @!player)
+                        (.pause @!player))}
+          (if show-audio-player-loading?
+            [loading/loading-icon service-color "text-3xl"]
+            (if (.-paused @!player)
+              [:i.fa-solid.fa-play]
+              [:i.fa-solid.fa-pause]))]
          [:button.focus:outline-none.mx-2.text-xl
           {:on-click #(set! (.-currentTime @!player) (+ @!elapsed-time 5))}
           [:i.fa-solid.fa-forward]]
          [:button.focus:ring-transparent.mx-2.text-xl
           {:class    (when-not (and media-queue (< (+ media-queue-pos 1) (count media-queue)))
                        "opacity-50 cursor-auto")
-           :on-click (when (and media-queue (< (+ media-queue-pos 1) (count media-queue)))
-                       #(do
-                          (rf/dispatch [::events/change-media-queue-pos
-                                        (+ media-queue-pos 1)])
-                          (reset! !elapsed-time 0)))}
+           :on-click #(when (and media-queue (< (+ media-queue-pos 1) (count media-queue)))
+                        (rf/dispatch [::events/change-media-queue-pos
+                                      (+ media-queue-pos 1)]))}
           [:i.fa-solid.fa-forward-step]]
          [:button.focus:ring-transparent.mx-2
           {:on-click #(rf/dispatch [::events/toggle-loop-playlist])}

+ 27 - 25
src/frontend/tubo/events.cljs

@@ -72,9 +72,9 @@
 
 (rf/reg-fx
  ::player-playback
- (fn [{:keys [player]}]
+ (fn [{:keys [player paused?]}]
    (when (and player (> (.-readyState player) 0))
-     (if (.-paused player)
+     (if paused?
        (.play player)
        (.pause player)))))
 
@@ -105,13 +105,6 @@
     :store (assoc store :volume-level value)
     ::player-volume {:player player :volume value}}))
 
-(rf/reg-event-fx
- ::start-playback
- (fn [{:keys [db]} [_ player]]
-   {::player-playback {:player player}
-    ::player-volume {:player player :volume (:volume-level db)}
-    ::player-mute {:player player :muted? (:muted db)}}))
-
 (rf/reg-event-fx
  ::toggle-mute
  [(rf/inject-cofx :store)]
@@ -238,27 +231,28 @@
  ::add-to-media-queue
   [(rf/inject-cofx :store)]
   (fn [{:keys [db store]} [_ stream]]
-    (let [updated-db (update db :media-queue conj stream)
-          idx (.indexOf (:media-queue updated-db) stream)]
+    (let [updated-db (update db :media-queue conj stream)]
       {:db    updated-db
-       :store (assoc store :media-queue (:media-queue updated-db))
-       :fx    [[:dispatch [::fetch-audio-player-stream (:url stream) idx]]]})))
+       :store (assoc store :media-queue (:media-queue updated-db))})))
 
 (rf/reg-event-fx
  ::change-media-queue-pos
  [(rf/inject-cofx :store)]
  (fn [{:keys [db store]} [_ idx]]
-   {:db (assoc db :media-queue-pos idx)
-    :store (assoc store :media-queue-pos idx)}))
+   (let [stream (get (:media-queue db) idx)]
+     {:db    (-> db
+                 (assoc :media-queue-pos idx)
+                 (assoc-in [:media-queue idx :stream] ""))
+      :store (assoc store :media-queue-pos idx)
+      :fx    [[:dispatch [::fetch-audio-player-stream (:url stream) idx]]]})))
 
 (rf/reg-event-fx
  ::change-media-queue-stream
  [(rf/inject-cofx :store)]
  (fn [{:keys [db store]} [_ src idx]]
    (let [update-entry #(assoc-in % [:media-queue idx :stream] src)]
-     (when-not (-> db :media-queue (nth idx) :stream)
-       {:db (update-entry db)
-        :store (update-entry store)}))))
+     {:db    (update-entry db)
+      :store (update-entry store)})))
 
 (rf/reg-event-fx
  ::toggle-audio-player
@@ -277,9 +271,15 @@
  ::switch-to-audio-player
  [(rf/inject-cofx :store)]
  (fn [{:keys [db store]} [_ stream service-color]]
-   {:db (assoc db :show-audio-player true)
-    :store (assoc store :show-audio-player true)
-    :fx [[:dispatch [::add-to-media-queue (conj stream {:service-color service-color})]]]}))
+   (let [full-stream (conj {:service-color service-color} stream)
+         updated-db (update db :media-queue conj full-stream)
+         idx        (.indexOf (:media-queue updated-db) full-stream)]
+     {:db    (-> updated-db
+                 (assoc :show-audio-player true))
+      :store (-> store
+                 (assoc :show-audio-player true)
+                 (assoc :media-queue (:media-queue updated-db)))
+      :fx    [[:dispatch [::fetch-audio-player-stream (:url stream) idx]]]})))
 
 (rf/reg-event-fx
  ::enqueue-related-streams
@@ -287,10 +287,12 @@
  (fn [{:keys [db store]} [_ streams service-color]]
    {:db (assoc db :show-audio-player true)
     :store (assoc store :show-audio-player true)
-    :fx (into [] (map #(identity [:dispatch
-                                  [::add-to-media-queue
-                                   (conj {:service-color service-color} %)]])
-                      streams))}))
+    :fx (into [] (conj
+                  (map #(identity [:dispatch
+                                   [::add-to-media-queue
+                                    (conj {:service-color service-color} %)]])
+                       streams)
+                  [:dispatch [::fetch-audio-player-stream (:url (first streams)) 0]]))}))
 
 (rf/reg-event-fx
  ::add-to-bookmarks