From 1d3a850662c42f891eaf1a774646b7a71953ca00 Mon Sep 17 00:00:00 2001 From: Ryan Feline Date: Thu, 4 Apr 2019 11:24:20 +0100 Subject: [PATCH 01/12] Deprecate `playAt` as it does not perform the way it was intended. --- core/src/main/java/com/novoda/noplayer/NoPlayer.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/com/novoda/noplayer/NoPlayer.java b/core/src/main/java/com/novoda/noplayer/NoPlayer.java index 71db17b6..60bade45 100644 --- a/core/src/main/java/com/novoda/noplayer/NoPlayer.java +++ b/core/src/main/java/com/novoda/noplayer/NoPlayer.java @@ -32,12 +32,16 @@ public interface NoPlayer extends PlayerState { void play() throws IllegalStateException; /** - * Plays content of a prepared Player at a given position. + * Deprecated: This does not perform the way it was originally intended. A seek can, and most likely will, + * occur after the content has already started playing. This can lead to some unexpected behaviour. + * Plays content of a prepared Player at a given position. Use {@link #loadVideo(Uri, Options)} passing + * a initial position to the {@link Options}. * * @param positionInMillis to start playing content from. * @throws IllegalStateException - if called before {@link NoPlayer#loadVideo(Uri, Options)}. * @see NoPlayer.PreparedListener */ + @Deprecated void playAt(long positionInMillis) throws IllegalStateException; /** From e34dc6a817a1206433009c29c1ef69315e126760 Mon Sep 17 00:00:00 2001 From: Ryan Feline Date: Thu, 4 Apr 2019 11:34:41 +0100 Subject: [PATCH 02/12] Add initial position to the player options. --- .../java/com/novoda/noplayer/Options.java | 20 +++++++++++++++++-- .../com/novoda/noplayer/OptionsBuilder.java | 20 +++++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/com/novoda/noplayer/Options.java b/core/src/main/java/com/novoda/noplayer/Options.java index 6e5363dc..8fb88b67 100644 --- a/core/src/main/java/com/novoda/noplayer/Options.java +++ b/core/src/main/java/com/novoda/noplayer/Options.java @@ -1,15 +1,22 @@ package com.novoda.noplayer; +import com.novoda.noplayer.internal.utils.Optional; + public class Options { private final ContentType contentType; private final int minDurationBeforeQualityIncreaseInMillis; private final int maxInitialBitrate; + private final Optional initialPositionInMillis; - Options(ContentType contentType, int minDurationBeforeQualityIncreaseInMillis, int maxInitialBitrate) { + Options(ContentType contentType, + int minDurationBeforeQualityIncreaseInMillis, + int maxInitialBitrate, + Optional initialPositionInMillis) { this.contentType = contentType; this.minDurationBeforeQualityIncreaseInMillis = minDurationBeforeQualityIncreaseInMillis; this.maxInitialBitrate = maxInitialBitrate; + this.initialPositionInMillis = initialPositionInMillis; } public ContentType contentType() { @@ -24,6 +31,10 @@ public int maxInitialBitrate() { return maxInitialBitrate; } + public Optional getInitialPositionInMillis() { + return initialPositionInMillis; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -41,7 +52,10 @@ public boolean equals(Object o) { if (maxInitialBitrate != options.maxInitialBitrate) { return false; } - return contentType == options.contentType; + if (contentType != options.contentType) { + return false; + } + return initialPositionInMillis != null ? initialPositionInMillis.equals(options.initialPositionInMillis) : options.initialPositionInMillis == null; } @Override @@ -49,6 +63,7 @@ public int hashCode() { int result = contentType != null ? contentType.hashCode() : 0; result = 31 * result + minDurationBeforeQualityIncreaseInMillis; result = 31 * result + maxInitialBitrate; + result = 31 * result + (initialPositionInMillis != null ? initialPositionInMillis.hashCode() : 0); return result; } @@ -58,6 +73,7 @@ public String toString() { + "contentType=" + contentType + ", minDurationBeforeQualityIncreaseInMillis=" + minDurationBeforeQualityIncreaseInMillis + ", maxInitialBitrate=" + maxInitialBitrate + + ", initialPositionInMillis=" + initialPositionInMillis + '}'; } } diff --git a/core/src/main/java/com/novoda/noplayer/OptionsBuilder.java b/core/src/main/java/com/novoda/noplayer/OptionsBuilder.java index 406d8965..71b5c07b 100644 --- a/core/src/main/java/com/novoda/noplayer/OptionsBuilder.java +++ b/core/src/main/java/com/novoda/noplayer/OptionsBuilder.java @@ -2,6 +2,8 @@ import android.net.Uri; +import com.novoda.noplayer.internal.utils.Optional; + /** * Builds instances of {@link Options} for {@link NoPlayer#loadVideo(Uri, Options)}. */ @@ -13,6 +15,7 @@ public class OptionsBuilder { private ContentType contentType = ContentType.H264; private int minDurationBeforeQualityIncreaseInMillis = DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS; private int maxInitialBitrate = DEFAULT_MAX_INITIAL_BITRATE; + private Optional initialPositionInMillis = Optional.absent(); /** * Sets {@link OptionsBuilder} to build {@link Options} with a given {@link ContentType}. @@ -44,19 +47,32 @@ public OptionsBuilder withMinDurationBeforeQualityIncreaseInMillis(int minDurati * allows the player to choose a higher quality video track at the beginning. * * @param maxInitialBitrate maximum bitrate that limits the initial track selection. - * @return {@link OptionsBuilder} + * @return {@link OptionsBuilder}. */ public OptionsBuilder withMaxInitialBitrate(int maxInitialBitrate) { this.maxInitialBitrate = maxInitialBitrate; return this; } + /** + * Sets {@link OptionsBuilder} to build {@link Options} with given initial position in millis in order + * to specify the start position of the content that will be played. Omitting to set this will start + * playback at the beginning of the content. + * + * @param initialPositionInMillis position that the content should begin playback at. + * @return {@link OptionsBuilder}. + */ + public OptionsBuilder withInitialPositionInMillis(long initialPositionInMillis) { + this.initialPositionInMillis = Optional.of(initialPositionInMillis); + return this; + } + /** * Builds a new {@link Options} instance. * * @return a {@link Options} instance. */ public Options build() { - return new Options(contentType, minDurationBeforeQualityIncreaseInMillis, maxInitialBitrate); + return new Options(contentType, minDurationBeforeQualityIncreaseInMillis, maxInitialBitrate, initialPositionInMillis); } } From 02bbcd5034ef98dd65238a72e8b5f24b034b41f4 Mon Sep 17 00:00:00 2001 From: Ryan Feline Date: Thu, 4 Apr 2019 12:07:36 +0100 Subject: [PATCH 03/12] Add initial position to the player options. --- .../noplayer/internal/exoplayer/ExoPlayerFacade.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerFacade.java b/core/src/main/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerFacade.java index 9535f623..301e9bef 100644 --- a/core/src/main/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerFacade.java +++ b/core/src/main/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerFacade.java @@ -26,7 +26,6 @@ class ExoPlayerFacade { - private static final boolean RESET_POSITION = true; private static final boolean DO_NOT_RESET_STATE = false; private final BandwidthMeterCreator bandwidthMeterCreator; @@ -136,7 +135,14 @@ void loadVideo(PlayerSurfaceHolder playerSurfaceHolder, bandwidthMeter ); attachToSurface(playerSurfaceHolder); - exoPlayer.prepare(mediaSource, RESET_POSITION, DO_NOT_RESET_STATE); + + boolean hasInitialPosition = options.getInitialPositionInMillis().isPresent(); + if (hasInitialPosition) { + Long initialPositionInMillis = options.getInitialPositionInMillis().get(); + exoPlayer.seekTo(exoPlayer.getCurrentWindowIndex(), initialPositionInMillis); + } + + exoPlayer.prepare(mediaSource, !hasInitialPosition, DO_NOT_RESET_STATE); } private void setMovieAudioAttributes(SimpleExoPlayer exoPlayer) { From 8cd8e679bc829ea931a84d5938c3819b70ac134e Mon Sep 17 00:00:00 2001 From: Ryan Feline Date: Thu, 4 Apr 2019 13:47:32 +0100 Subject: [PATCH 04/12] Use underlying seek method instead of seek given a window. --- .../com/novoda/noplayer/internal/exoplayer/ExoPlayerFacade.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerFacade.java b/core/src/main/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerFacade.java index 301e9bef..ba501be5 100644 --- a/core/src/main/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerFacade.java +++ b/core/src/main/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerFacade.java @@ -139,7 +139,7 @@ void loadVideo(PlayerSurfaceHolder playerSurfaceHolder, boolean hasInitialPosition = options.getInitialPositionInMillis().isPresent(); if (hasInitialPosition) { Long initialPositionInMillis = options.getInitialPositionInMillis().get(); - exoPlayer.seekTo(exoPlayer.getCurrentWindowIndex(), initialPositionInMillis); + exoPlayer.seekTo(initialPositionInMillis); } exoPlayer.prepare(mediaSource, !hasInitialPosition, DO_NOT_RESET_STATE); From ba6c5ec67719cd5dd03e4b43de4fe3565af0a68a Mon Sep 17 00:00:00 2001 From: Ryan Feline Date: Thu, 4 Apr 2019 13:47:48 +0100 Subject: [PATCH 05/12] Add a `toOptionsBuilder` on `Options`. --- core/src/main/java/com/novoda/noplayer/Options.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/src/main/java/com/novoda/noplayer/Options.java b/core/src/main/java/com/novoda/noplayer/Options.java index 8fb88b67..af74575d 100644 --- a/core/src/main/java/com/novoda/noplayer/Options.java +++ b/core/src/main/java/com/novoda/noplayer/Options.java @@ -9,6 +9,18 @@ public class Options { private final int maxInitialBitrate; private final Optional initialPositionInMillis; + public OptionsBuilder toOptionsBuilder() { + OptionsBuilder optionsBuilder = new OptionsBuilder() + .withContentType(contentType) + .withMinDurationBeforeQualityIncreaseInMillis(minDurationBeforeQualityIncreaseInMillis) + .withMaxInitialBitrate(maxInitialBitrate); + + if (initialPositionInMillis.isPresent()) { + optionsBuilder = optionsBuilder.withInitialPositionInMillis(initialPositionInMillis.get()); + } + return optionsBuilder; + } + Options(ContentType contentType, int minDurationBeforeQualityIncreaseInMillis, int maxInitialBitrate, From 113d76f41aa691b7e1ad519c57dfc6b3ed72dc0a Mon Sep 17 00:00:00 2001 From: Ryan Feline Date: Thu, 4 Apr 2019 13:48:18 +0100 Subject: [PATCH 06/12] Add Facade tests for initial position, ensuring a seek occurs. --- .../exoplayer/ExoPlayerFacadeTest.java | 46 ++++++++++++++++--- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/core/src/test/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerFacadeTest.java b/core/src/test/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerFacadeTest.java index 9fcdafa8..d1c77dae 100644 --- a/core/src/test/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerFacadeTest.java +++ b/core/src/test/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerFacadeTest.java @@ -39,6 +39,7 @@ import org.junit.experimental.runners.Enclosed; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; +import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -47,9 +48,12 @@ import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -59,6 +63,7 @@ public class ExoPlayerFacadeTest { private static final boolean SELECTED = true; + private static final long TWENTY_FIVE_SECONDS_IN_MILLIS = 25000; private static final long TWO_MINUTES_IN_MILLIS = 120000; private static final long TEN_MINUTES_IN_MILLIS = 600000; @@ -68,9 +73,12 @@ public class ExoPlayerFacadeTest { private static final boolean PLAY_WHEN_READY = true; private static final boolean DO_NOT_PLAY_WHEN_READY = false; private static final boolean RESET_POSITION = true; + private static final boolean DO_NOT_RESET_POSITION = false; private static final boolean DO_NOT_RESET_STATE = false; - private static final Options OPTIONS = new OptionsBuilder().withContentType(ContentType.DASH).build(); + private static final Options OPTIONS = new OptionsBuilder() + .withContentType(ContentType.DASH) + .build(); public static class GivenVideoNotLoaded extends Base { @@ -144,13 +152,38 @@ public void givenNonLollipopDevice_whenLoadingVideo_thenDoesNotSetAudioAttribute @Test public void givenMediaSource_whenLoadingVideo_thenPreparesInternalExoPlayer() { - MediaSource mediaSource = givenMediaSource(); + MediaSource mediaSource = givenMediaSource(OPTIONS); facade.loadVideo(surfaceViewHolder, drmSessionCreator, uri, OPTIONS, exoPlayerForwarder, mediaCodecSelector); verify(exoPlayer).prepare(mediaSource, RESET_POSITION, DO_NOT_RESET_STATE); } + @Test + public void givenInitialPosition_whenLoadingVideo_thenPerformsSeekBeforePreparing() { + Options options = OPTIONS.toOptionsBuilder() + .withInitialPositionInMillis(TWENTY_FIVE_SECONDS_IN_MILLIS) + .build(); + MediaSource mediaSource = givenMediaSource(options); + + facade.loadVideo(surfaceViewHolder, drmSessionCreator, uri, options, exoPlayerForwarder, mediaCodecSelector); + + InOrder inOrder = inOrder(exoPlayer); + inOrder.verify(exoPlayer).seekTo(TWENTY_FIVE_SECONDS_IN_MILLIS); + inOrder.verify(exoPlayer).prepare(mediaSource, DO_NOT_RESET_POSITION, DO_NOT_RESET_STATE); + } + + @Test + public void givenNoInitialPosition_whenLoadingVideo_thenDoesNotPerformSeekBeforePreparing() { + MediaSource mediaSource = givenMediaSource(OPTIONS); + + facade.loadVideo(surfaceViewHolder, drmSessionCreator, uri, OPTIONS, exoPlayerForwarder, mediaCodecSelector); + + InOrder inOrder = inOrder(exoPlayer); + inOrder.verify(exoPlayer, never()).seekTo(TWENTY_FIVE_SECONDS_IN_MILLIS); + inOrder.verify(exoPlayer).prepare(mediaSource, RESET_POSITION, DO_NOT_RESET_STATE); + } + @Test public void whenQueryingIsPlaying_thenReturnsFalse() { @@ -250,7 +283,7 @@ public void setUp() { } private void givenPlayerIsLoaded() { - givenMediaSource(); + givenMediaSource(OPTIONS); facade.loadVideo(surfaceViewHolder, drmSessionCreator, uri, OPTIONS, exoPlayerForwarder, mediaCodecSelector); } @@ -514,8 +547,9 @@ public void setUp() { given(exoPlayerForwarder.drmSessionEventListener()).willReturn(drmSessionEventListener); given(exoPlayerForwarder.mediaSourceEventListener()).willReturn(mediaSourceEventListener); given(bandwidthMeterCreator.create(anyLong())).willReturn(defaultBandwidthMeter); - given(trackSelectorCreator.create(OPTIONS, defaultBandwidthMeter)).willReturn(trackSelector); + given(trackSelectorCreator.create(any(Options.class), eq(defaultBandwidthMeter))).willReturn(trackSelector); given(exoPlayerCreator.create(drmSessionCreator, drmSessionEventListener, mediaCodecSelector, trackSelector.trackSelector())).willReturn(exoPlayer); + willDoNothing().given(exoPlayer).seekTo(anyInt()); given(rendererTypeRequesterCreator.createfrom(exoPlayer)).willReturn(rendererTypeRequester); facade = new ExoPlayerFacade( bandwidthMeterCreator, @@ -530,11 +564,11 @@ public void setUp() { textureViewHolder = PlayerSurfaceHolder.create(textureView); } - MediaSource givenMediaSource() { + MediaSource givenMediaSource(Options options) { MediaSource mediaSource = mock(MediaSource.class); given( mediaSourceFactory.create( - OPTIONS, + options, uri, mediaSourceEventListener, defaultBandwidthMeter From 1326cba16ac7e25d05bee6be0323f4592622bbd0 Mon Sep 17 00:00:00 2001 From: Ryan Feline Date: Thu, 4 Apr 2019 14:23:05 +0100 Subject: [PATCH 07/12] Add java doc to the Options. --- core/src/main/java/com/novoda/noplayer/Options.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/src/main/java/com/novoda/noplayer/Options.java b/core/src/main/java/com/novoda/noplayer/Options.java index af74575d..e5daa175 100644 --- a/core/src/main/java/com/novoda/noplayer/Options.java +++ b/core/src/main/java/com/novoda/noplayer/Options.java @@ -2,6 +2,9 @@ import com.novoda.noplayer.internal.utils.Optional; +/** + * Options to customise the underlying player. + */ public class Options { private final ContentType contentType; @@ -9,6 +12,11 @@ public class Options { private final int maxInitialBitrate; private final Optional initialPositionInMillis; + /** + * Creates a {@link OptionsBuilder} from this Options. + * + * @return a new instance of {@link OptionsBuilder}. + */ public OptionsBuilder toOptionsBuilder() { OptionsBuilder optionsBuilder = new OptionsBuilder() .withContentType(contentType) From 0d7cb91ec0f2546c02c41bd816f60fb6ef5117b6 Mon Sep 17 00:00:00 2001 From: Ryan Feline Date: Thu, 4 Apr 2019 14:29:44 +0100 Subject: [PATCH 08/12] Fix line length. --- core/src/main/java/com/novoda/noplayer/Options.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/com/novoda/noplayer/Options.java b/core/src/main/java/com/novoda/noplayer/Options.java index e5daa175..da2f5aa6 100644 --- a/core/src/main/java/com/novoda/noplayer/Options.java +++ b/core/src/main/java/com/novoda/noplayer/Options.java @@ -75,7 +75,8 @@ public boolean equals(Object o) { if (contentType != options.contentType) { return false; } - return initialPositionInMillis != null ? initialPositionInMillis.equals(options.initialPositionInMillis) : options.initialPositionInMillis == null; + return initialPositionInMillis != null + ? initialPositionInMillis.equals(options.initialPositionInMillis) : options.initialPositionInMillis == null; } @Override From c4b67388a4f9fc6d1fca2f7cf8d05795f5abfd1a Mon Sep 17 00:00:00 2001 From: Francesco Pontillo Date: Fri, 5 Apr 2019 17:39:32 +0100 Subject: [PATCH 09/12] Add max video bitrate in options and change it with methods --- .../java/com/novoda/noplayer/NoPlayer.java | 12 ++++++++++ .../java/com/novoda/noplayer/Options.java | 15 +++++++++++- .../com/novoda/noplayer/OptionsBuilder.java | 23 +++++++++++++++++- .../exoplayer/CompositeTrackSelector.java | 8 +++++++ .../CompositeTrackSelectorCreator.java | 4 ++++ .../internal/exoplayer/ExoPlayerFacade.java | 11 ++++++++- .../internal/exoplayer/ExoPlayerTwoImpl.java | 10 ++++++++ .../mediasource/ExoPlayerTrackSelector.java | 16 +++++++++++++ .../ExoPlayerVideoTrackSelector.java | 8 +++++++ .../mediaplayer/AndroidMediaPlayerFacade.java | 10 ++++++++ .../mediaplayer/AndroidMediaPlayerImpl.java | 11 +++++++++ .../ExoPlayerVideoTrackSelectorTest.java | 24 ++++++++++++++++--- 12 files changed, 146 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/com/novoda/noplayer/NoPlayer.java b/core/src/main/java/com/novoda/noplayer/NoPlayer.java index 60bade45..5a345c19 100644 --- a/core/src/main/java/com/novoda/noplayer/NoPlayer.java +++ b/core/src/main/java/com/novoda/noplayer/NoPlayer.java @@ -227,6 +227,18 @@ public interface NoPlayer extends PlayerState { @FloatRange(from = 0.0f, to = 1.0f) float getVolume() throws IllegalStateException; + /** + * Clears the maximum video bitrate, if set. + */ + void clearMaxVideoBitrate(); + + /** + * Sets a maximum video bitrate. If the content is playing, the video will switch to a different quality. + * + * @param maxVideoBitrate The maximum video bitrate in bit per second. + */ + void setMaxVideoBitrate(int maxVideoBitrate); + interface PlayerError { PlayerErrorType type(); diff --git a/core/src/main/java/com/novoda/noplayer/Options.java b/core/src/main/java/com/novoda/noplayer/Options.java index da2f5aa6..8fa5dfe5 100644 --- a/core/src/main/java/com/novoda/noplayer/Options.java +++ b/core/src/main/java/com/novoda/noplayer/Options.java @@ -10,6 +10,7 @@ public class Options { private final ContentType contentType; private final int minDurationBeforeQualityIncreaseInMillis; private final int maxInitialBitrate; + private final int maxVideoBitrate; private final Optional initialPositionInMillis; /** @@ -21,7 +22,8 @@ public OptionsBuilder toOptionsBuilder() { OptionsBuilder optionsBuilder = new OptionsBuilder() .withContentType(contentType) .withMinDurationBeforeQualityIncreaseInMillis(minDurationBeforeQualityIncreaseInMillis) - .withMaxInitialBitrate(maxInitialBitrate); + .withMaxInitialBitrate(maxInitialBitrate) + .withMaxVideoBitrate(maxVideoBitrate); if (initialPositionInMillis.isPresent()) { optionsBuilder = optionsBuilder.withInitialPositionInMillis(initialPositionInMillis.get()); @@ -32,10 +34,12 @@ public OptionsBuilder toOptionsBuilder() { Options(ContentType contentType, int minDurationBeforeQualityIncreaseInMillis, int maxInitialBitrate, + int maxVideoBitrate, Optional initialPositionInMillis) { this.contentType = contentType; this.minDurationBeforeQualityIncreaseInMillis = minDurationBeforeQualityIncreaseInMillis; this.maxInitialBitrate = maxInitialBitrate; + this.maxVideoBitrate = maxVideoBitrate; this.initialPositionInMillis = initialPositionInMillis; } @@ -51,6 +55,10 @@ public int maxInitialBitrate() { return maxInitialBitrate; } + public int maxVideoBitrate() { + return maxVideoBitrate; + } + public Optional getInitialPositionInMillis() { return initialPositionInMillis; } @@ -72,6 +80,9 @@ public boolean equals(Object o) { if (maxInitialBitrate != options.maxInitialBitrate) { return false; } + if (maxVideoBitrate != options.maxVideoBitrate) { + return false; + } if (contentType != options.contentType) { return false; } @@ -84,6 +95,7 @@ public int hashCode() { int result = contentType != null ? contentType.hashCode() : 0; result = 31 * result + minDurationBeforeQualityIncreaseInMillis; result = 31 * result + maxInitialBitrate; + result = 31 * result + maxVideoBitrate; result = 31 * result + (initialPositionInMillis != null ? initialPositionInMillis.hashCode() : 0); return result; } @@ -94,6 +106,7 @@ public String toString() { + "contentType=" + contentType + ", minDurationBeforeQualityIncreaseInMillis=" + minDurationBeforeQualityIncreaseInMillis + ", maxInitialBitrate=" + maxInitialBitrate + + ", maxVideoBitrate=" + maxVideoBitrate + ", initialPositionInMillis=" + initialPositionInMillis + '}'; } diff --git a/core/src/main/java/com/novoda/noplayer/OptionsBuilder.java b/core/src/main/java/com/novoda/noplayer/OptionsBuilder.java index 71b5c07b..f1dd322e 100644 --- a/core/src/main/java/com/novoda/noplayer/OptionsBuilder.java +++ b/core/src/main/java/com/novoda/noplayer/OptionsBuilder.java @@ -11,10 +11,12 @@ public class OptionsBuilder { private static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000; private static final int DEFAULT_MAX_INITIAL_BITRATE = 800000; + private static final int DEFAULT_MAX_VIDEO_BITRATE = Integer.MAX_VALUE; private ContentType contentType = ContentType.H264; private int minDurationBeforeQualityIncreaseInMillis = DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS; private int maxInitialBitrate = DEFAULT_MAX_INITIAL_BITRATE; + private int maxVideoBitrate = DEFAULT_MAX_VIDEO_BITRATE; private Optional initialPositionInMillis = Optional.absent(); /** @@ -54,6 +56,19 @@ public OptionsBuilder withMaxInitialBitrate(int maxInitialBitrate) { return this; } + /** + * Sets {@link OptionsBuilder} to build {@link Options} with given maximum video bitrate in order to + * control what is the maximum video quality with which {@link NoPlayer} starts the playback. Setting a higher value + * allows the player to choose a higher quality video track. + * + * @param maxVideoBitrate maximum bitrate that limits the initial track selection. + * @return {@link OptionsBuilder} + */ + public OptionsBuilder withMaxVideoBitrate(int maxVideoBitrate) { + this.maxVideoBitrate = maxVideoBitrate; + return this; + } + /** * Sets {@link OptionsBuilder} to build {@link Options} with given initial position in millis in order * to specify the start position of the content that will be played. Omitting to set this will start @@ -73,6 +88,12 @@ public OptionsBuilder withInitialPositionInMillis(long initialPositionInMillis) * @return a {@link Options} instance. */ public Options build() { - return new Options(contentType, minDurationBeforeQualityIncreaseInMillis, maxInitialBitrate, initialPositionInMillis); + return new Options( + contentType, + minDurationBeforeQualityIncreaseInMillis, + maxInitialBitrate, + maxVideoBitrate, + initialPositionInMillis + ); } } diff --git a/core/src/main/java/com/novoda/noplayer/internal/exoplayer/CompositeTrackSelector.java b/core/src/main/java/com/novoda/noplayer/internal/exoplayer/CompositeTrackSelector.java index 41f7b3d1..762e68a2 100644 --- a/core/src/main/java/com/novoda/noplayer/internal/exoplayer/CompositeTrackSelector.java +++ b/core/src/main/java/com/novoda/noplayer/internal/exoplayer/CompositeTrackSelector.java @@ -77,4 +77,12 @@ List getSubtitleTracks(RendererTypeRequester rendererTypeRe boolean clearSubtitleTrack(RendererTypeRequester rendererTypeRequester) { return subtitleTrackSelector.clearSubtitleTrack(rendererTypeRequester); } + + void clearMaxVideoBitrate() { + videoTrackSelector.clearMaxVideoBitrate(); + } + + void setMaxVideoBitrate(int maxVideoBitrate) { + videoTrackSelector.setMaxVideoBitrate(maxVideoBitrate); + } } diff --git a/core/src/main/java/com/novoda/noplayer/internal/exoplayer/CompositeTrackSelectorCreator.java b/core/src/main/java/com/novoda/noplayer/internal/exoplayer/CompositeTrackSelectorCreator.java index a0a18e12..10cc368a 100644 --- a/core/src/main/java/com/novoda/noplayer/internal/exoplayer/CompositeTrackSelectorCreator.java +++ b/core/src/main/java/com/novoda/noplayer/internal/exoplayer/CompositeTrackSelectorCreator.java @@ -25,6 +25,10 @@ CompositeTrackSelector create(Options options, DefaultBandwidthMeter bandwidthMe Clock.DEFAULT ); DefaultTrackSelector trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory); + DefaultTrackSelector.Parameters trackSelectorParameters = trackSelector.buildUponParameters() + .setMaxVideoBitrate(options.maxVideoBitrate()) + .build(); + trackSelector.setParameters(trackSelectorParameters); ExoPlayerTrackSelector exoPlayerTrackSelector = ExoPlayerTrackSelector.newInstance(trackSelector); ExoPlayerAudioTrackSelector audioTrackSelector = new ExoPlayerAudioTrackSelector(exoPlayerTrackSelector); diff --git a/core/src/main/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerFacade.java b/core/src/main/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerFacade.java index ba501be5..7abe86b2 100644 --- a/core/src/main/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerFacade.java +++ b/core/src/main/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerFacade.java @@ -237,10 +237,19 @@ float getVolume() { return exoPlayer.getVolume(); } + void clearMaxVideoBitrate() { + assertVideoLoaded(); + compositeTrackSelector.clearMaxVideoBitrate(); + } + + void setMaxVideoBitrate(int maxVideoBitrate) { + assertVideoLoaded(); + compositeTrackSelector.setMaxVideoBitrate(maxVideoBitrate); + } + private void assertVideoLoaded() { if (exoPlayer == null) { throw new IllegalStateException("Video must be loaded before trying to interact with the player"); } } - } diff --git a/core/src/main/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerTwoImpl.java b/core/src/main/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerTwoImpl.java index 19445960..a483d432 100644 --- a/core/src/main/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerTwoImpl.java +++ b/core/src/main/java/com/novoda/noplayer/internal/exoplayer/ExoPlayerTwoImpl.java @@ -136,6 +136,16 @@ public float getVolume() { return exoPlayer.getVolume(); } + @Override + public void clearMaxVideoBitrate() { + exoPlayer.clearMaxVideoBitrate(); + } + + @Override + public void setMaxVideoBitrate(int maxVideoBitrate) { + exoPlayer.setMaxVideoBitrate(maxVideoBitrate); + } + @Override public Listeners getListeners() { return listenersHolder; diff --git a/core/src/main/java/com/novoda/noplayer/internal/exoplayer/mediasource/ExoPlayerTrackSelector.java b/core/src/main/java/com/novoda/noplayer/internal/exoplayer/mediasource/ExoPlayerTrackSelector.java index b1b22d68..9843339f 100644 --- a/core/src/main/java/com/novoda/noplayer/internal/exoplayer/mediasource/ExoPlayerTrackSelector.java +++ b/core/src/main/java/com/novoda/noplayer/internal/exoplayer/mediasource/ExoPlayerTrackSelector.java @@ -80,4 +80,20 @@ boolean supportsTrackSwitching(TrackType trackType, && trackGroups.get(groupIndex).length > 0 && trackInfo().getAdaptiveSupport(audioRendererIndex.get(), groupIndex, false) != RendererCapabilities.ADAPTIVE_NOT_SUPPORTED; } + + void clearMaxVideoBitrate() { + setMaxVideoBitrateParameter(Integer.MAX_VALUE); + } + + void setMaxVideoBitrate(int maxVideoBitrate) { + setMaxVideoBitrateParameter(maxVideoBitrate); + } + + private void setMaxVideoBitrateParameter(int maxValue) { + trackSelector.setParameters( + trackSelector.buildUponParameters() + .setMaxVideoBitrate(maxValue) + .build() + ); + } } diff --git a/core/src/main/java/com/novoda/noplayer/internal/exoplayer/mediasource/ExoPlayerVideoTrackSelector.java b/core/src/main/java/com/novoda/noplayer/internal/exoplayer/mediasource/ExoPlayerVideoTrackSelector.java index 323c9c14..1db69d52 100644 --- a/core/src/main/java/com/novoda/noplayer/internal/exoplayer/mediasource/ExoPlayerVideoTrackSelector.java +++ b/core/src/main/java/com/novoda/noplayer/internal/exoplayer/mediasource/ExoPlayerVideoTrackSelector.java @@ -87,4 +87,12 @@ private Optional findSelectedVideoTrack(Format selectedVideoFo public boolean clearVideoTrack(RendererTypeRequester rendererTypeRequester) { return trackSelector.clearSelectionOverrideFor(VIDEO, rendererTypeRequester); } + + public void clearMaxVideoBitrate() { + trackSelector.clearMaxVideoBitrate(); + } + + public void setMaxVideoBitrate(int maxVideoBitrate) { + trackSelector.setMaxVideoBitrate(maxVideoBitrate); + } } diff --git a/core/src/main/java/com/novoda/noplayer/internal/mediaplayer/AndroidMediaPlayerFacade.java b/core/src/main/java/com/novoda/noplayer/internal/mediaplayer/AndroidMediaPlayerFacade.java index ecda160b..bfe2ce61 100644 --- a/core/src/main/java/com/novoda/noplayer/internal/mediaplayer/AndroidMediaPlayerFacade.java +++ b/core/src/main/java/com/novoda/noplayer/internal/mediaplayer/AndroidMediaPlayerFacade.java @@ -322,4 +322,14 @@ float getVolume() { assertIsInPlaybackState(); return volume; } + + void clearMaxVideoBitrate() { + assertIsInPlaybackState(); + NoPlayerLog.w("Tried to clear max video bitrate but has not been implemented for MediaPlayer."); + } + + void setMaxVideoBitrate(int maxVideoBitrate) { + assertIsInPlaybackState(); + NoPlayerLog.w("Tried to set max video bitrate but has not been implemented for MediaPlayer."); + } } diff --git a/core/src/main/java/com/novoda/noplayer/internal/mediaplayer/AndroidMediaPlayerImpl.java b/core/src/main/java/com/novoda/noplayer/internal/mediaplayer/AndroidMediaPlayerImpl.java index 1cb6d918..cdc83214 100644 --- a/core/src/main/java/com/novoda/noplayer/internal/mediaplayer/AndroidMediaPlayerImpl.java +++ b/core/src/main/java/com/novoda/noplayer/internal/mediaplayer/AndroidMediaPlayerImpl.java @@ -5,6 +5,7 @@ import android.view.Surface; import android.view.SurfaceHolder; import android.view.View; + import com.novoda.noplayer.Listeners; import com.novoda.noplayer.NoPlayer; import com.novoda.noplayer.Options; @@ -366,6 +367,16 @@ public List getSubtitleTracks() throws IllegalStateExceptio return mediaPlayer.getSubtitleTracks(); } + @Override + public void clearMaxVideoBitrate() { + mediaPlayer.clearMaxVideoBitrate(); + } + + @Override + public void setMaxVideoBitrate(int maxVideoBitrate) { + mediaPlayer.setMaxVideoBitrate(maxVideoBitrate); + } + @Override public void stop() { reset(); diff --git a/core/src/test/java/com/novoda/noplayer/internal/exoplayer/mediasource/ExoPlayerVideoTrackSelectorTest.java b/core/src/test/java/com/novoda/noplayer/internal/exoplayer/mediasource/ExoPlayerVideoTrackSelectorTest.java index ffbe5633..64de7d04 100644 --- a/core/src/test/java/com/novoda/noplayer/internal/exoplayer/mediasource/ExoPlayerVideoTrackSelectorTest.java +++ b/core/src/test/java/com/novoda/noplayer/internal/exoplayer/mediasource/ExoPlayerVideoTrackSelectorTest.java @@ -11,9 +11,6 @@ import com.novoda.noplayer.internal.utils.Optional; import com.novoda.noplayer.model.PlayerVideoTrack; -import java.util.Arrays; -import java.util.List; - import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -22,6 +19,9 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.util.Arrays; +import java.util.List; + import static com.novoda.noplayer.internal.exoplayer.mediasource.VideoFormatFixture.aVideoFormat; import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -118,6 +118,24 @@ public void givenNoCurrentlySelectedTrack_whenGettingCurrentlySelectedVideoTrack assertThat(selectedVideoTrack).isEqualTo(Optional.absent()); } + @Test + public void givenTrackSelector_whenClearMaxVideoBitrate_thenClearsMaxVideoBitrate() { + givenTrackSelectorContainsTracks(); + + exoPlayerVideoTrackSelector.clearMaxVideoBitrate(); + + verify(trackSelector).clearMaxVideoBitrate(); + } + + @Test + public void givenTrackSelector_whenSetMaxVideoBitrate1000000_thenSetsMaxVideoBitrate1000000() { + givenTrackSelectorContainsTracks(); + + exoPlayerVideoTrackSelector.setMaxVideoBitrate(1000000); + + verify(trackSelector).setMaxVideoBitrate(1000000); + } + private void givenTrackSelectorContainsTracks() { TrackGroupArray trackGroups = new TrackGroupArray( new TrackGroup(VIDEO_FORMAT, ADDITIONAL_VIDEO_FORMAT) From 4e2a35339f1291a1255bcc125f2650872e52b738 Mon Sep 17 00:00:00 2001 From: Francesco Pontillo Date: Fri, 5 Apr 2019 17:39:42 +0100 Subject: [PATCH 10/12] Update demo to use max video bitrate --- .../java/com/novoda/demo/MainActivity.java | 27 ++++++++++++++++++- demo/src/main/res/layout/activity_main.xml | 7 +++++ demo/src/main/res/values/strings.xml | 1 + 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/demo/src/main/java/com/novoda/demo/MainActivity.java b/demo/src/main/java/com/novoda/demo/MainActivity.java index 8bb4f5a9..1662f90d 100644 --- a/demo/src/main/java/com/novoda/demo/MainActivity.java +++ b/demo/src/main/java/com/novoda/demo/MainActivity.java @@ -5,6 +5,8 @@ import android.os.Bundle; import android.util.Log; import android.view.View; +import android.widget.CheckBox; +import android.widget.CompoundButton; import android.widget.Toast; import com.novoda.noplayer.ContentType; @@ -17,14 +19,16 @@ public class MainActivity extends Activity { - private static final String URI_VIDEO_WIDEVINE_EXAMPLE_MODULAR_MPD = "https://storage.googleapis.com/content-samples/multi-audio/manifest.mpd"; + private static final String URI_VIDEO_WIDEVINE_EXAMPLE_MODULAR_MPD = "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd"; private static final String EXAMPLE_MODULAR_LICENSE_SERVER_PROXY = "https://proxy.uat.widevine.com/proxy?provider=widevine_test"; private static final int HALF_A_SECOND_IN_MILLIS = 500; private static final int TWO_MEGABITS = 2000000; + private static final int MAX_VIDEO_BITRATE = 800000; private NoPlayer player; private DemoPresenter demoPresenter; private DialogCreator dialogCreator; + private CheckBox hdSelectionCheckBox; @Override protected void onCreate(Bundle savedInstanceState) { @@ -35,11 +39,13 @@ protected void onCreate(Bundle savedInstanceState) { View videoSelectionButton = findViewById(R.id.button_video_selection); View audioSelectionButton = findViewById(R.id.button_audio_selection); View subtitleSelectionButton = findViewById(R.id.button_subtitle_selection); + hdSelectionCheckBox = findViewById(R.id.button_hd_selection); ControllerView controllerView = findViewById(R.id.controller_view); videoSelectionButton.setOnClickListener(showVideoSelectionDialog); audioSelectionButton.setOnClickListener(showAudioSelectionDialog); subtitleSelectionButton.setOnClickListener(showSubtitleSelectionDialog); + hdSelectionCheckBox.setOnCheckedChangeListener(toggleHdSelection); DataPostingModularDrm drmHandler = new DataPostingModularDrm(EXAMPLE_MODULAR_LICENSE_SERVER_PROXY); @@ -70,10 +76,18 @@ protected void onStart() { .withContentType(ContentType.DASH) .withMinDurationBeforeQualityIncreaseInMillis(HALF_A_SECOND_IN_MILLIS) .withMaxInitialBitrate(TWO_MEGABITS) + .withMaxVideoBitrate(getMaxVideoBitrate()) .build(); demoPresenter.startPresenting(uri, options); } + private int getMaxVideoBitrate() { + if (hdSelectionCheckBox.isChecked()) { + return Integer.MAX_VALUE; + } + return MAX_VIDEO_BITRATE; + } + private final View.OnClickListener showVideoSelectionDialog = new View.OnClickListener() { @Override public void onClick(View v) { @@ -104,6 +118,17 @@ public void onClick(View v) { } }; + private final CompoundButton.OnCheckedChangeListener toggleHdSelection = new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) { + player.clearMaxVideoBitrate(); + } else { + player.setMaxVideoBitrate(MAX_VIDEO_BITRATE); + } + } + }; + @Override protected void onStop() { demoPresenter.stopPresenting(); diff --git a/demo/src/main/res/layout/activity_main.xml b/demo/src/main/res/layout/activity_main.xml index 4ea31e72..840f7797 100644 --- a/demo/src/main/res/layout/activity_main.xml +++ b/demo/src/main/res/layout/activity_main.xml @@ -33,6 +33,13 @@ android:layout_height="wrap_content" android:text="@string/subtitle" /> + + Video Audio Subtitle + HD Play/pause From 283fe983579e0d8f9814460c01295523627eb1f5 Mon Sep 17 00:00:00 2001 From: Francesco Pontillo Date: Mon, 8 Apr 2019 10:47:24 +0100 Subject: [PATCH 11/12] Suppress warnings --- .../noplayer/internal/mediaplayer/AndroidMediaPlayerFacade.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/src/main/java/com/novoda/noplayer/internal/mediaplayer/AndroidMediaPlayerFacade.java b/core/src/main/java/com/novoda/noplayer/internal/mediaplayer/AndroidMediaPlayerFacade.java index bfe2ce61..17d79639 100644 --- a/core/src/main/java/com/novoda/noplayer/internal/mediaplayer/AndroidMediaPlayerFacade.java +++ b/core/src/main/java/com/novoda/noplayer/internal/mediaplayer/AndroidMediaPlayerFacade.java @@ -26,6 +26,8 @@ import static com.novoda.noplayer.internal.mediaplayer.PlaybackStateChecker.PlaybackState.PAUSED; import static com.novoda.noplayer.internal.mediaplayer.PlaybackStateChecker.PlaybackState.PLAYING; +// Not much we can do, wrapping MediaPlayer is a lot of work +@SuppressWarnings("PMD.GodClass") class AndroidMediaPlayerFacade { private static final Map NO_HEADERS = null; From 4eabcd6ccce416252dcab9000e4168dbad6534fe Mon Sep 17 00:00:00 2001 From: Juanky Soriano Date: Mon, 8 Apr 2019 07:47:18 -0500 Subject: [PATCH 12/12] Bumps up version to 4.5.4 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 09c7c002..b0839531 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ allprojects { - version = '4.5.3' + version = '4.5.4' } def teamPropsFile(propsFile) {