diff --git a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java index f835c87466..40663fa236 100644 --- a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java +++ b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java @@ -16,16 +16,20 @@ package com.google.android.exoplayer2.source.hls.playlist; import android.net.Uri; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.util.MimeTypes; + +import junit.framework.TestCase; + import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.Charset; import java.util.Collections; +import java.util.Comparator; import java.util.List; -import junit.framework.TestCase; /** * Test for {@link HlsMasterPlaylistParserTest} @@ -144,6 +148,44 @@ public class HlsMasterPlaylistParserTest extends TestCase { assertEquals(Collections.emptyList(), playlist.muxedCaptionFormats); } + public void testReorderedVariantCopy() throws IOException { + HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST); + HlsMasterPlaylist nonReorderedPlaylist = + playlist.copyWithReorderedVariants(new Comparator() { + @Override + public int compare(HlsMasterPlaylist.HlsUrl url1, HlsMasterPlaylist.HlsUrl url2) { + return 0; + } + }); + assertEquals(playlist.variants, nonReorderedPlaylist.variants); + HlsMasterPlaylist.HlsUrl preferred = null; + for (HlsMasterPlaylist.HlsUrl url : playlist.variants) { + if (preferred == null || url.format.bitrate > preferred.format.bitrate) { + preferred = url; + } + } + + assertNotNull(preferred); + + final Comparator comparator = Collections.reverseOrder(new Comparator() { + @Override + public int compare(HlsMasterPlaylist.HlsUrl url1, HlsMasterPlaylist.HlsUrl url2) { + if (url1.format.bitrate > url2.format.bitrate) { + return 1; + } + + if (url2.format.bitrate > url1.format.bitrate) { + return -1; + } + + return 0; + } + }); + HlsMasterPlaylist reorderedPlaylist = playlist.copyWithReorderedVariants(comparator); + + assertEquals(reorderedPlaylist.variants.get(0), preferred); + } + private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString) throws IOException { Uri playlistUri = Uri.parse(uri); diff --git a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParserTest.java b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParserTest.java new file mode 100644 index 0000000000..2816832704 --- /dev/null +++ b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParserTest.java @@ -0,0 +1,107 @@ +package com.google.android.exoplayer2.source.hls.playlist; + +import android.net.Uri; + +import com.google.android.exoplayer2.C; + +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.Comparator; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +public class ReorderingHlsPlaylistParserTest extends TestCase { + private static final String MASTER_PLAYLIST = " #EXTM3U \n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + + "http://example.com/low.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=2560000,FRAME-RATE=25,RESOLUTION=384x160\n" + + "http://example.com/mid.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=7680000,FRAME-RATE=29.997\n" + + "http://example.com/hi.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n" + + "http://example.com/audio-only.m3u8"; + + public void testReorderingWithNonMasterPlaylist() throws IOException { + Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); + String playlistString = "#EXTM3U\n" + + "#EXT-X-VERSION:3\n" + + "#EXT-X-PLAYLIST-TYPE:VOD\n" + + "#EXT-X-START:TIME-OFFSET=-25" + + "#EXT-X-TARGETDURATION:8\n" + + "#EXT-X-MEDIA-SEQUENCE:2679\n" + + "#EXT-X-DISCONTINUITY-SEQUENCE:4\n" + + "#EXT-X-ALLOW-CACHE:YES\n" + + "\n" + + "#EXTINF:7.975,\n" + + "#EXT-X-BYTERANGE:51370@0\n" + + "https://priv.example.com/fileSequence2679.ts\n" + + "\n" + + "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=2680\",IV=0x1566B\n" + + "#EXTINF:7.975,\n" + + "#EXT-X-BYTERANGE:51501@2147483648\n" + + "https://priv.example.com/fileSequence2680.ts\n" + + "\n" + + "#EXT-X-KEY:METHOD=NONE\n" + + "#EXTINF:7.941,\n" + + "#EXT-X-BYTERANGE:51501\n" // @2147535149 + + "https://priv.example.com/fileSequence2681.ts\n" + + "\n" + + "#EXT-X-DISCONTINUITY\n" + + "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=2682\"\n" + + "#EXTINF:7.975,\n" + + "#EXT-X-BYTERANGE:51740\n" // @2147586650 + + "https://priv.example.com/fileSequence2682.ts\n" + + "\n" + + "#EXTINF:7.975,\n" + + "https://priv.example.com/fileSequence2683.ts\n" + + "#EXT-X-ENDLIST"; + InputStream inputStream = new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME))); + Comparator comparator = mock(Comparator.class); + ReorderingHlsPlaylistParser playlistParser = new ReorderingHlsPlaylistParser(new HlsPlaylistParser(), + comparator); + final HlsMediaPlaylist playlist = (HlsMediaPlaylist) playlistParser.parse(playlistUri, inputStream); + assertNotNull(playlist); + // We should never compare the variants for a media level playlist. + verify(comparator, never()).compare(any(HlsMasterPlaylist.HlsUrl.class), any(HlsMasterPlaylist.HlsUrl.class)); + } + + public void testReorderingForMasterPlaylist() throws IOException { + Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); + ByteArrayInputStream inputStream = new ByteArrayInputStream( + MASTER_PLAYLIST.getBytes(Charset.forName(C.UTF8_NAME))); + final Comparator comparator = Collections.reverseOrder(new Comparator() { + @Override + public int compare(HlsMasterPlaylist.HlsUrl url1, HlsMasterPlaylist.HlsUrl url2) { + if (url1.format.bitrate > url2.format.bitrate) { + return 1; + } + + if (url2.format.bitrate > url1.format.bitrate) { + return -1; + } + + return 0; + } + }); + ReorderingHlsPlaylistParser playlistParser = new ReorderingHlsPlaylistParser(new HlsPlaylistParser(), + comparator); + final HlsMasterPlaylist reorderedPlaylist = (HlsMasterPlaylist) playlistParser.parse(playlistUri, inputStream); + assertNotNull(reorderedPlaylist); + + inputStream.reset(); + final HlsMasterPlaylist playlist = (HlsMasterPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); + assertEquals(reorderedPlaylist.variants.get(0).format, playlist.variants.get(2).format); + } +} \ No newline at end of file diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index fd3d533337..b7f7124e44 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -26,9 +26,12 @@ import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.List; @@ -52,6 +55,7 @@ public final class HlsMediaSource implements MediaSource, private final HlsDataSourceFactory dataSourceFactory; private final int minLoadableRetryCount; private final EventDispatcher eventDispatcher; + private final ParsingLoadable.Parser playlistParser; private HlsPlaylistTracker playlistTracker; private Listener sourceListener; @@ -72,9 +76,17 @@ public final class HlsMediaSource implements MediaSource, public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { + this(manifestUri, dataSourceFactory, minLoadableRetryCount, eventHandler, eventListener, new HlsPlaylistParser()); + } + + public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, + int minLoadableRetryCount, Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener, + ParsingLoadable.Parser playlistParser) { this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; + this.playlistParser = playlistParser; eventDispatcher = new EventDispatcher(eventHandler, eventListener); } @@ -82,7 +94,7 @@ public final class HlsMediaSource implements MediaSource, public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { Assertions.checkState(playlistTracker == null); playlistTracker = new HlsPlaylistTracker(manifestUri, dataSourceFactory, eventDispatcher, - minLoadableRetryCount, this); + minLoadableRetryCount, this, playlistParser); sourceListener = listener; playlistTracker.start(); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java index 04192def9d..5ded975f88 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.MimeTypes; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.List; /** @@ -123,6 +124,25 @@ public final class HlsMasterPlaylist extends HlsPlaylist { muxedAudioFormat, muxedCaptionFormats); } + /** + * Returns a copy of this playlist which includes the variants sorted using the passed comparator. NOTE: the variants + * will be sorted in ascending order by default. If you wish to use descending order, you can wrap your comparator in + * {@link Collections#reverseOrder(Comparator)}. + * + * @param variantComparator the comparator to use to sort the variant list. + * @return a copy of this playlist which includes the variants sorted using the passed comparator. + */ + public HlsMasterPlaylist copyWithReorderedVariants(Comparator variantComparator) { + return new HlsMasterPlaylist(baseUri, tags, filterVariants(variants, variantComparator), audios, + subtitles, muxedAudioFormat, muxedCaptionFormats); + } + + private List filterVariants(List variants, Comparator variantComparator) { + List reorderedList = new ArrayList<>(variants); + Collections.sort(reorderedList, variantComparator); + return reorderedList; + } + /** * Creates a playlist with a single variant. * diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java index 567dbd4af6..a0e299632d 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java @@ -120,7 +120,7 @@ public final class HlsPlaylistTracker implements Loader.Callback playlistParser; private final int minRetryCount; private final IdentityHashMap playlistBundles; private final Handler playlistRefreshHandler; @@ -145,7 +145,7 @@ public final class HlsPlaylistTracker implements Loader.Callback playlistParser) { this.initialPlaylistUri = initialPlaylistUri; this.dataSourceFactory = dataSourceFactory; this.eventDispatcher = eventDispatcher; @@ -153,7 +153,7 @@ public final class HlsPlaylistTracker implements Loader.Callback(); initialPlaylistLoader = new Loader("HlsPlaylistTracker:MasterPlaylist"); - playlistParser = new HlsPlaylistParser(); + this.playlistParser = playlistParser; playlistBundles = new IdentityHashMap<>(); playlistRefreshHandler = new Handler(); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParser.java new file mode 100644 index 0000000000..bdd47e8c28 --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParser.java @@ -0,0 +1,39 @@ +package com.google.android.exoplayer2.source.hls.playlist; + +import android.net.Uri; + +import com.google.android.exoplayer2.upstream.ParsingLoadable; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Comparator; + +/** + * Parser for {@link HlsPlaylist}s that reorders the variants based on the comparator passed. + */ +public class ReorderingHlsPlaylistParser implements ParsingLoadable.Parser { + private final ParsingLoadable.Parser playlistParser; + private final Comparator variantComparator; + + /** + * @param playlistParser the {@link ParsingLoadable.Parser} to wrap. + * @param variantComparator the {@link Comparator} to use to reorder the variants. + * See {@link HlsMasterPlaylist#copyWithReorderedVariants(Comparator)} for more details. + */ + public ReorderingHlsPlaylistParser(ParsingLoadable.Parser playlistParser, + Comparator variantComparator) { + this.playlistParser = playlistParser; + this.variantComparator = variantComparator; + } + + @Override + public HlsPlaylist parse(Uri uri, InputStream inputStream) throws IOException { + final HlsPlaylist playlist = playlistParser.parse(uri, inputStream); + + if (playlist instanceof HlsMasterPlaylist) { + return ((HlsMasterPlaylist) playlist).copyWithReorderedVariants(variantComparator); + } + + return playlist; + } +}