diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java index 488341d4f3..f0adf274ee 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java @@ -18,6 +18,7 @@ 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 java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.Charset; @@ -29,70 +30,86 @@ import junit.framework.TestCase; */ public class HlsMasterPlaylistParserTest extends TestCase { - public void testParseMasterPlaylist() { - Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); - String playlistString = "#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=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n" - + "http://example.com/spaces_in_codecs.m3u8\n" - + "\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=2560000,RESOLUTION=384x160\n" - + "http://example.com/mid.m3u8\n" - + "\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=7680000\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"; - ByteArrayInputStream inputStream = new ByteArrayInputStream( - playlistString.getBytes(Charset.forName(C.UTF8_NAME))); + private static final String PLAYLIST_URI = "https://example.com/test.m3u8"; + + 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=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n" + + "http://example.com/spaces_in_codecs.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=2560000,RESOLUTION=384x160\n" + + "http://example.com/mid.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=7680000\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"; + + private static final String PLAYLIST_WITH_INVALID_HEADER = "#EXTMU3\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + + "http://example.com/low.m3u8\n"; + + public void testParseMasterPlaylist() throws IOException{ + HlsPlaylist playlist = parsePlaylist(PLAYLIST_URI, MASTER_PLAYLIST); + assertNotNull(playlist); + assertEquals(HlsPlaylist.TYPE_MASTER, playlist.type); + + HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist; + + List variants = masterPlaylist.variants; + assertNotNull(variants); + assertEquals(5, variants.size()); + + assertEquals(1280000, variants.get(0).format.bitrate); + assertNotNull(variants.get(0).format.codecs); + assertEquals("mp4a.40.2,avc1.66.30", variants.get(0).format.codecs); + assertEquals(304, variants.get(0).format.width); + assertEquals(128, variants.get(0).format.height); + assertEquals("http://example.com/low.m3u8", variants.get(0).url); + + assertEquals(1280000, variants.get(1).format.bitrate); + assertNotNull(variants.get(1).format.codecs); + assertEquals("mp4a.40.2 , avc1.66.30 ", variants.get(1).format.codecs); + assertEquals("http://example.com/spaces_in_codecs.m3u8", variants.get(1).url); + + assertEquals(2560000, variants.get(2).format.bitrate); + assertEquals(null, variants.get(2).format.codecs); + assertEquals(384, variants.get(2).format.width); + assertEquals(160, variants.get(2).format.height); + assertEquals("http://example.com/mid.m3u8", variants.get(2).url); + + assertEquals(7680000, variants.get(3).format.bitrate); + assertEquals(null, variants.get(3).format.codecs); + assertEquals(Format.NO_VALUE, variants.get(3).format.width); + assertEquals(Format.NO_VALUE, variants.get(3).format.height); + assertEquals("http://example.com/hi.m3u8", variants.get(3).url); + + assertEquals(65000, variants.get(4).format.bitrate); + assertNotNull(variants.get(4).format.codecs); + assertEquals("mp4a.40.5", variants.get(4).format.codecs); + assertEquals(Format.NO_VALUE, variants.get(4).format.width); + assertEquals(Format.NO_VALUE, variants.get(4).format.height); + assertEquals("http://example.com/audio-only.m3u8", variants.get(4).url); + } + + public void testPlaylistWithInvalidHeader() throws IOException { try { - HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUri, inputStream); - assertNotNull(playlist); - assertEquals(HlsPlaylist.TYPE_MASTER, playlist.type); - - HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist; - - List variants = masterPlaylist.variants; - assertNotNull(variants); - assertEquals(5, variants.size()); - - assertEquals(1280000, variants.get(0).format.bitrate); - assertNotNull(variants.get(0).format.codecs); - assertEquals("mp4a.40.2,avc1.66.30", variants.get(0).format.codecs); - assertEquals(304, variants.get(0).format.width); - assertEquals(128, variants.get(0).format.height); - assertEquals("http://example.com/low.m3u8", variants.get(0).url); - - assertEquals(1280000, variants.get(1).format.bitrate); - assertNotNull(variants.get(1).format.codecs); - assertEquals("mp4a.40.2 , avc1.66.30 ", variants.get(1).format.codecs); - assertEquals("http://example.com/spaces_in_codecs.m3u8", variants.get(1).url); - - assertEquals(2560000, variants.get(2).format.bitrate); - assertEquals(null, variants.get(2).format.codecs); - assertEquals(384, variants.get(2).format.width); - assertEquals(160, variants.get(2).format.height); - assertEquals("http://example.com/mid.m3u8", variants.get(2).url); - - assertEquals(7680000, variants.get(3).format.bitrate); - assertEquals(null, variants.get(3).format.codecs); - assertEquals(Format.NO_VALUE, variants.get(3).format.width); - assertEquals(Format.NO_VALUE, variants.get(3).format.height); - assertEquals("http://example.com/hi.m3u8", variants.get(3).url); - - assertEquals(65000, variants.get(4).format.bitrate); - assertNotNull(variants.get(4).format.codecs); - assertEquals("mp4a.40.5", variants.get(4).format.codecs); - assertEquals(Format.NO_VALUE, variants.get(4).format.width); - assertEquals(Format.NO_VALUE, variants.get(4).format.height); - assertEquals("http://example.com/audio-only.m3u8", variants.get(4).url); - } catch (IOException exception) { - fail(exception.getMessage()); + parsePlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER); + fail("Expected exception not thrown."); + } catch (ParserException e) { + // Expected due to invalid header. } } + private static HlsPlaylist parsePlaylist(String uri, String playlistString) throws IOException { + Uri playlistUri = Uri.parse(uri); + ByteArrayInputStream inputStream = new ByteArrayInputStream( + playlistString.getBytes(Charset.forName(C.UTF8_NAME))); + return new HlsPlaylistParser().parse(playlistUri, inputStream); + } + } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 6e7a8d2766..460c4576bd 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -39,6 +39,22 @@ import java.util.regex.Pattern; */ public final class HlsPlaylistParser implements ParsingLoadable.Parser { + /** + * Thrown if the input does not start with an HLS playlist header. + */ + public static final class UnrecognizedInputFormatException extends ParserException { + + public final Uri inputUri; + + public UnrecognizedInputFormatException(Uri inputUri) { + super("Input does not start with the #EXTM3U header. Uri: " + inputUri); + this.inputUri = inputUri; + } + + } + + private static final String PLAYLIST_HEADER = "#EXTM3U"; + private static final String TAG_VERSION = "#EXT-X-VERSION"; private static final String TAG_STREAM_INF = "#EXT-X-STREAM-INF"; private static final String TAG_MEDIA = "#EXT-X-MEDIA"; @@ -97,6 +113,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser extraLines = new LinkedList<>(); String line; try { + if (!checkPlaylistHeader(reader)) { + throw new UnrecognizedInputFormatException(uri); + } while ((line = reader.readLine()) != null) { line = line.trim(); if (line.isEmpty()) { @@ -124,6 +143,35 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variants = new ArrayList<>(); diff --git a/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java b/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java index 89862d8c75..ef4aa05cfe 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java @@ -494,7 +494,7 @@ public final class ParsableByteArray { return null; } int lineLimit = position; - while (lineLimit < limit && data[lineLimit] != '\n' && data[lineLimit] != '\r') { + while (lineLimit < limit && !Util.isLinebreak(data[lineLimit])) { lineLimit++; } if (lineLimit - position >= 3 && data[position] == (byte) 0xEF diff --git a/library/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/src/main/java/com/google/android/exoplayer2/util/Util.java index 4477de7abb..e854c05165 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -254,6 +254,16 @@ public final class Util { return value.getBytes(Charset.defaultCharset()); // UTF-8 is the default on Android. } + /** + * Returns whether the given character is a carriage return ('\r') or a line feed ('\n'). + * + * @param c The character. + * @return Whether the given character is a linebreak. + */ + public static boolean isLinebreak(int c) { + return c == '\n' || c == '\r'; + } + /** * Converts text to lower case using {@link Locale#US}. *