WIP reproductor de audio

This commit is contained in:
Erik Everardo 2023-12-20 23:06:01 -06:00
parent d082d7c4d6
commit c6c32e54fb
4 changed files with 165 additions and 13 deletions

View File

@ -2,6 +2,8 @@ package com.isolaatti.audio.player
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Looper
import android.util.Log
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
@ -9,17 +11,98 @@ import androidx.media3.common.MediaItem
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import com.isolaatti.audio.common.domain.Audio import com.isolaatti.audio.common.domain.Audio
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.Timer
import java.util.TimerTask
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
class AudioPlayerConnector( class AudioPlayerConnector(
private val context: Context private val context: Context
): LifecycleEventObserver { ): LifecycleEventObserver {
companion object {
const val TAG = "AudioPlayerConnector"
}
private var player: Player? = null private var player: Player? = null
private var audio: Audio? = null
private var mediaItem: MediaItem? = null private var mediaItem: MediaItem? = null
private var ended = false
private val listeners: MutableList<Listener> = mutableListOf()
private val scheduleExecutorService = Executors.newScheduledThreadPool(1)
private var timerFuture: ScheduledFuture<*>? = null
private val timerRunnable = Runnable {
CoroutineScope(Dispatchers.Main).launch {
audio?.let {
val progress = (player?.currentPosition)
if(progress != null) {
listeners.forEach { listener -> listener.progressChanged((progress / 1000).toInt(), it) }
}
}
}
}
private fun startTimer() {
Log.d(TAG, "startTimer()")
if(timerFuture == null) {
timerFuture = scheduleExecutorService.scheduleAtFixedRate(timerRunnable, 0, 100, TimeUnit.MILLISECONDS)
}
}
private fun stopTimer() {
timerFuture?.cancel(false)
timerFuture = null
}
private val playerListener: Player.Listener = object: Player.Listener { private val playerListener: Player.Listener = object: Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
if(audio != null) {
listeners.forEach { listener -> listener.onPlaying(isPlaying, audio!!)}
}
if(isPlaying) {
ended = false
}
}
override fun onIsLoadingChanged(isLoading: Boolean) {
audio?.let {
listeners.forEach { listener -> listener.isLoading(isLoading, it) }
}
}
override fun onPlaybackStateChanged(playbackState: Int) {
when(playbackState) {
Player.STATE_ENDED -> {
Log.d(TAG, "STATE_ENDED")
audio?.let {
listeners.forEach { listener -> listener.onPlaying(false, it)}
}
stopTimer()
ended = true
}
Player.STATE_BUFFERING -> {}
Player.STATE_IDLE -> {}
Player.STATE_READY -> {
Log.d(TAG, "STATE_READY")
player?.totalBufferedDuration?.let {
val seconds = (it / 1000).toInt()
Log.d(TAG, "Duration $it")
audio?.let {
listeners.forEach { listener -> listener.durationChanged(seconds, it) }
}
}
startTimer()
}
}
}
} }
private fun initializePlayer() { private fun initializePlayer() {
@ -29,11 +112,16 @@ class AudioPlayerConnector(
player?.prepare() player?.prepare()
} }
fun addListener(listener: Listener) {
listeners.add(listener)
}
private fun releasePlayer() { private fun releasePlayer() {
player?.run { player?.run {
release() release()
} }
player = null player = null
stopTimer()
} }
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
@ -53,9 +141,31 @@ class AudioPlayerConnector(
} }
} }
fun playAudio(audio: Audio) { fun playPauseAudio(audio: Audio) {
// intention is to pause current audio
if(audio == this.audio && player?.isPlaying == true) {
player?.pause()
return
} else if(audio == this.audio) {
if(ended) {
player?.seekTo(0)
ended = false
return
}
player?.play()
return
}
this.audio = audio
mediaItem = MediaItem.fromUri(Uri.parse(audio.downloadUrl)) mediaItem = MediaItem.fromUri(Uri.parse(audio.downloadUrl))
player?.setMediaItem(mediaItem!!) player?.setMediaItem(mediaItem!!)
} }
interface Listener {
fun onPlaying(isPlaying: Boolean, audio: Audio)
fun isLoading(isLoading: Boolean, audio: Audio)
fun progressChanged(second: Int, audio: Audio)
fun durationChanged(duration: Int, audio: Audio)
}
} }

