33import android .util .Log ;
44
55import com .google .android .exoplayer2 .MediaItem ;
6+ import com .google .android .exoplayer2 .PlaybackException ;
67import com .google .android .exoplayer2 .Timeline ;
7- import com .google .android .exoplayer2 .source .CompositeMediaSource ;
8+ import com .google .android .exoplayer2 .source .BaseMediaSource ;
89import com .google .android .exoplayer2 .source .MediaPeriod ;
9- import com .google .android .exoplayer2 .source .MediaSource ;
1010import com .google .android .exoplayer2 .source .SilenceMediaSource ;
11+ import com .google .android .exoplayer2 .source .SinglePeriodTimeline ;
1112import com .google .android .exoplayer2 .upstream .Allocator ;
1213import com .google .android .exoplayer2 .upstream .TransferListener ;
1314
1415import org .schabi .newpipe .player .mediaitem .ExceptionTag ;
15- import org .schabi .newpipe .player .mediaitem .MediaItemTag ;
1616import org .schabi .newpipe .player .playqueue .PlayQueueItem ;
1717
1818import java .io .IOException ;
2222import androidx .annotation .NonNull ;
2323import androidx .annotation .Nullable ;
2424
25- public class FailedMediaSource extends CompositeMediaSource < Void > implements ManagedMediaSource {
25+ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSource {
2626 /**
2727 * Play 2 seconds of silenced audio when a stream fails to resolve due to a known issue,
2828 * such as {@link org.schabi.newpipe.extractor.exceptions.ExtractionException}.
@@ -32,12 +32,12 @@ public class FailedMediaSource extends CompositeMediaSource<Void> implements Man
3232 * not recommended, it may cause ExoPlayer to buffer for a while.
3333 * */
3434 public static final long SILENCE_DURATION_US = TimeUnit .SECONDS .toMicros (2 );
35+ public static final MediaPeriod SILENT_MEDIA = makeSilentMediaPeriod (SILENCE_DURATION_US );
3536
3637 private final String TAG = "FailedMediaSource@" + Integer .toHexString (hashCode ());
3738 private final PlayQueueItem playQueueItem ;
3839 private final Throwable error ;
3940 private final long retryTimestamp ;
40- private final MediaSource source ;
4141 private final MediaItem mediaItem ;
4242 /**
4343 * Fail the play queue item associated with this source, with potential future retries.
@@ -56,15 +56,10 @@ public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem,
5656 this .playQueueItem = playQueueItem ;
5757 this .error = error ;
5858 this .retryTimestamp = retryTimestamp ;
59-
60- final MediaItemTag tag = ExceptionTag
59+ this .mediaItem = ExceptionTag
6160 .of (playQueueItem , Collections .singletonList (error ))
62- .withExtras (this );
63- this .mediaItem = tag .asMediaItem ();
64- this .source = new SilenceMediaSource .Factory ()
65- .setDurationUs (SILENCE_DURATION_US )
66- .setTag (tag )
67- .createMediaSource ();
61+ .withExtras (this )
62+ .asMediaItem ();
6863 }
6964
7065 public static FailedMediaSource of (@ NonNull final PlayQueueItem playQueueItem ,
@@ -91,49 +86,77 @@ private boolean canRetry() {
9186 return System .currentTimeMillis () >= retryTimestamp ;
9287 }
9388
94- /**
95- * Returns the {@link MediaItem} whose media is provided by the source.
96- */
9789 @ Override
9890 public MediaItem getMediaItem () {
9991 return mediaItem ;
10092 }
10193
94+ /**
95+ * Prepares the source with {@link Timeline} info on the silence playback when the error
96+ * is classed as {@link FailedMediaSourceException}, for example, when the error is
97+ * {@link org.schabi.newpipe.extractor.exceptions.ExtractionException ExtractionException}.
98+ * These types of error are swallowed by {@link FailedMediaSource}, and the underlying
99+ * exception is carried to the {@link MediaItem} metadata during playback.
100+ * <br><br>
101+ * If the exception is not known, e.g. {@link java.net.UnknownHostException} or some
102+ * other network issue, then no source info is refreshed and
103+ * {@link #maybeThrowSourceInfoRefreshError()} be will triggered.
104+ * <br><br>
105+ * Note that this method is called only once until {@link #releaseSourceInternal()} is called,
106+ * so if no action is done in here, playback will stall unless
107+ * {@link #maybeThrowSourceInfoRefreshError()} is called.
108+ *
109+ * @param mediaTransferListener No data transfer listener needed, ignored here.
110+ */
102111 @ Override
103112 protected void prepareSourceInternal (@ Nullable final TransferListener mediaTransferListener ) {
104- super .prepareSourceInternal (mediaTransferListener );
105113 Log .e (TAG , "Loading failed source: " , error );
106114 if (error instanceof FailedMediaSourceException ) {
107- prepareChildSource ( null , source );
115+ refreshSourceInfo ( makeSilentMediaTimeline ( SILENCE_DURATION_US , mediaItem ) );
108116 }
109117 }
110118
111-
119+ /**
120+ * If the error is not known, e.g. network issue, then the exception is not swallowed here in
121+ * {@link FailedMediaSource}. The exception is then propagated to the player, which
122+ * {@link org.schabi.newpipe.player.Player Player} can react to inside
123+ * {@link com.google.android.exoplayer2.Player.Listener#onPlayerError(PlaybackException)}.
124+ *
125+ * @throws IOException An error which will always result in
126+ * {@link com.google.android.exoplayer2.PlaybackException#ERROR_CODE_IO_UNSPECIFIED}.
127+ */
112128 @ Override
113129 public void maybeThrowSourceInfoRefreshError () throws IOException {
114130 if (!(error instanceof FailedMediaSourceException )) {
115131 throw new IOException (error );
116132 }
117- super .maybeThrowSourceInfoRefreshError ();
118133 }
119134
135+ /**
136+ * This method is only called if {@link #prepareSourceInternal(TransferListener)}
137+ * refreshes the source info with no exception. All parameters are ignored as this
138+ * returns a static and reused piece of silent audio.
139+ *
140+ * @param id The identifier of the period.
141+ * @param allocator An {@link Allocator} from which to obtain media buffer allocations.
142+ * @param startPositionUs The expected start position, in microseconds.
143+ * @return The common {@link MediaPeriod} holding the silence.
144+ */
120145 @ Override
121- protected void onChildSourceInfoRefreshed (final Void id ,
122- final MediaSource mediaSource ,
123- final Timeline timeline ) {
124- refreshSourceInfo ( timeline ) ;
146+ public MediaPeriod createPeriod (final MediaPeriodId id ,
147+ final Allocator allocator ,
148+ final long startPositionUs ) {
149+ return SILENT_MEDIA ;
125150 }
126151
127-
128152 @ Override
129- public MediaPeriod createPeriod (final MediaPeriodId id , final Allocator allocator ,
130- final long startPositionUs ) {
131- return source .createPeriod (id , allocator , startPositionUs );
153+ public void releasePeriod (final MediaPeriod mediaPeriod ) {
154+ /* Do Nothing (we want to keep re-using the Silent MediaPeriod) */
132155 }
133156
134157 @ Override
135- public void releasePeriod ( final MediaPeriod mediaPeriod ) {
136- source . releasePeriod ( mediaPeriod );
158+ protected void releaseSourceInternal ( ) {
159+ /* Do Nothing, no clean-up for processing/extra thread is needed by this MediaSource */
137160 }
138161
139162 @ Override
@@ -168,4 +191,22 @@ public StreamInfoLoadException(final Throwable cause) {
168191 super (cause );
169192 }
170193 }
194+
195+ private static Timeline makeSilentMediaTimeline (final long durationUs ,
196+ @ NonNull final MediaItem mediaItem ) {
197+ return new SinglePeriodTimeline (
198+ durationUs ,
199+ /* isSeekable= */ true ,
200+ /* isDynamic= */ false ,
201+ /* useLiveConfiguration= */ false ,
202+ /* manifest= */ null ,
203+ mediaItem );
204+ }
205+
206+ private static MediaPeriod makeSilentMediaPeriod (final long durationUs ) {
207+ return new SilenceMediaSource .Factory ()
208+ .setDurationUs (durationUs )
209+ .createMediaSource ()
210+ .createPeriod (null , null , 0 );
211+ }
171212}
0 commit comments