|
112 | 112 | import com.google.android.exoplayer2.C; |
113 | 113 | import com.google.android.exoplayer2.DefaultRenderersFactory; |
114 | 114 | import com.google.android.exoplayer2.ExoPlaybackException; |
| 115 | +import com.google.android.exoplayer2.MediaItem; |
115 | 116 | import com.google.android.exoplayer2.PlaybackParameters; |
116 | 117 | import com.google.android.exoplayer2.Player.PositionInfo; |
117 | 118 | import com.google.android.exoplayer2.RenderersFactory; |
|
122 | 123 | import com.google.android.exoplayer2.source.TrackGroup; |
123 | 124 | import com.google.android.exoplayer2.source.TrackGroupArray; |
124 | 125 | import com.google.android.exoplayer2.text.Cue; |
| 126 | +import com.google.android.exoplayer2.trackselection.MappingTrackSelector; |
125 | 127 | import com.google.android.exoplayer2.trackselection.TrackSelectionArray; |
126 | 128 | import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; |
127 | 129 | import com.google.android.exoplayer2.ui.CaptionStyleCompat; |
|
144 | 146 | import org.schabi.newpipe.extractor.MediaFormat; |
145 | 147 | import org.schabi.newpipe.extractor.stream.StreamInfo; |
146 | 148 | import org.schabi.newpipe.extractor.stream.StreamSegment; |
| 149 | +import org.schabi.newpipe.extractor.stream.StreamType; |
147 | 150 | import org.schabi.newpipe.extractor.stream.VideoStream; |
148 | 151 | import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; |
149 | 152 | import org.schabi.newpipe.fragments.detail.VideoDetailFragment; |
|
175 | 178 | import org.schabi.newpipe.player.resolver.AudioPlaybackResolver; |
176 | 179 | import org.schabi.newpipe.player.resolver.MediaSourceTag; |
177 | 180 | import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; |
| 181 | +import org.schabi.newpipe.player.resolver.VideoPlaybackResolver.SourceType; |
178 | 182 | import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper; |
179 | 183 | import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder; |
180 | 184 | import org.schabi.newpipe.util.DeviceUtils; |
|
193 | 197 | import java.util.List; |
194 | 198 | import java.util.Objects; |
195 | 199 | import java.util.Optional; |
| 200 | +import java.util.stream.IntStream; |
196 | 201 |
|
197 | 202 | import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; |
198 | 203 | import io.reactivex.rxjava3.core.Observable; |
@@ -2449,9 +2454,9 @@ public void onPlaybackParametersChanged(@NonNull final PlaybackParameters playba |
2449 | 2454 | } |
2450 | 2455 |
|
2451 | 2456 | @Override |
2452 | | - public void onPositionDiscontinuity( |
2453 | | - final PositionInfo oldPosition, final PositionInfo newPosition, |
2454 | | - @DiscontinuityReason final int discontinuityReason) { |
| 2457 | + public void onPositionDiscontinuity(@NonNull final PositionInfo oldPosition, |
| 2458 | + @NonNull final PositionInfo newPosition, |
| 2459 | + @DiscontinuityReason final int discontinuityReason) { |
2455 | 2460 | if (DEBUG) { |
2456 | 2461 | Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with " |
2457 | 2462 | + "discontinuityReason = [" + discontinuityReason + "]"); |
@@ -2499,7 +2504,7 @@ public void onRenderedFirstFrame() { |
2499 | 2504 | } |
2500 | 2505 |
|
2501 | 2506 | @Override |
2502 | | - public void onCues(final List<Cue> cues) { |
| 2507 | + public void onCues(@NonNull final List<Cue> cues) { |
2503 | 2508 | binding.subtitleView.onCues(cues); |
2504 | 2509 | } |
2505 | 2510 | //endregion |
@@ -3005,18 +3010,19 @@ private void maybeUpdateCurrentMetadata() { |
3005 | 3010 |
|
3006 | 3011 | final MediaSourceTag metadata; |
3007 | 3012 | try { |
3008 | | - metadata = (MediaSourceTag) simpleExoPlayer.getCurrentTag(); |
3009 | | - } catch (IndexOutOfBoundsException | ClassCastException error) { |
| 3013 | + final MediaItem currentMediaItem = simpleExoPlayer.getCurrentMediaItem(); |
| 3014 | + if (currentMediaItem == null || currentMediaItem.playbackProperties == null |
| 3015 | + || currentMediaItem.playbackProperties.tag == null) { |
| 3016 | + return; |
| 3017 | + } |
| 3018 | + metadata = (MediaSourceTag) currentMediaItem.playbackProperties.tag; |
| 3019 | + } catch (final IndexOutOfBoundsException | ClassCastException ex) { |
3010 | 3020 | if (DEBUG) { |
3011 | | - Log.d(TAG, "Could not update metadata: " + error.getMessage()); |
3012 | | - error.printStackTrace(); |
| 3021 | + Log.d(TAG, "Could not update metadata", ex); |
3013 | 3022 | } |
3014 | 3023 | return; |
3015 | 3024 | } |
3016 | 3025 |
|
3017 | | - if (metadata == null) { |
3018 | | - return; |
3019 | | - } |
3020 | 3026 | maybeAutoQueueNextStream(metadata); |
3021 | 3027 |
|
3022 | 3028 | if (currentMetadata == metadata) { |
@@ -3292,7 +3298,27 @@ public void onStartDrag(final PlayQueueItemHolder viewHolder) { |
3292 | 3298 | @Override // own playback listener |
3293 | 3299 | @Nullable |
3294 | 3300 | public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { |
3295 | | - return (isAudioOnly ? audioResolver : videoResolver).resolve(info); |
| 3301 | + if (audioPlayerSelected()) { |
| 3302 | + return audioResolver.resolve(info); |
| 3303 | + } |
| 3304 | + |
| 3305 | + if (isAudioOnly && videoResolver.getStreamSourceType().orElse( |
| 3306 | + SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY) |
| 3307 | + == SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY) { |
| 3308 | + // If the current info has only video streams with audio and if the stream is played as |
| 3309 | + // audio, we need to use the audio resolver, otherwise the video stream will be played |
| 3310 | + // in background. |
| 3311 | + return audioResolver.resolve(info); |
| 3312 | + } |
| 3313 | + |
| 3314 | + // Even if the stream is played in background, we need to use the video resolver if the |
| 3315 | + // info played is separated video-only and audio-only streams; otherwise, if the audio |
| 3316 | + // resolver was called when the app was in background, the app will only stream audio when |
| 3317 | + // the user come back to the app and will never fetch the video stream. |
| 3318 | + // Note that the video is not fetched when the app is in background because the video |
| 3319 | + // renderer is fully disabled (see useVideoSource method), except for HLS streams |
| 3320 | + // (see https://github.com/google/ExoPlayer/issues/9282). |
| 3321 | + return videoResolver.resolve(info); |
3296 | 3322 | } |
3297 | 3323 |
|
3298 | 3324 | public void disablePreloadingOfCurrentTrack() { |
@@ -4147,19 +4173,125 @@ public AppCompatActivity getParentActivity() { |
4147 | 4173 | return (AppCompatActivity) ((ViewGroup) binding.getRoot().getParent()).getContext(); |
4148 | 4174 | } |
4149 | 4175 |
|
4150 | | - private void useVideoSource(final boolean video) { |
4151 | | - if (playQueue == null || isAudioOnly == !video || audioPlayerSelected()) { |
| 4176 | + private void useVideoSource(final boolean videoEnabled) { |
| 4177 | + if (playQueue == null || isAudioOnly == !videoEnabled || audioPlayerSelected()) { |
4152 | 4178 | return; |
4153 | 4179 | } |
4154 | 4180 |
|
4155 | | - isAudioOnly = !video; |
4156 | | - // When a user returns from background controls could be hidden |
4157 | | - // but systemUI will be shown 100%. Hide it |
| 4181 | + isAudioOnly = !videoEnabled; |
| 4182 | + // When a user returns from background, controls could be hidden but SystemUI will be shown |
| 4183 | + // 100%. Hide it. |
4158 | 4184 | if (!isAudioOnly && !isControlsVisible()) { |
4159 | 4185 | hideSystemUIIfNeeded(); |
4160 | 4186 | } |
| 4187 | + |
| 4188 | + // The current metadata may be null sometimes (for e.g. when using an unstable connection |
| 4189 | + // in livestreams) so we will be not able to execute the block below. |
| 4190 | + // Reload the play queue manager in this case, which is the behavior when we don't know the |
| 4191 | + // index of the video renderer or playQueueManagerReloadingNeeded returns true. |
| 4192 | + if (currentMetadata == null) { |
| 4193 | + reloadPlayQueueManager(); |
| 4194 | + setRecovery(); |
| 4195 | + return; |
| 4196 | + } |
| 4197 | + |
| 4198 | + final int videoRenderIndex = getVideoRendererIndex(); |
| 4199 | + final StreamInfo info = currentMetadata.getMetadata(); |
| 4200 | + |
| 4201 | + // In the case we don't know the source type, fallback to the one with video with audio or |
| 4202 | + // audio-only source. |
| 4203 | + final SourceType sourceType = videoResolver.getStreamSourceType().orElse( |
| 4204 | + SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY); |
| 4205 | + |
| 4206 | + if (playQueueManagerReloadingNeeded(sourceType, info, videoRenderIndex)) { |
| 4207 | + reloadPlayQueueManager(); |
| 4208 | + } else { |
| 4209 | + final StreamType streamType = info.getStreamType(); |
| 4210 | + if (streamType == StreamType.AUDIO_STREAM |
| 4211 | + || streamType == StreamType.AUDIO_LIVE_STREAM) { |
| 4212 | + // Nothing to do more than setting the recovery position |
| 4213 | + setRecovery(); |
| 4214 | + return; |
| 4215 | + } |
| 4216 | + |
| 4217 | + final TrackGroupArray videoTrackGroupArray = Objects.requireNonNull( |
| 4218 | + trackSelector.getCurrentMappedTrackInfo()).getTrackGroups(videoRenderIndex); |
| 4219 | + if (videoEnabled) { |
| 4220 | + // Clearing the null selection override enable again the video stream (and its |
| 4221 | + // fetching). |
| 4222 | + trackSelector.setParameters(trackSelector.buildUponParameters() |
| 4223 | + .clearSelectionOverride(videoRenderIndex, videoTrackGroupArray)); |
| 4224 | + } else { |
| 4225 | + // Using setRendererDisabled still fetch the video stream in background, contrary |
| 4226 | + // to setSelectionOverride with a null override. |
| 4227 | + trackSelector.setParameters(trackSelector.buildUponParameters() |
| 4228 | + .setSelectionOverride(videoRenderIndex, videoTrackGroupArray, null)); |
| 4229 | + } |
| 4230 | + } |
| 4231 | + |
4161 | 4232 | setRecovery(); |
4162 | | - reloadPlayQueueManager(); |
| 4233 | + } |
| 4234 | + |
| 4235 | + /** |
| 4236 | + * Return whether the play queue manager needs to be reloaded when switching player type. |
| 4237 | + * |
| 4238 | + * <p> |
| 4239 | + * The play queue manager needs to be reloaded if the video renderer index is not known and if |
| 4240 | + * the content is not an audio content, but also if none of the following cases is met: |
| 4241 | + * |
| 4242 | + * <ul> |
| 4243 | + * <li>the content is an {@link StreamType#AUDIO_STREAM audio stream} or an |
| 4244 | + * {@link StreamType#AUDIO_LIVE_STREAM audio live stream};</li> |
| 4245 | + * <li>the content is a {@link StreamType#LIVE_STREAM live stream} and the source type is a |
| 4246 | + * {@link SourceType#LIVE_STREAM live source};</li> |
| 4247 | + * <li>the content's source is {@link SourceType#VIDEO_WITH_SEPARATED_AUDIO a video stream |
| 4248 | + * with a separated audio source} or has no audio-only streams available <b>and</b> is a |
| 4249 | + * {@link StreamType#LIVE_STREAM live stream} or a |
| 4250 | + * {@link StreamType#LIVE_STREAM live stream}. |
| 4251 | + * </li> |
| 4252 | + * </ul> |
| 4253 | + * </p> |
| 4254 | + * |
| 4255 | + * @param sourceType the {@link SourceType} of the stream |
| 4256 | + * @param streamInfo the {@link StreamInfo} of the stream |
| 4257 | + * @param videoRendererIndex the video renderer index of the video source, if that's a video |
| 4258 | + * source (or {@link #RENDERER_UNAVAILABLE}) |
| 4259 | + * @return whether the play queue manager needs to be reloaded |
| 4260 | + */ |
| 4261 | + private boolean playQueueManagerReloadingNeeded(final SourceType sourceType, |
| 4262 | + @NonNull final StreamInfo streamInfo, |
| 4263 | + final int videoRendererIndex) { |
| 4264 | + final StreamType streamType = streamInfo.getStreamType(); |
| 4265 | + |
| 4266 | + if (videoRendererIndex == RENDERER_UNAVAILABLE && streamType != StreamType.AUDIO_STREAM |
| 4267 | + && streamType != StreamType.AUDIO_LIVE_STREAM) { |
| 4268 | + return true; |
| 4269 | + } |
| 4270 | + |
| 4271 | + // The content is an audio stream, an audio live stream, or a live stream with a live |
| 4272 | + // source: it's not needed to reload the play queue manager because the stream source will |
| 4273 | + // be the same |
| 4274 | + if ((streamType == StreamType.AUDIO_STREAM || streamType == StreamType.AUDIO_LIVE_STREAM) |
| 4275 | + || (streamType == StreamType.LIVE_STREAM |
| 4276 | + && sourceType == SourceType.LIVE_STREAM)) { |
| 4277 | + return false; |
| 4278 | + } |
| 4279 | + |
| 4280 | + // The content's source is a video with separated audio or a video with audio -> the video |
| 4281 | + // and its fetch may be disabled |
| 4282 | + // The content's source is a video with embedded audio and the content has no separated |
| 4283 | + // audio stream available: it's probably not needed to reload the play queue manager |
| 4284 | + // because the stream source will be probably the same as the current played |
| 4285 | + if (sourceType == SourceType.VIDEO_WITH_SEPARATED_AUDIO |
| 4286 | + || (sourceType == SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY |
| 4287 | + && isNullOrEmpty(streamInfo.getAudioStreams()))) { |
| 4288 | + // It's not needed to reload the play queue manager only if the content's stream type |
| 4289 | + // is a video stream or a live stream |
| 4290 | + return streamType != StreamType.VIDEO_STREAM && streamType != StreamType.LIVE_STREAM; |
| 4291 | + } |
| 4292 | + |
| 4293 | + // Other cases: the play queue manager reload is needed |
| 4294 | + return true; |
4163 | 4295 | } |
4164 | 4296 | //endregion |
4165 | 4297 |
|
@@ -4197,7 +4329,7 @@ private boolean isLoading() { |
4197 | 4329 | private boolean isLive() { |
4198 | 4330 | try { |
4199 | 4331 | return !exoPlayerIsNull() && simpleExoPlayer.isCurrentWindowDynamic(); |
4200 | | - } catch (@NonNull final IndexOutOfBoundsException e) { |
| 4332 | + } catch (final IndexOutOfBoundsException e) { |
4201 | 4333 | // Why would this even happen =(... but lets log it anyway, better safe than sorry |
4202 | 4334 | if (DEBUG) { |
4203 | 4335 | Log.d(TAG, "player.isCurrentWindowDynamic() failed: ", e); |
@@ -4375,15 +4507,42 @@ private void setupVideoSurface() { |
4375 | 4507 | } |
4376 | 4508 |
|
4377 | 4509 | private void cleanupVideoSurface() { |
4378 | | - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // >=API23 |
4379 | | - if (surfaceHolderCallback != null) { |
4380 | | - if (binding != null) { |
4381 | | - binding.surfaceView.getHolder().removeCallback(surfaceHolderCallback); |
4382 | | - } |
4383 | | - surfaceHolderCallback.release(); |
4384 | | - surfaceHolderCallback = null; |
| 4510 | + // Only for API >= 23 |
| 4511 | + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && surfaceHolderCallback != null) { |
| 4512 | + if (binding != null) { |
| 4513 | + binding.surfaceView.getHolder().removeCallback(surfaceHolderCallback); |
4385 | 4514 | } |
| 4515 | + surfaceHolderCallback.release(); |
| 4516 | + surfaceHolderCallback = null; |
4386 | 4517 | } |
4387 | 4518 | } |
4388 | 4519 | //endregion |
| 4520 | + |
| 4521 | + /** |
| 4522 | + * Get the video renderer index of the current playing stream. |
| 4523 | + * |
| 4524 | + * This method returns the video renderer index of the current |
| 4525 | + * {@link MappingTrackSelector.MappedTrackInfo} or {@link #RENDERER_UNAVAILABLE} if the current |
| 4526 | + * {@link MappingTrackSelector.MappedTrackInfo} is null or if there is no video renderer index. |
| 4527 | + * |
| 4528 | + * @return the video renderer index or {@link #RENDERER_UNAVAILABLE} if it cannot be get |
| 4529 | + */ |
| 4530 | + private int getVideoRendererIndex() { |
| 4531 | + final MappingTrackSelector.MappedTrackInfo mappedTrackInfo = trackSelector |
| 4532 | + .getCurrentMappedTrackInfo(); |
| 4533 | + |
| 4534 | + if (mappedTrackInfo == null) { |
| 4535 | + return RENDERER_UNAVAILABLE; |
| 4536 | + } |
| 4537 | + |
| 4538 | + // Check every renderer |
| 4539 | + return IntStream.range(0, mappedTrackInfo.getRendererCount()) |
| 4540 | + // Check the renderer is a video renderer and has at least one track |
| 4541 | + .filter(i -> !mappedTrackInfo.getTrackGroups(i).isEmpty() |
| 4542 | + && simpleExoPlayer.getRendererType(i) == C.TRACK_TYPE_VIDEO) |
| 4543 | + // Return the first index found (there is at most one renderer per renderer type) |
| 4544 | + .findFirst() |
| 4545 | + // No video renderer index with at least one track found: return unavailable index |
| 4546 | + .orElse(RENDERER_UNAVAILABLE); |
| 4547 | + } |
4389 | 4548 | } |
0 commit comments