mirror of
https://github.com/androidx/media.git
synced 2025-05-10 09:12:16 +08:00
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:
parent
706a6b83a9
commit
4a6a8553e9
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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<>();
|
||||||
|
@ -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
|
||||||
|
@ -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}.
|
||||||
*
|
*
|
||||||
|
Loading…
x
Reference in New Issue
Block a user