@@ -15,16 +15,21 @@ import androidx.media3.exoplayer.ExoPlayer
1515import androidx.media3.session.*
1616import com.google.common.util.concurrent.Futures
1717import com.google.common.util.concurrent.ListenableFuture
18- import kotlinx.coroutines.MainScope
19- import kotlinx.coroutines.launch
18+ import dev.icerock.moko.resources.desc.desc
19+ import kotlinx.coroutines.*
20+ import project.pipepipe.app.MR
2021import project.pipepipe.app.mediasource.CustomMediaSourceFactory
2122import project.pipepipe.app.mediasource.toMediaItem
2223import project.pipepipe.app.PlaybackMode
2324import project.pipepipe.app.SharedContext
2425import project.pipepipe.app.SharedContext.playbackMode
2526import project.pipepipe.app.database.DatabaseOperations
2627import project.pipepipe.app.helper.ToastManager
28+ import project.pipepipe.app.helper.executeJobFlow
2729import project.pipepipe.shared.infoitem.StreamInfo
30+ import project.pipepipe.shared.infoitem.SponsorBlockSegmentInfo
31+ import project.pipepipe.shared.job.SupportedJobType
32+ import project.pipepipe.app.ui.component.player.SponsorBlockHelper
2833import project.pipepipe.app.uistate.VideoDetailPageState
2934import java.util.concurrent.ExecutorService
3035import java.util.concurrent.Executors
@@ -43,6 +48,12 @@ class PlaybackService : MediaLibraryService() {
4348 private lateinit var sessionCallbackExecutor: ExecutorService
4449 private var playbackButtonState = PlaybackButtonState .ALL_OFF
4550
51+ // SponsorBlock related fields
52+ private val sponsorBlockCache = mutableMapOf<String , List <SponsorBlockSegmentInfo >>()
53+ private val skippedSegments = mutableMapOf<String , MutableSet <String >>()
54+ private var sponsorBlockCheckJob: Job ? = null
55+ private val serviceScope = CoroutineScope (Dispatchers .Main + SupervisorJob ())
56+
4657 private enum class PlaybackButtonState (
4758 val repeatMode : Int ,
4859 val shuffleEnabled : Boolean ,
@@ -96,6 +107,10 @@ class PlaybackService : MediaLibraryService() {
96107 }
97108 }
98109
110+ companion object {
111+ private const val SPONSOR_BLOCK_CHECK_INTERVAL_MS = 500L
112+ }
113+
99114 object CustomCommands {
100115 const val ACTION_SET_PLAYBACK_MODE = " project.pipepipe.app.action.SET_PLAYBACK_MODE"
101116 const val ARG_MODE = " mode"
@@ -291,6 +306,13 @@ class PlaybackService : MediaLibraryService() {
291306 }
292307 stopPlaybackReceiver = null
293308 }
309+
310+ // Clean up SponsorBlock resources
311+ stopSponsorBlockCheck()
312+ serviceScope.cancel()
313+ sponsorBlockCache.clear()
314+ skippedSegments.clear()
315+
294316 super .onDestroy()
295317 session?.release()
296318 player.release()
@@ -384,12 +406,87 @@ class PlaybackService : MediaLibraryService() {
384406 PendingIntent .FLAG_UPDATE_CURRENT or PendingIntent .FLAG_IMMUTABLE
385407 )
386408 }
409+
410+ // SponsorBlock related methods
411+ private fun loadSponsorBlockForMedia (mediaItem : MediaItem ) {
412+ val mediaId = mediaItem.mediaId
413+ val sponsorBlockUrl = mediaItem.mediaMetadata.extras?.getString(" KEY_SPONSORBLOCK_URL" )
414+
415+ if (sponsorBlockUrl == null || sponsorBlockCache.containsKey(mediaId)) {
416+ return
417+ }
418+
419+ serviceScope.launch {
420+ try {
421+ val result = withContext(Dispatchers .IO ) {
422+ executeJobFlow(
423+ SupportedJobType .FETCH_SPONSORBLOCK_SEGMENT_LIST ,
424+ sponsorBlockUrl,
425+ null
426+ )
427+ }
428+
429+ val segments = result.pagedData?.itemList as ? List <SponsorBlockSegmentInfo > ? : emptyList()
430+ sponsorBlockCache[mediaId] = segments
431+ } catch (e: Exception ) {
432+ e.printStackTrace()
433+ }
434+ }
435+ }
436+
437+ private fun startSponsorBlockCheck () {
438+ sponsorBlockCheckJob?.cancel()
439+ sponsorBlockCheckJob = serviceScope.launch {
440+ while (isActive) {
441+ checkAndSkipSponsorBlock()
442+ delay(SPONSOR_BLOCK_CHECK_INTERVAL_MS )
443+ }
444+ }
445+ }
446+
447+ private fun stopSponsorBlockCheck () {
448+ sponsorBlockCheckJob?.cancel()
449+ }
450+
451+ private fun checkAndSkipSponsorBlock () {
452+ if (! SponsorBlockHelper .isEnabled()) return
453+
454+ val mediaItem = player.currentMediaItem ? : return
455+ val mediaId = mediaItem.mediaId
456+ val segments = sponsorBlockCache[mediaId] ? : return
457+ val position = player.currentPosition
458+ val alreadySkipped = skippedSegments.getOrPut(mediaId) { mutableSetOf () }
459+
460+ val currentSegment = segments.firstOrNull { segment ->
461+ position.toDouble() in segment.startTime.. segment.endTime
462+ }
463+
464+ currentSegment?.let { segment ->
465+ if (! alreadySkipped.contains(segment.uuid) &&
466+ SponsorBlockHelper .shouldSkipSegment(segment)) {
467+
468+ player.seekTo(segment.endTime.toLong())
469+ alreadySkipped.add(segment.uuid)
470+
471+ if (SponsorBlockHelper .isNotificationsEnabled()) {
472+ MainScope ().launch {
473+ ToastManager .show(MR .strings.player_skipped_category.desc()
474+ .toString(this @PlaybackService).format(segment.category.name))
475+ }
476+ }
477+ }
478+ }
479+ }
387480 private fun createPlayerListener (): Player .Listener {
388481 return object : Player .Listener {
389482 override fun onMediaItemTransition (mediaItem : MediaItem ? , reason : Int ) {
390483 if (reason != Player .MEDIA_ITEM_TRANSITION_REASON_REPEAT ) {
391484 saveCurrentProgress()
392485 }
486+ mediaItem?.let {
487+ skippedSegments[it.mediaId] = mutableSetOf ()
488+ loadSponsorBlockForMedia(it)
489+ }
393490 }
394491
395492 override fun onRepeatModeChanged (repeatMode : Int ) {
@@ -404,6 +501,15 @@ class PlaybackService : MediaLibraryService() {
404501 if (playbackState == Player .STATE_ENDED ) {
405502 saveCurrentProgress()
406503 }
504+ when (playbackState) {
505+ Player .STATE_READY -> {
506+ player.currentMediaItem?.let { loadSponsorBlockForMedia(it) }
507+ startSponsorBlockCheck()
508+ }
509+ Player .STATE_ENDED , Player .STATE_IDLE -> {
510+ stopSponsorBlockCheck()
511+ }
512+ }
407513 }
408514
409515 override fun onIsPlayingChanged (isPlaying : Boolean ) {
0 commit comments