3
3
import android .util .Log ;
4
4
5
5
import com .google .android .exoplayer2 .MediaItem ;
6
+ import com .google .android .exoplayer2 .PlaybackException ;
6
7
import com .google .android .exoplayer2 .Timeline ;
7
- import com .google .android .exoplayer2 .source .CompositeMediaSource ;
8
+ import com .google .android .exoplayer2 .source .BaseMediaSource ;
8
9
import com .google .android .exoplayer2 .source .MediaPeriod ;
9
- import com .google .android .exoplayer2 .source .MediaSource ;
10
10
import com .google .android .exoplayer2 .source .SilenceMediaSource ;
11
+ import com .google .android .exoplayer2 .source .SinglePeriodTimeline ;
11
12
import com .google .android .exoplayer2 .upstream .Allocator ;
12
13
import com .google .android .exoplayer2 .upstream .TransferListener ;
13
14
14
15
import org .schabi .newpipe .player .mediaitem .ExceptionTag ;
15
- import org .schabi .newpipe .player .mediaitem .MediaItemTag ;
16
16
import org .schabi .newpipe .player .playqueue .PlayQueueItem ;
17
17
18
18
import java .io .IOException ;
22
22
import androidx .annotation .NonNull ;
23
23
import androidx .annotation .Nullable ;
24
24
25
- public class FailedMediaSource extends CompositeMediaSource < Void > implements ManagedMediaSource {
25
+ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSource {
26
26
/**
27
27
* Play 2 seconds of silenced audio when a stream fails to resolve due to a known issue,
28
28
* such as {@link org.schabi.newpipe.extractor.exceptions.ExtractionException}.
@@ -32,12 +32,12 @@ public class FailedMediaSource extends CompositeMediaSource<Void> implements Man
32
32
* not recommended, it may cause ExoPlayer to buffer for a while.
33
33
* */
34
34
public static final long SILENCE_DURATION_US = TimeUnit .SECONDS .toMicros (2 );
35
+ public static final MediaPeriod SILENT_MEDIA = makeSilentMediaPeriod (SILENCE_DURATION_US );
35
36
36
37
private final String TAG = "FailedMediaSource@" + Integer .toHexString (hashCode ());
37
38
private final PlayQueueItem playQueueItem ;
38
39
private final Throwable error ;
39
40
private final long retryTimestamp ;
40
- private final MediaSource source ;
41
41
private final MediaItem mediaItem ;
42
42
/**
43
43
* Fail the play queue item associated with this source, with potential future retries.
@@ -56,15 +56,10 @@ public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem,
56
56
this .playQueueItem = playQueueItem ;
57
57
this .error = error ;
58
58
this .retryTimestamp = retryTimestamp ;
59
-
60
- final MediaItemTag tag = ExceptionTag
59
+ this .mediaItem = ExceptionTag
61
60
.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 ();
68
63
}
69
64
70
65
public static FailedMediaSource of (@ NonNull final PlayQueueItem playQueueItem ,
@@ -91,49 +86,78 @@ private boolean canRetry() {
91
86
return System .currentTimeMillis () >= retryTimestamp ;
92
87
}
93
88
94
- /**
95
- * Returns the {@link MediaItem} whose media is provided by the source.
96
- */
97
89
@ Override
98
90
public MediaItem getMediaItem () {
99
91
return mediaItem ;
100
92
}
101
93
94
+ /**
95
+ * Prepare the source with info on the silence playback {@link Timeline} when the error
96
+ * is known as {@link FailedMediaSourceException}. For exemple, the error is Extractor-related,
97
+ * e.g. {@link org.schabi.newpipe.extractor.exceptions.ExtractionException ExtractionException}.
98
+ * These types of error is swallowed by this
99
+ * {@link com.google.android.exoplayer2.source.MediaSource MediaSource},
100
+ * and the exceptions are carried to the {@link MediaItem} metadata.
101
+ * <br><br>
102
+ * If the exception is not known, e.g. {@link java.net.UnknownHostException} or some
103
+ * other network issue, then no source info is refreshed and
104
+ * {@link #maybeThrowSourceInfoRefreshError()} be will triggered.
105
+ * <br><br>
106
+ * Note that the method is called only once until {@link #releaseSourceInternal()} is called,
107
+ * so if no action is done in this method, playback will stall unless
108
+ * {@link #maybeThrowSourceInfoRefreshError()} is called.
109
+ *
110
+ * @param mediaTransferListener No data transfer listener needed, ignored here.
111
+ */
102
112
@ Override
103
113
protected void prepareSourceInternal (@ Nullable final TransferListener mediaTransferListener ) {
104
- super .prepareSourceInternal (mediaTransferListener );
105
114
Log .e (TAG , "Loading failed source: " , error );
106
115
if (error instanceof FailedMediaSourceException ) {
107
- prepareChildSource ( null , source );
116
+ refreshSourceInfo ( makeSilentMediaTimeline ( SILENCE_DURATION_US , mediaItem ) );
108
117
}
109
118
}
110
119
111
-
120
+ /**
121
+ * If the error is not known, e.g. network issue, then the exception is not swallowed in this
122
+ * {@link com.google.android.exoplayer2.source.MediaSource MediaSource}. This error is then
123
+ * propagated to the player, which {@link org.schabi.newpipe.player.Player Player} can react to
124
+ * it in {@link com.google.android.exoplayer2.Player.Listener#onPlayerError(PlaybackException)}.
125
+ *
126
+ * @throws IOException An error which will always result in
127
+ * {@link com.google.android.exoplayer2.PlaybackException#ERROR_CODE_IO_UNSPECIFIED}.
128
+ */
112
129
@ Override
113
130
public void maybeThrowSourceInfoRefreshError () throws IOException {
114
131
if (!(error instanceof FailedMediaSourceException )) {
115
132
throw new IOException (error );
116
133
}
117
- super .maybeThrowSourceInfoRefreshError ();
118
134
}
119
135
136
+ /**
137
+ * This method is only called if {@link #prepareSourceInternal(TransferListener)}
138
+ * refreshes the source info with no exception. All parameters are ignored as this
139
+ * returns a static piece of silence media.
140
+ *
141
+ * @param id The identifier of the period.
142
+ * @param allocator An {@link Allocator} from which to obtain media buffer allocations.
143
+ * @param startPositionUs The expected start position, in microseconds.
144
+ * @return The common {@link MediaPeriod} holding the silence.
145
+ */
120
146
@ Override
121
- protected void onChildSourceInfoRefreshed (final Void id ,
122
- final MediaSource mediaSource ,
123
- final Timeline timeline ) {
124
- refreshSourceInfo ( timeline ) ;
147
+ public MediaPeriod createPeriod (final MediaPeriodId id ,
148
+ final Allocator allocator ,
149
+ final long startPositionUs ) {
150
+ return SILENT_MEDIA ;
125
151
}
126
152
127
-
128
153
@ Override
129
- public MediaPeriod createPeriod (final MediaPeriodId id , final Allocator allocator ,
130
- final long startPositionUs ) {
131
- return source .createPeriod (id , allocator , startPositionUs );
154
+ public void releasePeriod (final MediaPeriod mediaPeriod ) {
155
+ /* Do Nothing (we want to keep re-using the Silent MediaPeriod) */
132
156
}
133
157
134
158
@ Override
135
- public void releasePeriod ( final MediaPeriod mediaPeriod ) {
136
- source . releasePeriod ( mediaPeriod );
159
+ protected void releaseSourceInternal ( ) {
160
+ /* Do Nothing, no clean-up for processing/extra thread is needed by this MediaSource */
137
161
}
138
162
139
163
@ Override
@@ -168,4 +192,22 @@ public StreamInfoLoadException(final Throwable cause) {
168
192
super (cause );
169
193
}
170
194
}
195
+
196
+ private static Timeline makeSilentMediaTimeline (final long durationUs ,
197
+ @ NonNull final MediaItem mediaItem ) {
198
+ return new SinglePeriodTimeline (
199
+ durationUs ,
200
+ /* isSeekable= */ true ,
201
+ /* isDynamic= */ false ,
202
+ /* useLiveConfiguration= */ false ,
203
+ /* manifest= */ null ,
204
+ mediaItem );
205
+ }
206
+
207
+ private static MediaPeriod makeSilentMediaPeriod (final long durationUs ) {
208
+ return new SilenceMediaSource .Factory ()
209
+ .setDurationUs (durationUs )
210
+ .createMediaSource ()
211
+ .createPeriod (null , null , 0 );
212
+ }
171
213
}
0 commit comments