|
| 1 | +(ns status-im2.contexts.chat.messages.content.audio.view |
| 2 | + (:require ["react-native-blob-util" :default ReactNativeBlobUtil] |
| 3 | + [goog.string :as gstring] |
| 4 | + [reagent.core :as reagent] |
| 5 | + [react-native.audio-toolkit :as audio] |
| 6 | + [status-im2.contexts.chat.messages.content.audio.style :as style] |
| 7 | + [react-native.platform :as platform] |
| 8 | + [taoensso.timbre :as log] |
| 9 | + [quo2.foundations.colors :as colors] |
| 10 | + [quo2.core :as quo] |
| 11 | + [react-native.core :as rn] |
| 12 | + [utils.re-frame :as rf] |
| 13 | + [utils.i18n :as i18n])) |
| 14 | + |
| 15 | +(def ^:const media-server-uri-prefix "https://localhost:") |
| 16 | +(def ^:const audio-path "/messages/audio") |
| 17 | +(def ^:const uri-param "?messageId=") |
| 18 | + |
| 19 | +(defonce active-players (atom {})) |
| 20 | +(defonce audio-uris (atom {})) |
| 21 | +(defonce progress-timer (atom nil)) |
| 22 | +(defonce current-player-key (reagent/atom nil)) |
| 23 | + |
| 24 | +(defn get-player-key |
| 25 | + [message-id in-pinned-view?] |
| 26 | + (str in-pinned-view? message-id)) |
| 27 | + |
| 28 | +(defn destroy-player |
| 29 | + [player-key] |
| 30 | + (when-let [player (@active-players player-key)] |
| 31 | + (audio/destroy-player player) |
| 32 | + (swap! active-players dissoc player-key))) |
| 33 | + |
| 34 | +(defn update-state |
| 35 | + [state new-state] |
| 36 | + (when-not (= @state new-state) |
| 37 | + (reset! state new-state))) |
| 38 | + |
| 39 | +(defn seek-player |
| 40 | + [player-key player-state value on-success] |
| 41 | + (when-let [player (@active-players player-key)] |
| 42 | + (audio/seek-player |
| 43 | + player |
| 44 | + value |
| 45 | + #(when on-success (on-success)) |
| 46 | + #(update-state player-state :error)) |
| 47 | + (update-state player-state :seeking))) |
| 48 | + |
| 49 | +(defn download-audio-http |
| 50 | + [base64-uri on-success] |
| 51 | + (-> (.config ReactNativeBlobUtil (clj->js {:trusty platform/ios?})) |
| 52 | + (.fetch "GET" (str base64-uri)) |
| 53 | + (.then #(on-success (.base64 ^js %))) |
| 54 | + (.catch #(log/error "could not fetch audio " base64-uri)))) |
| 55 | + |
| 56 | +(defn create-player |
| 57 | + [{:keys [progress-ref player-state player-key]} audio-url on-success] |
| 58 | + (download-audio-http |
| 59 | + audio-url |
| 60 | + (fn [base64-data] |
| 61 | + (let [player (audio/new-player |
| 62 | + (str "data:audio/acc;base64," base64-data) |
| 63 | + {:autoDestroy false |
| 64 | + :continuesToPlayInBackground false} |
| 65 | + (fn [] |
| 66 | + (update-state player-state :ready-to-play) |
| 67 | + (reset! progress-ref 0) |
| 68 | + (when (and @progress-timer (= @current-player-key player-key)) |
| 69 | + (js/clearInterval @progress-timer) |
| 70 | + (reset! progress-timer nil))))] |
| 71 | + (swap! active-players assoc player-key player) |
| 72 | + (audio/prepare-player |
| 73 | + player |
| 74 | + #(when on-success (on-success)) |
| 75 | + #(update-state player-state :error))))) |
| 76 | + (update-state player-state :preparing)) |
| 77 | + |
| 78 | +(defn play-pause-player |
| 79 | + [{:keys [player-key player-state progress-ref message-id audio-duration-ms seeking-audio? |
| 80 | + user-interaction?] |
| 81 | + :as params}] |
| 82 | + (let [mediaserver-port (rf/sub [:mediaserver/port]) |
| 83 | + audio-uri (str media-server-uri-prefix |
| 84 | + mediaserver-port |
| 85 | + audio-path |
| 86 | + uri-param |
| 87 | + message-id) |
| 88 | + player (@active-players player-key) |
| 89 | + playing? (= @player-state :playing)] |
| 90 | + (when-not playing? |
| 91 | + (reset! current-player-key player-key)) |
| 92 | + (if (and player |
| 93 | + (= (@audio-uris player-key) audio-uri)) |
| 94 | + (audio/toggle-playpause-player |
| 95 | + player |
| 96 | + (fn [] |
| 97 | + (update-state player-state :playing) |
| 98 | + (when @progress-timer |
| 99 | + (js/clearInterval @progress-timer)) |
| 100 | + (reset! progress-timer |
| 101 | + (js/setInterval |
| 102 | + (fn [] |
| 103 | + (let [player (@active-players player-key) |
| 104 | + current-time (audio/get-player-current-time player) |
| 105 | + playing? (= @player-state :playing)] |
| 106 | + (when (and playing? (not @seeking-audio?) (> current-time 0)) |
| 107 | + (reset! progress-ref current-time)))) |
| 108 | + 100))) |
| 109 | + (fn [] |
| 110 | + (update-state player-state :ready-to-play) |
| 111 | + (when (and @progress-timer user-interaction?) |
| 112 | + (js/clearInterval @progress-timer) |
| 113 | + (reset! progress-timer nil))) |
| 114 | + #(update-state player-state :error)) |
| 115 | + (do |
| 116 | + (swap! audio-uris assoc player-key audio-uri) |
| 117 | + (destroy-player player-key) |
| 118 | + (create-player params |
| 119 | + audio-uri |
| 120 | + (fn [] |
| 121 | + (reset! seeking-audio? false) |
| 122 | + (if (> @progress-ref 0) |
| 123 | + (let [seek-time (* audio-duration-ms @progress-ref) |
| 124 | + checked-seek-time (min audio-duration-ms seek-time)] |
| 125 | + (seek-player |
| 126 | + player-key |
| 127 | + player-state |
| 128 | + checked-seek-time |
| 129 | + #(play-pause-player params))) |
| 130 | + (play-pause-player params)))))))) |
| 131 | + |
| 132 | +(defn audio-message |
| 133 | + [{:keys [audio-duration-ms message-id]} |
| 134 | + {:keys [in-pinned-view?]}] |
| 135 | + (let [player-state (reagent/atom :not-loaded) |
| 136 | + progress (reagent/atom 0) |
| 137 | + seeking-audio? (reagent/atom false) |
| 138 | + player-key (get-player-key message-id in-pinned-view?)] |
| 139 | + [:f> |
| 140 | + (fn [] |
| 141 | + (let [player (@active-players player-key) |
| 142 | + duration (if (and player (not (#{:preparing :not-loaded :error} @player-state))) |
| 143 | + (audio/get-player-duration player) |
| 144 | + audio-duration-ms) |
| 145 | + time-secs (quot |
| 146 | + (if (or @seeking-audio? (#{:playing :seeking} @player-state)) |
| 147 | + (if (<= @progress 1) (* duration @progress) @progress) |
| 148 | + duration) |
| 149 | + 1000)] |
| 150 | + (rn/use-effect (fn [] #(destroy-player player-key))) |
| 151 | + (rn/use-effect |
| 152 | + (fn [] |
| 153 | + (when (and (some? @current-player-key) |
| 154 | + (not= @current-player-key player-key) |
| 155 | + (= @player-state :playing)) |
| 156 | + (play-pause-player {:player-key player-key |
| 157 | + :player-state player-state |
| 158 | + :progress-ref progress |
| 159 | + :message-id message-id |
| 160 | + :audio-duration-ms duration |
| 161 | + :seeking-audio? seeking-audio? |
| 162 | + :user-interaction? false}))) |
| 163 | + [@current-player-key]) |
| 164 | + (if (= @player-state :error) |
| 165 | + [quo/text |
| 166 | + {:style style/error-label |
| 167 | + :accessibility-label :audio-error-label |
| 168 | + :weight :medium |
| 169 | + :size :paragraph-2} |
| 170 | + (i18n/label :error-loading-audio)] |
| 171 | + [rn/view |
| 172 | + {:accessibility-label :audio-message-container |
| 173 | + :style (style/container)} |
| 174 | + [rn/touchable-opacity |
| 175 | + {:accessibility-label :play-pause-audio-message-button |
| 176 | + :on-press #(play-pause-player {:player-key player-key |
| 177 | + :player-state player-state |
| 178 | + :progress-ref progress |
| 179 | + :message-id message-id |
| 180 | + :audio-duration-ms duration |
| 181 | + :seeking-audio? seeking-audio? |
| 182 | + :user-interaction? true}) |
| 183 | + :style (style/play-pause-container)} |
| 184 | + [quo/icon |
| 185 | + (case @player-state |
| 186 | + :preparing :i/loading |
| 187 | + :playing :i/pause-audio |
| 188 | + :i/play-audio) |
| 189 | + {:size 20 |
| 190 | + :color colors/white}]] |
| 191 | + [quo/soundtrack |
| 192 | + {:style style/slider-container |
| 193 | + :audio-current-time-ms progress |
| 194 | + :player-ref (@active-players player-key) |
| 195 | + :seeking-audio? seeking-audio?}] |
| 196 | + [quo/text |
| 197 | + {:style style/timestamp |
| 198 | + :accessibility-label :audio-duration-label |
| 199 | + :weight :medium |
| 200 | + :size :paragraph-2} |
| 201 | + (gstring/format "%02d:%02d" (quot time-secs 60) (mod time-secs 60))]])))])) |
0 commit comments