Skip to content

Commit

Permalink
Check #EXTM3U header is present in HLS playlists
Browse files Browse the repository at this point in the history
Issue:#2301

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=144334062
  • Loading branch information
AquilesCanta authored and ojw28 committed Jan 13, 2017
1 parent 706a6b8 commit 4a6a855
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
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;
Expand All @@ -29,70 +30,86 @@
*/
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<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 {
HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUri, inputStream);
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);
} 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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,22 @@
*/
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_STREAM_INF = "#EXT-X-STREAM-INF";
private static final String TAG_MEDIA = "#EXT-X-MEDIA";
Expand Down Expand Up @@ -97,6 +113,9 @@ public HlsPlaylist parse(Uri uri, InputStream inputStream) throws IOException {
Queue<String> extraLines = new LinkedList<>();
String line;
try {
if (!checkPlaylistHeader(reader)) {
throw new UnrecognizedInputFormatException(uri);
}
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty()) {
Expand Down Expand Up @@ -124,6 +143,35 @@ public HlsPlaylist parse(Uri uri, InputStream inputStream) throws IOException {
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)
throws IOException {
ArrayList<HlsMasterPlaylist.HlsUrl> variants = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ public String readLine() {
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
Expand Down
10 changes: 10 additions & 0 deletions library/src/main/java/com/google/android/exoplayer2/util/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,16 @@ public static byte[] getUtf8Bytes(String value) {
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}.
*
Expand Down

0 comments on commit 4a6a855

Please sign in to comment.