View File

@ -7,6 +7,7 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.content.res.AppCompatResources
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
@ -86,6 +87,27 @@ class ProfileMainFragment : Fragment() {
} }
} }
private val audioPlayerConnectorListener = object: AudioPlayerConnector.Listener {
override fun onPlaying(isPlaying: Boolean, audio: Audio) {
viewBinding.playButton.icon = AppCompatResources.getDrawable(requireContext(), if(isPlaying) R.drawable.baseline_pause_circle_24 else R.drawable.baseline_play_circle_24)
}
override fun isLoading(isLoading: Boolean, audio: Audio) {
viewBinding.playButton.isEnabled = !isLoading
viewBinding.audioProgress.isIndeterminate = isLoading
}
override fun progressChanged(second: Int, audio: Audio) {
viewBinding.audioProgress.setProgress(second, true)
}
override fun durationChanged(duration: Int, audio: Audio) {
viewBinding.audioProgress.max = duration
}
}
private val profileObserver = Observer<UserProfile> { profile -> private val profileObserver = Observer<UserProfile> { profile ->
viewBinding.profileImageView.load(UrlGen.userProfileImage(profile.userId), imageLoader) viewBinding.profileImageView.load(UrlGen.userProfileImage(profile.userId), imageLoader)
@ -110,7 +132,7 @@ class ProfileMainFragment : Fragment() {
} }
audioDescriptionAudio = profile.descriptionAudio audioDescriptionAudio = profile.descriptionAudio
viewBinding.playButton.visibility = if(profile.descriptionAudio != null) View.VISIBLE else View.GONE viewBinding.playButtonContainer.visibility = if(profile.descriptionAudio != null) View.VISIBLE else View.GONE
setupUiForUserType(profile.isUserItself) setupUiForUserType(profile.isUserItself)
} }
@ -258,7 +280,7 @@ class ProfileMainFragment : Fragment() {
} }
viewBinding.playButton.setOnClickListener { viewBinding.playButton.setOnClickListener {
audioDescriptionAudio?.let { audio -> audioDescriptionAudio?.let { audio ->
audioPlayerConnector.playAudio(audio) audioPlayerConnector.playPauseAudio(audio)
} }
} }
} }
@ -370,6 +392,8 @@ class ProfileMainFragment : Fragment() {
audioPlayerConnector = AudioPlayerConnector(requireContext()) audioPlayerConnector = AudioPlayerConnector(requireContext())
audioPlayerConnector.addListener(audioPlayerConnectorListener)
viewLifecycleOwner.lifecycle.addObserver(audioPlayerConnector) viewLifecycleOwner.lifecycle.addObserver(audioPlayerConnector)

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM11,16H9V8h2V16zM15,16h-2V8h2V16z"/>
</vector>

View File

@ -71,18 +71,31 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.google.android.material.button.MaterialButton
android:id="@+id/play_button" <RelativeLayout
android:id="@+id/play_button_container"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" tools:visibility="visible">
android:layout_marginStart="8dp"
app:icon="@drawable/baseline_play_circle_24" <com.google.android.material.progressindicator.CircularProgressIndicator
android:visibility="gone" android:id="@+id/audio_progress"
tools:visibility="visible" android:layout_width="wrap_content"
style="?attr/materialIconButtonStyle"/> android:layout_height="wrap_content" />
<com.google.android.material.button.MaterialButton
android:id="@+id/play_button"
style="?attr/materialIconButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/baseline_play_circle_24" />
</RelativeLayout>
<TextView <TextView
android:id="@+id/text_view_description" android:id="@+id/text_view_description"
@ -95,7 +108,7 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/play_button" app:layout_constraintStart_toEndOf="@id/play_button_container"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="Hi there, I am software developer!" /> tools:text="Hi there, I am software developer!" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>