Check #EXTM3U header is present in HLS playlists

Issue:#2301

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=144334062
This commit is contained in:
aquilescanta 2017-01-12 09:27:11 -08:00 committed by Oliver Woodman
parent 706a6b83a9
commit 4a6a8553e9
4 changed files with 138 additions and 63 deletions

View File

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.hls.playlist;
import android.net.Uri; import android.net.Uri;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
@ -29,70 +30,86 @@ import junit.framework.TestCase;
*/ */
public class HlsMasterPlaylistParserTest extends TestCase { public class HlsMasterPlaylistParserTest extends TestCase {
public void testParseMasterPlaylist() { private static final String PLAYLIST_URI = "https://example.com/test.m3u8";
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
String playlistString = "#EXTM3U\n" private static final String MASTER_PLAYLIST = " #EXTM3U \n"
+ "\n" + "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
+ "http://example.com/low.m3u8\n" + "http://example.com/low.m3u8\n"
+ "\n" + "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n" + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n"
+ "http://example.com/spaces_in_codecs.m3u8\n" + "http://example.com/spaces_in_codecs.m3u8\n"
+ "\n" + "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=2560000,RESOLUTION=384x160\n" + "#EXT-X-STREAM-INF:BANDWIDTH=2560000,RESOLUTION=384x160\n"
+ "http://example.com/mid.m3u8\n" + "http://example.com/mid.m3u8\n"
+ "\n" + "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=7680000\n" + "#EXT-X-STREAM-INF:BANDWIDTH=7680000\n"
+ "http://example.com/hi.m3u8\n" + "http://example.com/hi.m3u8\n"
+ "\n" + "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n" + "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n"
+ "http://example.com/audio-only.m3u8"; + "http://example.com/audio-only.m3u8";
ByteArrayInputStream inputStream = new ByteArrayInputStream(
playlistString.getBytes(Charset.forName(C.UTF8_NAME))); 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<HlsMasterPlaylist.HlsUrl> 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 { try {
HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUri, inputStream); parsePlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER);
assertNotNull(playlist); fail("Expected exception not thrown.");
assertEquals(HlsPlaylist.TYPE_MASTER, playlist.type); } catch (ParserException e) {
// Expected due to invalid header.
HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist;
List<HlsMasterPlaylist.HlsUrl> 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());
} }
} }
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);
}
} }

View File

@ -39,6 +39,22 @@ import java.util.regex.Pattern;
*/ */
public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlaylist> { public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlaylist> {
/**
* 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_VERSION = "#EXT-X-VERSION";
private static final String TAG_STREAM_INF = "#EXT-X-STREAM-INF"; private static final String TAG_STREAM_INF = "#EXT-X-STREAM-INF";
private static final String TAG_MEDIA = "#EXT-X-MEDIA"; private static final String TAG_MEDIA = "#EXT-X-MEDIA";
@ -97,6 +113,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
Queue<String> extraLines = new LinkedList<>(); Queue<String> extraLines = new LinkedList<>();
String line; String line;
try { try {
if (!checkPlaylistHeader(reader)) {
throw new UnrecognizedInputFormatException(uri);
}
while ((line = reader.readLine()) != null) { while ((line = reader.readLine()) != null) {
line = line.trim(); line = line.trim();
if (line.isEmpty()) { if (line.isEmpty()) {
@ -124,6 +143,35 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
throw new ParserException("Failed to parse the playlist, could not identify any tags."); throw new ParserException("Failed to parse the playlist, could not identify any tags.");
} }
private static boolean checkPlaylistHeader(BufferedReader reader) throws IOException {
int last = reader.read();
if (last == 0xEF) {
if (reader.read() != 0xBB || reader.read() != 0xBF) {
return false;
}
// The playlist contains a Byte Order Mark, which gets discarded.
last = reader.read();
}
last = skipIgnorableWhitespace(reader, true, last);
int playlistHeaderLength = PLAYLIST_HEADER.length();
for (int i = 0; i < playlistHeaderLength; i++) {
if (last != PLAYLIST_HEADER.charAt(i)) {
return false;
}
last = reader.read();
}
last = skipIgnorableWhitespace(reader, false, last);
return Util.isLinebreak(last);
}
private static int skipIgnorableWhitespace(BufferedReader reader, boolean skipLinebreaks, int c)
throws IOException {
while (c != -1 && Character.isWhitespace(c) && (skipLinebreaks || !Util.isLinebreak(c))) {
c = reader.read();
}
return c;
}
private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, String baseUri) private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, String baseUri)
throws IOException { throws IOException {
ArrayList<HlsMasterPlaylist.HlsUrl> variants = new ArrayList<>(); ArrayList<HlsMasterPlaylist.HlsUrl> variants = new ArrayList<>();

View File

@ -494,7 +494,7 @@ public final class ParsableByteArray {
return null; return null;
} }
int lineLimit = position; int lineLimit = position;
while (lineLimit < limit && data[lineLimit] != '\n' && data[lineLimit] != '\r') { while (lineLimit < limit && !Util.isLinebreak(data[lineLimit])) {
lineLimit++; lineLimit++;
} }
if (lineLimit - position >= 3 && data[position] == (byte) 0xEF if (lineLimit - position >= 3 && data[position] == (byte) 0xEF

View File

@ -254,6 +254,16 @@ public final class Util {
return value.getBytes(Charset.defaultCharset()); // UTF-8 is the default on Android. 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}. * Converts text to lower case using {@link Locale#US}.
* *