Propagate codec information from EXT-X-STREAM-INF to EXT-X-MEDIA

This is the first CL in a series to add chunkless preparation support.

Also did a bit a tidying up in HlsSampleStreamWrappen and
HlsMasterPlaylistParserTest.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=174461737
This commit is contained in:
aquilescanta 2017-11-03 08:01:00 -07:00 committed by Oliver Woodman
parent dbe0e602ef
commit 4630fa2b4c
5 changed files with 136 additions and 81 deletions

View file

@ -749,6 +749,32 @@ public final class Util {
+ ") " + ExoPlayerLibraryInfo.VERSION_SLASHY;
}
/**
* Returns a copy of {@code codecs} without the codecs whose track type doesn't match
* {@code trackType}.
*
* @param codecs A codec sequence string, as defined in RFC 6381.
* @param trackType One of {@link C}{@code .TRACK_TYPE_*}.
* @return A copy of {@code codecs} without the codecs whose track type doesn't match
* {@code trackType}.
*/
public static String getCodecsOfType(String codecs, int trackType) {
if (TextUtils.isEmpty(codecs)) {
return null;
}
String[] codecArray = codecs.trim().split("(\\s*,\\s*)");
StringBuilder builder = new StringBuilder();
for (String codec : codecArray) {
if (trackType == MimeTypes.getTrackTypeOfCodec(codec)) {
if (builder.length() > 0) {
builder.append(",");
}
builder.append(codec);
}
}
return builder.length() > 0 ? builder.toString() : null;
}
/**
* Converts a sample bit depth to a corresponding PCM encoding constant.
*

View file

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.util;
import static com.google.android.exoplayer2.util.Util.binarySearchCeil;
import static com.google.android.exoplayer2.util.Util.binarySearchFloor;
import static com.google.android.exoplayer2.util.Util.escapeFileName;
import static com.google.android.exoplayer2.util.Util.getCodecsOfType;
import static com.google.android.exoplayer2.util.Util.parseXsDateTime;
import static com.google.android.exoplayer2.util.Util.parseXsDuration;
import static com.google.android.exoplayer2.util.Util.unescapeFileName;
@ -181,6 +182,18 @@ public class UtilTest {
assertThat(parseXsDateTime("2014-09-19T13:18:55.000-800")).isEqualTo(1411161535000L);
}
@Test
public void testGetCodecsOfType() {
assertThat(getCodecsOfType(null, C.TRACK_TYPE_VIDEO)).isNull();
assertThat(getCodecsOfType("avc1.64001e,vp9.63.1", C.TRACK_TYPE_AUDIO)).isNull();
assertThat(getCodecsOfType(" vp9.63.1, ec-3 ", C.TRACK_TYPE_AUDIO)).isEqualTo("ec-3");
assertThat(getCodecsOfType("avc1.61e, vp9.63.1, ec-3 ", C.TRACK_TYPE_VIDEO))
.isEqualTo("avc1.61e,vp9.63.1");
assertThat(getCodecsOfType("avc1.61e, vp9.63.1, ec-3 ", C.TRACK_TYPE_VIDEO))
.isEqualTo("avc1.61e,vp9.63.1");
assertThat(getCodecsOfType("invalidCodec1, invalidCodec2 ", C.TRACK_TYPE_AUDIO)).isNull();
}
@Test
public void testUnescapeInvalidFileName() {
assertThat(Util.unescapeFileName("%a")).isNull();

View file

@ -34,7 +34,7 @@ public class HlsMasterPlaylistParserTest extends TestCase {
private static final String PLAYLIST_URI = "https://example.com/test.m3u8";
private static final String MASTER_PLAYLIST = " #EXTM3U \n"
private static final String PLAYLIST_SIMPLE = " #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"
@ -51,7 +51,7 @@ public class HlsMasterPlaylistParserTest extends TestCase {
+ "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n"
+ "http://example.com/audio-only.m3u8";
private static final String AVG_BANDWIDTH_MASTER_PLAYLIST = " #EXTM3U \n"
private static final String PLAYLIST_WITH_AVG_BANDWIDTH = " #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"
@ -64,19 +64,33 @@ public class HlsMasterPlaylistParserTest extends TestCase {
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
+ "http://example.com/low.m3u8\n";
private static final String MASTER_PLAYLIST_WITH_CC = " #EXTM3U \n"
private static final String PLAYLIST_WITH_CC = " #EXTM3U \n"
+ "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
+ "http://example.com/low.m3u8\n";
private static final String MASTER_PLAYLIST_WITHOUT_CC = " #EXTM3U \n"
private static final String PLAYLIST_WITHOUT_CC = " #EXTM3U \n"
+ "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128,"
+ "CLOSED-CAPTIONS=NONE\n"
+ "http://example.com/low.m3u8\n";
private static final String PLAYLIST_WITH_AUDIO_MEDIA_TAG = "#EXTM3U\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=2227464,CODECS=\"avc1.640020,mp4a.40.2\",AUDIO=\"aud1\"\n"
+ "uri1.m3u8\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=8178040,CODECS=\"avc1.64002a,mp4a.40.2\",AUDIO=\"aud1\"\n"
+ "uri2.m3u8\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=2448841,CODECS=\"avc1.640020,ac-3\",AUDIO=\"aud2\"\n"
+ "uri1.m3u8\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=8399417,CODECS=\"avc1.64002a,ac-3\",AUDIO=\"aud2\"\n"
+ "uri2.m3u8\n"
+ "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud1\",LANGUAGE=\"en\",NAME=\"English\","
+ "AUTOSELECT=YES,DEFAULT=YES,CHANNELS=\"2\",URI=\"a1/prog_index.m3u8\"\n"
+ "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud2\",LANGUAGE=\"en\",NAME=\"English\","
+ "AUTOSELECT=YES,DEFAULT=YES,CHANNELS=\"6\",URI=\"a2/prog_index.m3u8\"\n";
public void testParseMasterPlaylist() throws IOException{
HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST);
HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE);
List<HlsMasterPlaylist.HlsUrl> variants = masterPlaylist.variants;
assertEquals(5, variants.size());
@ -116,7 +130,7 @@ public class HlsMasterPlaylistParserTest extends TestCase {
public void testMasterPlaylistWithBandwdithAverage() throws IOException {
HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI,
AVG_BANDWIDTH_MASTER_PLAYLIST);
PLAYLIST_WITH_AVG_BANDWIDTH);
List<HlsMasterPlaylist.HlsUrl> variants = masterPlaylist.variants;
@ -134,7 +148,7 @@ public class HlsMasterPlaylistParserTest extends TestCase {
}
public void testPlaylistWithClosedCaption() throws IOException {
HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST_WITH_CC);
HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_CC);
assertEquals(1, playlist.muxedCaptionFormats.size());
Format closedCaptionFormat = playlist.muxedCaptionFormats.get(0);
assertEquals(MimeTypes.APPLICATION_CEA708, closedCaptionFormat.sampleMimeType);
@ -143,10 +157,22 @@ public class HlsMasterPlaylistParserTest extends TestCase {
}
public void testPlaylistWithoutClosedCaptions() throws IOException {
HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST_WITHOUT_CC);
HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITHOUT_CC);
assertEquals(Collections.emptyList(), playlist.muxedCaptionFormats);
}
public void testCodecPropagation() throws IOException {
HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_AUDIO_MEDIA_TAG);
Format firstAudioFormat = playlist.audios.get(0).format;
assertEquals("mp4a.40.2", firstAudioFormat.codecs);
assertEquals(MimeTypes.AUDIO_AAC, firstAudioFormat.sampleMimeType);
Format secondAudioFormat = playlist.audios.get(1).format;
assertEquals("ac-3", secondAudioFormat.codecs);
assertEquals(MimeTypes.AUDIO_AC3, secondAudioFormat.sampleMimeType);
}
private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString)
throws IOException {
Uri playlistUri = Uri.parse(uri);

View file

@ -16,7 +16,6 @@
package com.google.android.exoplayer2.source.hls;
import android.os.Handler;
import android.text.TextUtils;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
@ -748,13 +747,8 @@ import java.util.LinkedList;
if (containerFormat == null) {
return sampleFormat;
}
String codecs = null;
int sampleTrackType = MimeTypes.getTrackType(sampleFormat.sampleMimeType);
if (sampleTrackType == C.TRACK_TYPE_AUDIO) {
codecs = getAudioCodecs(containerFormat.codecs);
} else if (sampleTrackType == C.TRACK_TYPE_VIDEO) {
codecs = getVideoCodecs(containerFormat.codecs);
}
String codecs = Util.getCodecsOfType(containerFormat.codecs, sampleTrackType);
return sampleFormat.copyWithContainerInfo(containerFormat.id, codecs, containerFormat.bitrate,
containerFormat.width, containerFormat.height, containerFormat.selectionFlags,
containerFormat.language);
@ -793,29 +787,4 @@ import java.util.LinkedList;
return true;
}
private static String getAudioCodecs(String codecs) {
return getCodecsOfType(codecs, C.TRACK_TYPE_AUDIO);
}
private static String getVideoCodecs(String codecs) {
return getCodecsOfType(codecs, C.TRACK_TYPE_VIDEO);
}
private static String getCodecsOfType(String codecs, int trackType) {
if (TextUtils.isEmpty(codecs)) {
return null;
}
String[] codecArray = codecs.split("(\\s*,\\s*)|(\\s*$)");
StringBuilder builder = new StringBuilder();
for (String codec : codecArray) {
if (trackType == MimeTypes.getTrackTypeOfCodec(codec)) {
if (builder.length() > 0) {
builder.append(",");
}
builder.append(codec);
}
}
return builder.length() > 0 ? builder.toString() : null;
}
}

View file

@ -35,6 +35,7 @@ import java.io.UnsupportedEncodingException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Queue;
@ -88,6 +89,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
private static final Pattern REGEX_AVERAGE_BANDWIDTH =
Pattern.compile("AVERAGE-BANDWIDTH=(\\d+)\\b");
private static final Pattern REGEX_AUDIO = Pattern.compile("AUDIO=\"(.+?)\"");
private static final Pattern REGEX_BANDWIDTH = Pattern.compile("[^-]BANDWIDTH=(\\d+)\\b");
private static final Pattern REGEX_CODECS = Pattern.compile("CODECS=\"(.+?)\"");
private static final Pattern REGEX_RESOLUTION = Pattern.compile("RESOLUTION=(\\d+x\\d+)");
@ -115,6 +117,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
+ "|" + TYPE_SUBTITLES + "|" + TYPE_CLOSED_CAPTIONS + ")");
private static final Pattern REGEX_LANGUAGE = Pattern.compile("LANGUAGE=\"(.+?)\"");
private static final Pattern REGEX_NAME = Pattern.compile("NAME=\"(.+?)\"");
private static final Pattern REGEX_GROUP_ID = Pattern.compile("GROUP-ID=\"(.+?)\"");
private static final Pattern REGEX_INSTREAM_ID =
Pattern.compile("INSTREAM-ID=\"((?:CC|SERVICE)\\d+)\"");
private static final Pattern REGEX_AUTOSELECT = compileBooleanAttrPattern("AUTOSELECT");
@ -190,9 +193,11 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, String baseUri)
throws IOException {
HashSet<String> variantUrls = new HashSet<>();
HashMap<String, String> audioGroupIdToCodecs = new HashMap<>();
ArrayList<HlsMasterPlaylist.HlsUrl> variants = new ArrayList<>();
ArrayList<HlsMasterPlaylist.HlsUrl> audios = new ArrayList<>();
ArrayList<HlsMasterPlaylist.HlsUrl> subtitles = new ArrayList<>();
ArrayList<String> mediaTags = new ArrayList<>();
ArrayList<String> tags = new ArrayList<>();
Format muxedAudioFormat = null;
List<Format> muxedCaptionFormats = null;
@ -208,47 +213,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
}
if (line.startsWith(TAG_MEDIA)) {
@C.SelectionFlags int selectionFlags = parseSelectionFlags(line);
String uri = parseOptionalStringAttr(line, REGEX_URI);
String id = parseStringAttr(line, REGEX_NAME);
String language = parseOptionalStringAttr(line, REGEX_LANGUAGE);
Format format;
switch (parseStringAttr(line, REGEX_TYPE)) {
case TYPE_AUDIO:
format = Format.createAudioContainerFormat(id, MimeTypes.APPLICATION_M3U8, null, null,
Format.NO_VALUE, Format.NO_VALUE, Format.NO_VALUE, null, selectionFlags, language);
if (uri == null) {
muxedAudioFormat = format;
} else {
audios.add(new HlsMasterPlaylist.HlsUrl(uri, format));
}
break;
case TYPE_SUBTITLES:
format = Format.createTextContainerFormat(id, MimeTypes.APPLICATION_M3U8,
MimeTypes.TEXT_VTT, null, Format.NO_VALUE, selectionFlags, language);
subtitles.add(new HlsMasterPlaylist.HlsUrl(uri, format));
break;
case TYPE_CLOSED_CAPTIONS:
String instreamId = parseStringAttr(line, REGEX_INSTREAM_ID);
String mimeType;
int accessibilityChannel;
if (instreamId.startsWith("CC")) {
mimeType = MimeTypes.APPLICATION_CEA608;
accessibilityChannel = Integer.parseInt(instreamId.substring(2));
} else /* starts with SERVICE */ {
mimeType = MimeTypes.APPLICATION_CEA708;
accessibilityChannel = Integer.parseInt(instreamId.substring(7));
}
if (muxedCaptionFormats == null) {
muxedCaptionFormats = new ArrayList<>();
}
muxedCaptionFormats.add(Format.createTextContainerFormat(id, null, mimeType, null,
Format.NO_VALUE, selectionFlags, language, accessibilityChannel));
break;
default:
// Do nothing.
break;
}
// Media tags are parsed at the end to include codec information from #EXT-X-STREAM-INF
// tags.
mediaTags.add(line);
} else if (line.startsWith(TAG_STREAM_INF)) {
noClosedCaptions |= line.contains(ATTR_CLOSED_CAPTIONS_NONE);
int bitrate = parseIntAttr(line, REGEX_BANDWIDTH);
@ -279,6 +246,10 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
if (frameRateString != null) {
frameRate = Float.parseFloat(frameRateString);
}
String audioGroupId = parseOptionalStringAttr(line, REGEX_AUDIO);
if (audioGroupId != null && codecs != null) {
audioGroupIdToCodecs.put(audioGroupId, Util.getCodecsOfType(codecs, C.TRACK_TYPE_AUDIO));
}
line = iterator.next(); // #EXT-X-STREAM-INF's URI.
if (variantUrls.add(line)) {
Format format = Format.createVideoContainerFormat(Integer.toString(variants.size()),
@ -287,6 +258,56 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
}
}
}
for (int i = 0; i < mediaTags.size(); i++) {
line = mediaTags.get(i);
@C.SelectionFlags int selectionFlags = parseSelectionFlags(line);
String uri = parseOptionalStringAttr(line, REGEX_URI);
String id = parseStringAttr(line, REGEX_NAME);
String language = parseOptionalStringAttr(line, REGEX_LANGUAGE);
String groupId = parseOptionalStringAttr(line, REGEX_GROUP_ID);
Format format;
switch (parseStringAttr(line, REGEX_TYPE)) {
case TYPE_AUDIO:
String codecs = audioGroupIdToCodecs.get(groupId);
String sampleMimeType = codecs != null ? MimeTypes.getMediaMimeType(codecs) : null;
format = Format.createAudioContainerFormat(id, MimeTypes.APPLICATION_M3U8, sampleMimeType,
codecs, Format.NO_VALUE, Format.NO_VALUE, Format.NO_VALUE, null, selectionFlags,
language);
if (uri == null) {
muxedAudioFormat = format;
} else {
audios.add(new HlsMasterPlaylist.HlsUrl(uri, format));
}
break;
case TYPE_SUBTITLES:
format = Format.createTextContainerFormat(id, MimeTypes.APPLICATION_M3U8,
MimeTypes.TEXT_VTT, null, Format.NO_VALUE, selectionFlags, language);
subtitles.add(new HlsMasterPlaylist.HlsUrl(uri, format));
break;
case TYPE_CLOSED_CAPTIONS:
String instreamId = parseStringAttr(line, REGEX_INSTREAM_ID);
String mimeType;
int accessibilityChannel;
if (instreamId.startsWith("CC")) {
mimeType = MimeTypes.APPLICATION_CEA608;
accessibilityChannel = Integer.parseInt(instreamId.substring(2));
} else /* starts with SERVICE */ {
mimeType = MimeTypes.APPLICATION_CEA708;
accessibilityChannel = Integer.parseInt(instreamId.substring(7));
}
if (muxedCaptionFormats == null) {
muxedCaptionFormats = new ArrayList<>();
}
muxedCaptionFormats.add(Format.createTextContainerFormat(id, null, mimeType, null,
Format.NO_VALUE, selectionFlags, language, accessibilityChannel));
break;
default:
// Do nothing.
break;
}
}
if (noClosedCaptions) {
muxedCaptionFormats = Collections.emptyList();
}