5858import android .support .v4 .media .session .MediaSessionCompat ;
5959import android .util .Log ;
6060import android .view .LayoutInflater ;
61+ import android .widget .Toast ;
6162
6263import androidx .annotation .NonNull ;
6364import androidx .annotation .Nullable ;
8990import org .schabi .newpipe .error .ErrorUtil ;
9091import org .schabi .newpipe .error .UserAction ;
9192import org .schabi .newpipe .extractor .Image ;
93+ import org .schabi .newpipe .extractor .sponsorblock .SponsorBlockAction ;
94+ import org .schabi .newpipe .extractor .sponsorblock .SponsorBlockCategory ;
95+ import org .schabi .newpipe .extractor .sponsorblock .SponsorBlockSegment ;
9296import org .schabi .newpipe .extractor .stream .AudioStream ;
9397import org .schabi .newpipe .extractor .stream .StreamInfo ;
9498import org .schabi .newpipe .extractor .stream .StreamType ;
127131import org .schabi .newpipe .util .StreamTypeUtil ;
128132import org .schabi .newpipe .util .image .CoilHelper ;
129133
134+ import java .util .Collections ;
135+ import java .util .HashSet ;
130136import java .util .List ;
131137import java .util .Objects ;
132138import java .util .Optional ;
139+ import java .util .Set ;
133140import java .util .stream .IntStream ;
134141
135142import coil3 .target .Target ;
@@ -202,6 +209,14 @@ public final class Player implements PlaybackListener, Listener {
202209 @ Nullable
203210 private coil3 .request .Disposable thumbnailDisposable ;
204211
212+ @ NonNull
213+ private List <SponsorBlockSegment > sponsorBlockSegments = Collections .emptyList ();
214+ @ NonNull
215+ private final Set <String > skippedSponsorBlockSegments = new HashSet <>();
216+ @ Nullable
217+ private String ignoredSponsorBlockSegment ;
218+ private boolean sponsorBlockSkipInProgress = false ;
219+
205220 /*//////////////////////////////////////////////////////////////////////////
206221 // Player
207222 //////////////////////////////////////////////////////////////////////////*/
@@ -1025,10 +1040,161 @@ public void triggerProgressUpdate() {
10251040 return ;
10261041 }
10271042
1043+ maybeSkipSponsorBlockSegment ();
10281044 onUpdateProgress (Math .max ((int ) simpleExoPlayer .getCurrentPosition (), 0 ),
10291045 (int ) simpleExoPlayer .getDuration (), simpleExoPlayer .getBufferedPercentage ());
10301046 }
10311047
1048+ private void maybeSkipSponsorBlockSegment () {
1049+ if (!isSponsorBlockEnabled () || sponsorBlockSegments .isEmpty () || !isPlaying ()
1050+ || simpleExoPlayer .getPlaybackState ()
1051+ != com .google .android .exoplayer2 .Player .STATE_READY ) {
1052+ return ;
1053+ }
1054+
1055+ final long currentPositionMillis = simpleExoPlayer .getCurrentPosition ();
1056+ SponsorBlockSegment activeSegment = null ;
1057+ for (final SponsorBlockSegment segment : sponsorBlockSegments ) {
1058+ if (isSegmentActive (segment , currentPositionMillis )) {
1059+ activeSegment = segment ;
1060+ break ;
1061+ }
1062+ }
1063+
1064+ if (activeSegment == null ) {
1065+ ignoredSponsorBlockSegment = null ;
1066+ return ;
1067+ }
1068+
1069+ final String segmentKey = getSegmentKey (activeSegment );
1070+ if (segmentKey .equals (ignoredSponsorBlockSegment )
1071+ || skippedSponsorBlockSegments .contains (segmentKey )
1072+ || activeSegment .getActionType () != SponsorBlockAction .SKIP
1073+ || activeSegment .getCategory () == SponsorBlockCategory .HIGHLIGHT ) {
1074+ return ;
1075+ }
1076+
1077+ final long segmentEndMillis = Math .round (activeSegment .getEndTimeSeconds () * 1000.0d );
1078+ final long durationMillis = simpleExoPlayer .getDuration ();
1079+ final long targetPositionMillis = durationMillis > 0 && durationMillis != C .TIME_UNSET
1080+ ? MathUtils .clamp (segmentEndMillis , 0 , durationMillis )
1081+ : Math .max (segmentEndMillis , 0 );
1082+ if (targetPositionMillis <= currentPositionMillis ) {
1083+ skippedSponsorBlockSegments .add (segmentKey );
1084+ return ;
1085+ }
1086+
1087+ sponsorBlockSkipInProgress = true ;
1088+ skippedSponsorBlockSegments .add (segmentKey );
1089+ simpleExoPlayer .seekTo (targetPositionMillis );
1090+ if (prefs .getBoolean (context .getString (R .string .sponsor_block_notifications_key ), true )) {
1091+ Toast .makeText (context ,
1092+ context .getString (R .string .sponsor_block_skipped_segment ,
1093+ getSegmentCategoryName (activeSegment )),
1094+ Toast .LENGTH_SHORT ).show ();
1095+ }
1096+ }
1097+
1098+ private boolean isSponsorBlockEnabled () {
1099+ return prefs .getBoolean (context .getString (R .string .sponsor_block_enable_key ), false );
1100+ }
1101+
1102+ private boolean isSegmentActive (@ NonNull final SponsorBlockSegment segment ,
1103+ final long currentPositionMillis ) {
1104+ if (!segment .isValid () || segment .getActionType () == SponsorBlockAction .UNKNOWN
1105+ || segment .getActionType () == SponsorBlockAction .POI
1106+ || segment .getActionType () == SponsorBlockAction .CHAPTER
1107+ || segment .getActionType () == SponsorBlockAction .FULL
1108+ || segment .getActionType () == SponsorBlockAction .MUTE
1109+ || !isCategoryEnabled (segment .getCategory ())) {
1110+ return false ;
1111+ }
1112+ final long segmentStartMillis = Math .round (segment .getStartTimeSeconds () * 1000.0d );
1113+ final long segmentEndMillis = Math .round (segment .getEndTimeSeconds () * 1000.0d );
1114+ return currentPositionMillis >= segmentStartMillis
1115+ && currentPositionMillis < segmentEndMillis ;
1116+ }
1117+
1118+ private boolean isCategoryEnabled (@ Nullable final SponsorBlockCategory category ) {
1119+ if (category == null ) {
1120+ return false ;
1121+ }
1122+ switch (category ) {
1123+ case SPONSOR :
1124+ return prefs .getBoolean (
1125+ context .getString (R .string .sponsor_block_category_sponsor_key ), true );
1126+ case INTRO :
1127+ return prefs .getBoolean (
1128+ context .getString (R .string .sponsor_block_category_intro_key ), false );
1129+ case OUTRO :
1130+ return prefs .getBoolean (
1131+ context .getString (R .string .sponsor_block_category_outro_key ), false );
1132+ case INTERACTION :
1133+ return prefs .getBoolean (
1134+ context .getString (R .string .sponsor_block_category_interaction_key ),
1135+ false );
1136+ case HIGHLIGHT :
1137+ return prefs .getBoolean (
1138+ context .getString (R .string .sponsor_block_category_highlight_key ),
1139+ false );
1140+ case SELF_PROMO :
1141+ return prefs .getBoolean (
1142+ context .getString (R .string .sponsor_block_category_self_promo_key ),
1143+ false );
1144+ case MUSIC_OFFTOPIC :
1145+ return prefs .getBoolean (
1146+ context .getString (R .string .sponsor_block_category_non_music_key ),
1147+ false );
1148+ case PREVIEW :
1149+ return prefs .getBoolean (
1150+ context .getString (R .string .sponsor_block_category_preview_key ), false );
1151+ case FILLER :
1152+ return prefs .getBoolean (
1153+ context .getString (R .string .sponsor_block_category_filler_key ), false );
1154+ default :
1155+ return false ;
1156+ }
1157+ }
1158+
1159+ @ NonNull
1160+ private String getSegmentKey (@ NonNull final SponsorBlockSegment segment ) {
1161+ if (!isNullOrEmpty (segment .getUuid ())) {
1162+ return segment .getUuid ();
1163+ }
1164+ return segment .getCategory () + ":" + segment .getActionType () + ":"
1165+ + segment .getStartTimeSeconds () + ":" + segment .getEndTimeSeconds ();
1166+ }
1167+
1168+ @ NonNull
1169+ private String getSegmentCategoryName (@ NonNull final SponsorBlockSegment segment ) {
1170+ final SponsorBlockCategory category = segment .getCategory ();
1171+ if (category == null ) {
1172+ return context .getString (R .string .sponsor_block_skipped_segment_fallback );
1173+ }
1174+ switch (category ) {
1175+ case SPONSOR :
1176+ return context .getString (R .string .sponsor_block_category_sponsor_title );
1177+ case INTRO :
1178+ return context .getString (R .string .sponsor_block_category_intro_title );
1179+ case OUTRO :
1180+ return context .getString (R .string .sponsor_block_category_outro_title );
1181+ case INTERACTION :
1182+ return context .getString (R .string .sponsor_block_category_interaction_title );
1183+ case HIGHLIGHT :
1184+ return context .getString (R .string .sponsor_block_category_highlight_title );
1185+ case SELF_PROMO :
1186+ return context .getString (R .string .sponsor_block_category_self_promo_title );
1187+ case MUSIC_OFFTOPIC :
1188+ return context .getString (R .string .sponsor_block_category_non_music_title );
1189+ case PREVIEW :
1190+ return context .getString (R .string .sponsor_block_category_preview_title );
1191+ case FILLER :
1192+ return context .getString (R .string .sponsor_block_category_filler_title );
1193+ default :
1194+ return context .getString (R .string .sponsor_block_skipped_segment_fallback );
1195+ }
1196+ }
1197+
10321198 private Disposable getProgressUpdateDisposable () {
10331199 return Observable .interval (PROGRESS_LOOP_INTERVAL_MILLIS , MILLISECONDS ,
10341200 AndroidSchedulers .mainThread ())
@@ -1444,6 +1610,20 @@ public void onPositionDiscontinuity(@NonNull final PositionInfo oldPosition,
14441610 return ;
14451611 }
14461612
1613+ if (discontinuityReason == DISCONTINUITY_REASON_SEEK && !sponsorBlockSkipInProgress ) {
1614+ final String seekTargetSegmentKey = getActiveSponsorBlockSegmentKey (
1615+ newPosition .positionMs );
1616+ if (prefs .getBoolean (context .getString (R .string .sponsor_block_graced_rewind_key ),
1617+ true )) {
1618+ ignoredSponsorBlockSegment = seekTargetSegmentKey ;
1619+ } else if (seekTargetSegmentKey != null ) {
1620+ skippedSponsorBlockSegments .remove (seekTargetSegmentKey );
1621+ }
1622+ }
1623+ if (sponsorBlockSkipInProgress ) {
1624+ sponsorBlockSkipInProgress = false ;
1625+ }
1626+
14471627 // Refresh the playback if there is a transition to the next video
14481628 final int newIndex = newPosition .mediaItemIndex ;
14491629 switch (discontinuityReason ) {
@@ -1919,6 +2099,7 @@ private void updateMetadataWith(@NonNull final StreamInfo info) {
19192099 return ;
19202100 }
19212101
2102+ updateSponsorBlockSegments (info );
19222103 maybeAutoQueueNextStream (info );
19232104
19242105 loadCurrentThumbnail (info .getThumbnails ());
@@ -1929,6 +2110,31 @@ private void updateMetadataWith(@NonNull final StreamInfo info) {
19292110 UIs .call (playerUi -> playerUi .onMetadataChanged (info ));
19302111 }
19312112
2113+ private void updateSponsorBlockSegments (@ NonNull final StreamInfo info ) {
2114+ skippedSponsorBlockSegments .clear ();
2115+ ignoredSponsorBlockSegment = null ;
2116+ sponsorBlockSkipInProgress = false ;
2117+ if (!isSponsorBlockEnabled ()) {
2118+ sponsorBlockSegments = Collections .emptyList ();
2119+ return ;
2120+ }
2121+ final List <SponsorBlockSegment > segments = info .getSponsorBlockSegments ();
2122+ sponsorBlockSegments = segments == null ? Collections .emptyList () : segments ;
2123+ }
2124+
2125+ @ Nullable
2126+ private String getActiveSponsorBlockSegmentKey (final long positionMillis ) {
2127+ if (!isSponsorBlockEnabled () || sponsorBlockSegments .isEmpty ()) {
2128+ return null ;
2129+ }
2130+ for (final SponsorBlockSegment segment : sponsorBlockSegments ) {
2131+ if (isSegmentActive (segment , positionMillis )) {
2132+ return getSegmentKey (segment );
2133+ }
2134+ }
2135+ return null ;
2136+ }
2137+
19322138 @ NonNull
19332139 public String getVideoUrl () {
19342140 return currentMetadata == null
0 commit comments