WIP reproductor de audio
This commit is contained in:
parent
d082d7c4d6
commit
c6c32e54fb
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
5
app/src/main/res/drawable/baseline_pause_circle_24.xml
Normal file
5
app/src/main/res/drawable/baseline_pause_circle_24.xml
Normal 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>
|
||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user