From 783ad76b21626ad30625d22afe384e089d471f2b Mon Sep 17 00:00:00 2001 From: Miguel Ángel Moreno Date: Wed, 1 Nov 2023 18:32:46 +0100 Subject: 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. --- src/frontend/tubo/components/audio_player.cljs | 243 ++++++++++++------------- src/frontend/tubo/components/play_queue.cljs | 48 +++-- src/frontend/tubo/events.cljs | 52 +++--- 3 files changed, 170 insertions(+), 173 deletions(-) (limited to 'src/frontend') diff --git a/src/frontend/tubo/components/audio_player.cljs b/src/frontend/tubo/components/audio_player.cljs index 7cc5fb1..ae4c4e4 100644 --- a/src/frontend/tubo/components/audio_player.cljs +++ b/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))}]]]]]))) diff --git a/src/frontend/tubo/components/play_queue.cljs b/src/frontend/tubo/components/play_queue.cljs index 258d06e..bf9f433 100644 --- a/src/frontend/tubo/components/play_queue.cljs +++ b/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])} diff --git a/src/frontend/tubo/events.cljs b/src/frontend/tubo/events.cljs index 8254cd4..79c9e93 100644 --- a/src/frontend/tubo/events.cljs +++ b/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 -- cgit v1.2.3