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,77 @@ 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
+ * 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
+ */
102
111
@ Override
103
112
protected void prepareSourceInternal (@ Nullable final TransferListener mediaTransferListener ) {
104
- super .prepareSourceInternal (mediaTransferListener );
105
113
Log .e (TAG , "Loading failed source: " , error );
106
114
if (error instanceof FailedMediaSourceException ) {
107
- prepareChildSource ( null , source );
115
+ refreshSourceInfo ( makeSilentMediaTimeline ( SILENCE_DURATION_US , mediaItem ) );
108
116
}
109
117
}
110
118
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
+ */
112
128
@ Override
113
129
public void maybeThrowSourceInfoRefreshError () throws IOException {
114
130
if (!(error instanceof FailedMediaSourceException )) {
115
131
throw new IOException (error );
116
132
}
117
- super .maybeThrowSourceInfoRefreshError ();
118
133
}
119
134
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
+ */
120
145
@ 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 ;
125
150
}
126
151
127
-
128
152
@ 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) */
132
155
}
133
156
134
157
@ 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 */
137
160
}
138
161
139
162
@ Override
@@ -168,4 +191,22 @@ public StreamInfoLoadException(final Throwable cause) {
168
191
super (cause );
169
192
}
170
193
}
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
+ }
171
212
}
0 commit comments