Skip to content

Commit d191145

Browse files
fix: sponsorblock not working as expected
1 parent ff4ec81 commit d191145

File tree

3 files changed

+122
-25
lines changed

3 files changed

+122
-25
lines changed

android/src/main/kotlin/project/pipepipe/app/mediasource/CustomMediaSourceFactory.kt

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,15 @@ class CustomMediaSourceFactory() : MediaSource.Factory {
5454
val dashUrl = extras.getString("KEY_DASH_URL")
5555
val hlsUrl = extras.getString("KEY_HLS_URL")
5656
val headers = extras.getSerializable("KEY_HEADER_MAP") as? MutableMap<String, String> ?: mutableMapOf()
57+
val sponsorblockUrl = extras.getString("KEY_SPONSORBLOCK_URL")
5758

5859
if (dashManifestString == null && dashUrl == null && hlsUrl == null) {
5960
return LazyUrlMediaSource(
6061
mediaItem = mediaItem,
6162
mediaSourceFactory = this,
6263
)
6364
}
64-
return createActualMediaSource(mediaItem, dashManifestString, dashUrl, hlsUrl, headers)
65+
return createActualMediaSource(mediaItem, dashManifestString, dashUrl, hlsUrl, headers, sponsorblockUrl)
6566
}
6667

6768

@@ -70,7 +71,8 @@ class CustomMediaSourceFactory() : MediaSource.Factory {
7071
dashManifest: String?,
7172
dashUrl: String?,
7273
hlsUrl: String?,
73-
headers: Map<String, String>
74+
headers: Map<String, String>,
75+
sponsorblockUrl: String?
7476
): MediaSource {
7577

7678
val requestHeaders = headers.toMutableMap().apply {
@@ -111,26 +113,27 @@ class CustomMediaSourceFactory() : MediaSource.Factory {
111113
Uri.parse("https://example.com/invalid.mpd"),
112114
ByteArrayInputStream(dashManifest.toByteArray(StandardCharsets.UTF_8))
113115
)
114-
val dashMediaItem = mediaItem.copyWithStreamInfo(Uri.EMPTY, MimeTypes.APPLICATION_MPD)
116+
val dashMediaItem = mediaItem.copyWithStreamInfo(Uri.EMPTY, MimeTypes.APPLICATION_MPD, sponsorblockUrl)
115117
dashMediaSourceFactory.createMediaSource(manifest, dashMediaItem)
116118
}
117119
dashUrl != null -> {
118120
val dashMediaSourceFactory = DashMediaSource.Factory(dataSourceFactory)
119-
val dashMediaItem = mediaItem.copyWithStreamInfo(dashUrl.toUri(), MimeTypes.APPLICATION_MPD)
121+
val dashMediaItem = mediaItem.copyWithStreamInfo(dashUrl.toUri(), MimeTypes.APPLICATION_MPD, sponsorblockUrl)
120122
dashMediaSourceFactory.createMediaSource(dashMediaItem)
121123
}
122124
hlsUrl != null -> {
123125
val hlsMediaSourceFactory = HlsMediaSource.Factory(dataSourceFactory)
124-
val hlsMediaItem = mediaItem.copyWithStreamInfo(hlsUrl.toUri(), MimeTypes.APPLICATION_M3U8)
126+
val hlsMediaItem = mediaItem.copyWithStreamInfo(hlsUrl.toUri(), MimeTypes.APPLICATION_M3U8, sponsorblockUrl)
125127
hlsMediaSourceFactory.createMediaSource(hlsMediaItem)
126128
}
127129
else -> error("Either dashManifest, dashUrl, or hlsUrl must be provided")
128130
}
129131
}
130132

131-
private fun MediaItem.copyWithStreamInfo(uri: Uri, mimeType: String): MediaItem {
133+
private fun MediaItem.copyWithStreamInfo(uri: Uri, mimeType: String, sponsorblockUrl: String?): MediaItem {
132134
val extras = Bundle().apply {
133135
putString("KEY_SERVICE_ID", mediaMetadata.extras!!.getString("KEY_SERVICE_ID"))
136+
putString("KEY_SPONSORBLOCK_URL", sponsorblockUrl)
134137
}
135138
return MediaItem.Builder()
136139
.setUri(uri)
@@ -205,7 +208,8 @@ class LazyUrlMediaSource(
205208
streamInfo.dashManifest,
206209
streamInfo.dashUrl,
207210
streamInfo.hlsUrl,
208-
streamInfo.headers
211+
streamInfo.headers,
212+
streamInfo.sponsorblockUrl
209213
)
210214
withContext(Dispatchers.Main) {
211215
eventListeners.forEach { (listener, handler) ->
@@ -281,6 +285,7 @@ fun StreamInfo.toMediaItem(): MediaItem {
281285
putString("KEY_SERVICE_ID", serviceId)
282286
putSerializable("KEY_HEADER_MAP", headers)
283287
putBoolean("KEY_USE_CACHE", streamType != StreamType.LIVE_STREAM)
288+
putString("KEY_SPONSORBLOCK_URL", sponsorblockUrl)
284289
}
285290
return MediaItem.Builder()
286291
.setUri("placeholder://stream")

android/src/main/kotlin/project/pipepipe/app/service/PlaybackService.kt

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,21 @@ import androidx.media3.exoplayer.ExoPlayer
1515
import androidx.media3.session.*
1616
import com.google.common.util.concurrent.Futures
1717
import 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
2021
import project.pipepipe.app.mediasource.CustomMediaSourceFactory
2122
import project.pipepipe.app.mediasource.toMediaItem
2223
import project.pipepipe.app.PlaybackMode
2324
import project.pipepipe.app.SharedContext
2425
import project.pipepipe.app.SharedContext.playbackMode
2526
import project.pipepipe.app.database.DatabaseOperations
2627
import project.pipepipe.app.helper.ToastManager
28+
import project.pipepipe.app.helper.executeJobFlow
2729
import 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
2833
import project.pipepipe.app.uistate.VideoDetailPageState
2934
import java.util.concurrent.ExecutorService
3035
import 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) {

android/src/main/kotlin/project/pipepipe/app/ui/component/player/VideoPlayer.kt

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -364,22 +364,8 @@ fun VideoPlayer(
364364
position.toDouble() in segment.startTime..segment.endTime
365365
}
366366
if (currentSegment != null && !skippedSegments.contains(currentSegment.uuid)) {
367-
if (SponsorBlockHelper.shouldSkipSegment(currentSegment)) {
368-
val skipToMs = (currentSegment.endTime).toLong()
369-
mediaController.seekTo(skipToMs)
370-
skippedSegments = skippedSegments + currentSegment.uuid
371-
lastSkippedSegment = currentSegment
372-
373-
// 不显示 unskip 按钮(因为是自动跳过)
374-
375-
// Show notification
376-
if (SponsorBlockHelper.isNotificationsEnabled()) {
377-
val categoryName = segmentDisplayNames[currentSegment.uuid] ?: ""
378-
ToastManager.show(playerSkippedText.replace("%s", categoryName))
379-
}
380-
}
381-
// Show manual skip button
382-
else if (SponsorBlockHelper.shouldShowSkipButton(currentSegment)) {
367+
// Only show manual skip button (automatic skipping is handled by PlaybackService)
368+
if (SponsorBlockHelper.shouldShowSkipButton(currentSegment)) {
383369
currentSegmentToSkip = currentSegment
384370
showSkipButton = true
385371
} else {

0 commit comments

Comments
 (0)