Tensegritics / ClojureDart

Clojure dialect for Flutter and Dart
1.44k stars 91 forks source link

Audio Service sample #316

Open ianffcs opened 4 months ago

ianffcs commented 4 months ago

Audio Service sample reflecting this example: https://github.com/ryanheise/audio_service/blob/minor/audio_service/example/lib/example_android13.dart

dupuchba commented 4 months ago

some remarks:

I've not finished the work, I am quite busy but it's a start

(ns sample.audio-service
  "Sample definided inside audio_service package on:
   https://github.com/ryanheise/audio_service/blob/minor/audio_service/example/lib/example_android13.dart"
  (:require ["package:flutter/material.dart" :as m]
            ["package:flutter/widgets.dart" :as w]
            ["package:audio_service/audio_service.dart" :as as]
            ["package:just_audio/just_audio.dart" :as ja]
            ["package:rxdart/rxdart.dart" :as rx]
            ["common.dart" :as c]
            ["dart:async" :as da]
            [cljd.flutter :as f]))

(def ^as/MediaItem media-item
  (as/MediaItem
    .id "https://s3.amazonaws.com/scifri-episodes/scifri20181123-episode.mp3"
    .album "Science Friday",
    .title "A Salute To Head-Scratching Science"
    .artist "Science Friday and WNYC Studios"
    .duration (Duration .milliseconds 5739820)
    .artUri (Uri/parse "https://media.wnyc.org/i/1400/1400/l/80/1/ScienceFriday_WNYCStudios_1400.jpg")))

(deftype MyAudioHandler [^ja/AudioPlayer audio-player]
  :extends (as/BaseAudioHandler)
  (play [_]
    (.play audio-player))
  (pause [_]
    (.pause audio-player))
  (stop [_]
    (.stop audio-player))
  ^:mixin as/SeekHandler
  (seek [_ ^Duration duration]
    (.seek audio-player duration)))

(defn ^:async main []
  (.ensureInitialized w/WidgetsFlutterBinding)
  (f/run
    (m/MaterialApp .title "Welcome to Flutter")
    .home
    (m/Scaffold .appBar (m/AppBar .title (m/Text "Welcome to ClojureDart")))
    .body
    m/Center
    :managed [audio-player (ja/AudioPlayer)]
    :let [transform-event
          (fn [^ja/PlaybackEvent event]
            (as/PlaybackState
              .controls [as/MediaControl.rewind
                         (if (some-> audio-player .-playing)
                           as/MediaControl.pause
                           as/MediaControl.play)
                         as/MediaControl.stop
                         as/MediaControl.fastForward]
              .systemActions #{as/MediaAction.seek
                               as/MediaAction.seekForward
                               as/MediaAction.seekBackward}
              .androidCompactActionIndices [0 1 3]
              .processingState (get {ja/ProcessingState.idle      as/AudioProcessingState.idle
                                     ja/ProcessingState.loading   as/AudioProcessingState.loading
                                     ja/ProcessingState.buffering as/AudioProcessingState.buffering
                                     ja/ProcessingState.ready     as/AudioProcessingState.ready
                                     ja/ProcessingState.completed as/AudioProcessingState.completed}
                                 (some-> audio-player
                                   .-processingState))
              .playing (-> audio-player
                         .-playing)
              .updatePosition (-> audio-player .-position)
              .bufferedPosition (-> audio-player .-bufferedPosition)
              .speed (-> audio-player .-speed)
              .queueIndex (-> event .-currentIndex)))]
    :watch [^MyAudioHandler? as-handler  (as/AudioService.init
                                           .builder (fn []  ^as/AudioHandler (->MyAudioHandler ^ja/AudioPlayer audio-player))
                                           .config (as/AudioServiceConfig
                                                     .androidNotificationChannelId "sample.audioService"
                                                     .androidNotificationChannelName "Sample Audio Service"
                                                     .androidNotificationOngoing true
                                                     .androidShowNotificationBadge true)) :dispose nil]
    :when as-handler
    :let [pipe-state (some-> audio-player
                       .-playbackEventStream
                       ^#/(da/Stream as/PlaybackState) (.map (fn [a] (prn 'CACA) (transform-event a)))
                       (.pipe (.-playbackState as-handler)))
          _add-item-to-audio-handler (.add (.-mediaItem as-handler) media-item)
          _set-audio-source-to-media-id (.setAudioSource audio-player (-> media-item
                                                                        .-id
                                                                        Uri/parse
                                                                        ja/AudioSource.uri))
          media-stream (rx/Rx.combineLatest2 (.-mediaItem as-handler)
                         (as/AudioService.position)
                         (fn [^as/MediaItem media-item
                              ^Duration position]
                           {:media-item media-item
                            :position position}))]
    :when audio-player
    (m/Column .mainAxisAlignment m/MainAxisAlignment.center)
    .children
    [(f/widget
       :watch [media-item (.-mediaItem as-handler)]
       :when media-item
       (m/Text (.-title ^as/MediaItem media-item)))
     (f/widget
       :watch [playback-state (.-playbackState as-handler) #_(stream (map dedupe) (.-playbackState as-handler))]
       :let [playing (some-> playback-state .-playing)]
       (m/Row .mainAxisAlignment m/MainAxisAlignment.center)
       .children
       [(m/IconButton
          .icon (m/Icon m/Icons.fast_rewind)
          .iconSize 64
          .onPressed (fn [] (.rewind ^MyAudioHandler as-handler) nil))
        (m/IconButton
          .icon (m/Icon (if playing m/Icons.pause m/Icons.play_arrow))
          .iconSize 64
          .onPressed (fn []
                       (if playing
                         (.pause ^MyAudioHandler as-handler)
                         (.play ^MyAudioHandler as-handler))
                       nil))
        (m/IconButton
          .icon (m/Icon m/Icons.stop)
          .iconSize 64
          .onPressed (fn []
                       (.stop ^MyAudioHandler as-handler)
                       nil))
        (m/IconButton
          .icon (m/Icon m/Icons.fast_forward)
          .iconSize 64
          .onPressed (fn []
                       (.fastForward ^MyAudioHandler as-handler)
                       nil))])

     (f/widget
       :watch [ms media-stream]
       :when ms
       (c/SeekBar
         .duration (or (some-> ^as/MediaItem (get ms :media-item) .-duration) Duration/zero)
         .position (or (some-> ms :position) Duration/zero)
         .onChangeEnd (fn [new-pos]
                        (.seek ^as/SeekHandler as-handler new-pos)
                        nil)))

     (f/widget
       :watch [ps (some-> ^as/AudioHandler as-handler
                    .-playbackState
                    (.map (fn [st]
                            (.-playing ^as/PlaybackState st)))
                    (.distinct))]
       (m/Text (str "Processing state: " (or ps as/AudioProcessingState.idle))))]))
cgrand commented 4 months ago

Hey @ianffcs thank you for this work. However I think it's better for samples to focus on one thing at a time so that the reader doesn't have to grok several concepts/libs at once. Here several unrelated packages are used. Would it be possible to reduce the scope?

ianffcs commented 4 months ago

I can't think on how I can reduce the scope, because it's a sample about having an audio player that plays on background. This is the minimal example that I've found around for doing this.

This is very useful for anyone that wants to implement this audio-background feature in any app.