구간반복 기능 추가

This commit is contained in:
klaus 2025-04-01 14:25:45 +09:00
parent c7af522cfb
commit bddf7b750b
7 changed files with 121 additions and 0 deletions

View File

@ -117,6 +117,7 @@ class AudioContentPlayerFragment(
}
adapter = AudioContentPlaylistDetailAdapter { contentId ->
binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle)
val extras = Bundle().apply {
putLong(
Constants.EXTRA_AUDIO_CONTENT_ID,
@ -165,6 +166,24 @@ class AudioContentPlayerFragment(
}
})
recyclerView.adapter = adapter
binding.ivLoopSegment.setOnClickListener {
val sessionCommand = SessionCommand("TOGGLE_SEGMENT_LOOP", Bundle.EMPTY)
val resultFuture = mediaController!!.sendCustomCommand(sessionCommand, Bundle.EMPTY)
resultFuture.addListener(
{
val result = resultFuture.get()
if (result.resultCode == SessionResult.RESULT_SUCCESS) {
val imageRes = result.extras.getInt(
Constants.EXTRA_PLAYLIST_SEGMENT_LOOP_IMAGE,
R.drawable.ic_loop_segment_idle
)
binding.ivLoopSegment.setImageResource(imageRes)
}
},
ContextCompat.getMainExecutor(requireContext())
)
}
}
private fun bindData() {
@ -282,6 +301,7 @@ class AudioContentPlayerFragment(
})
if (playlist.isNotEmpty()) {
binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle)
val extras = Bundle().apply {
putParcelableArrayList(
Constants.EXTRA_AUDIO_CONTENT_PLAYLIST,
@ -292,6 +312,7 @@ class AudioContentPlayerFragment(
mediaController!!.sendCustomCommand(sessionCommand, extras)
adapter.updateItems(playlist)
} else {
binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle)
context?.let {
val sessionCommand = SessionCommand("GET_PLAYLIST", Bundle.EMPTY)
val resultFuture = mediaController!!.sendCustomCommand(sessionCommand, Bundle.EMPTY)
@ -341,6 +362,7 @@ class AudioContentPlayerFragment(
binding.ivSkipForward.setOnClickListener {
mediaController?.let {
binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle)
val sessionCommand = SessionCommand(
"PLAY_NEXT_CONTENT",
Bundle.EMPTY
@ -351,6 +373,7 @@ class AudioContentPlayerFragment(
binding.ivSkipBack.setOnClickListener {
mediaController?.let {
binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle)
val sessionCommand = SessionCommand(
"PLAY_PREVIOUS_CONTENT",
Bundle.EMPTY
@ -361,6 +384,7 @@ class AudioContentPlayerFragment(
binding.ivSeekForward10.setOnClickListener {
mediaController?.let {
binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle)
val sessionCommand = SessionCommand(
"SEEK_FORWARD",
Bundle.EMPTY
@ -371,6 +395,7 @@ class AudioContentPlayerFragment(
binding.ivSeekBackward10.setOnClickListener {
mediaController?.let {
binding.ivLoopSegment.setImageResource(R.drawable.ic_loop_segment_idle)
val sessionCommand = SessionCommand(
"SEEK_BACKWARD",
Bundle.EMPTY

View File

@ -5,6 +5,8 @@ import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.core.os.BundleCompat
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
@ -25,6 +27,7 @@ import com.orhanobut.logger.Logger
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.playlist.detail.AudioContentPlaylistContent
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
@ -45,6 +48,12 @@ class AudioContentPlayerService : MediaSessionService() {
private val playlist = ArrayList<AudioContentPlaylistContent>()
private var loopStartMs: Long? = null
private var loopEndMs: Long? = null
private var isLooping = false
private val loopHandler = Handler(Looper.getMainLooper())
private val loopCheckInterval = 100L // 0.1초 간격
override fun onCreate() {
super.onCreate()
@ -66,6 +75,7 @@ class AudioContentPlayerService : MediaSessionService() {
compositeDisposable.dispose()
SharedPreferenceManager.isPlayerServiceRunning = false
stopLoop()
super.onDestroy()
}
@ -83,6 +93,44 @@ class AudioContentPlayerService : MediaSessionService() {
return super.onStartCommand(intent, flags, startId)
}
private fun toggleSegmentLoop() {
when {
loopStartMs == null -> {
loopStartMs = player?.currentPosition
}
loopEndMs == null -> {
loopEndMs = player?.currentPosition
isLooping = true
startLoopMonitoring()
}
else -> {
stopLoop()
}
}
}
private fun startLoopMonitoring() {
loopHandler.post(object : Runnable {
override fun run() {
if (isLooping && loopStartMs != null && loopEndMs != null && player != null) {
if (player!!.currentPosition >= loopEndMs!!) {
player!!.seekTo(loopStartMs!!)
}
loopHandler.postDelayed(this, loopCheckInterval)
}
}
})
}
private fun stopLoop() {
isLooping = false
loopStartMs = null
loopEndMs = null
loopHandler.removeCallbacksAndMessages(null)
}
private fun initPlayer() {
player = ExoPlayer.Builder(this).build()
player!!.addListener(object : Player.Listener {
@ -132,6 +180,7 @@ class AudioContentPlayerService : MediaSessionService() {
.add(SessionCommand("GET_PLAYLIST", Bundle.EMPTY))
.add(SessionCommand("SEEK_FORWARD", Bundle.EMPTY))
.add(SessionCommand("SEEK_BACKWARD", Bundle.EMPTY))
.add(SessionCommand("TOGGLE_SEGMENT_LOOP", Bundle.EMPTY))
.build()
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
@ -147,6 +196,7 @@ class AudioContentPlayerService : MediaSessionService() {
): ListenableFuture<SessionResult> {
return when (customCommand.customAction) {
"UPDATE_PLAYLIST" -> {
stopLoop()
val playlist = BundleCompat.getParcelableArrayList(
args,
Constants.EXTRA_AUDIO_CONTENT_PLAYLIST,
@ -163,31 +213,66 @@ class AudioContentPlayerService : MediaSessionService() {
}
"PLAY_NEXT_CONTENT" -> {
stopLoop()
playNextContent()
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}
"PLAY_PREVIOUS_CONTENT" -> {
stopLoop()
playPreviousContent()
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}
"PLAY_SELECTED_CONTENT" -> {
stopLoop()
val selectedContentId = args.getLong(Constants.EXTRA_AUDIO_CONTENT_ID)
playSelectedContent(contentId = selectedContentId)
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}
"SEEK_FORWARD" -> {
stopLoop()
playSeekForward()
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}
"SEEK_BACKWARD" -> {
stopLoop()
playSeekBackward()
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}
"TOGGLE_SEGMENT_LOOP" -> {
val extras = Bundle().apply {
putInt(
Constants.EXTRA_PLAYLIST_SEGMENT_LOOP_IMAGE,
when {
loopStartMs == null -> {
R.drawable.ic_loop_segment_start_set
}
loopEndMs == null -> {
R.drawable.ic_loop_segment_active
}
else -> {
R.drawable.ic_loop_segment_idle
}
}
)
}
toggleSegmentLoop()
Futures.immediateFuture(
SessionResult(
SessionResult.RESULT_SUCCESS,
extras
)
)
}
"GET_PLAYLIST" -> {
val extras = Bundle().apply {
putParcelableArrayList(

View File

@ -77,6 +77,7 @@ object Constants {
const val EXTRA_AUDIO_CONTENT_ALERT_PREVIEW = "audio_content_alert_preview"
const val EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL = "audio_content_cover_image_url"
const val EXTRA_AUDIO_CONTENT_PLAYLIST = "extra_audio_content_playlist"
const val EXTRA_PLAYLIST_SEGMENT_LOOP_IMAGE = "extra_playlist_segment_loop_image"
const val EXTRA_IS_SHOW_SECRET = "extra_is_show_secret"
const val LIVE_SERVICE_NOTIFICATION_ID: Int = 2

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -165,6 +165,16 @@
app:layout_constraintStart_toEndOf="@+id/iv_play_or_pause"
app:layout_constraintTop_toTopOf="@+id/iv_play_or_pause" />
<ImageView
android:id="@+id/iv_loop_segment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:padding="5dp"
android:src="@drawable/ic_loop_segment_idle"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<ImageView
android:id="@+id/iv_playlist"
android:layout_width="wrap_content"