Merge pull request #7184 from TiVo:p-subtitle-format-from-codecs

PiperOrigin-RevId: 305137114
This commit is contained in:
Oliver Woodman 2020-04-07 13:38:53 +01:00
commit 2b44ff3c71
5 changed files with 77 additions and 14 deletions

View File

@ -88,6 +88,8 @@
`OfflineLicenseHelper` `OfflineLicenseHelper`
([#7078](https://github.com/google/ExoPlayer/issues/7078)). ([#7078](https://github.com/google/ExoPlayer/issues/7078)).
* Remove generics from DRM components. * Remove generics from DRM components.
* HLS: Recognize IMSC subtitles
([#7185](https://github.com/google/ExoPlayer/issues/7185)).
* Downloads: Merge downloads in `SegmentDownloader` to improve overall * Downloads: Merge downloads in `SegmentDownloader` to improve overall
download speed ([#5978](https://github.com/google/ExoPlayer/issues/5978)). download speed ([#5978](https://github.com/google/ExoPlayer/issues/5978)).
* MP3: Add `IndexSeeker` for accurate seeks in VBR streams * MP3: Add `IndexSeeker` for accurate seeks in VBR streams

View File

@ -123,22 +123,22 @@ public final class MimeTypes {
customMimeTypes.add(customMimeType); customMimeTypes.add(customMimeType);
} }
/** Returns whether the given string is an audio mime type. */ /** Returns whether the given string is an audio MIME type. */
public static boolean isAudio(@Nullable String mimeType) { public static boolean isAudio(@Nullable String mimeType) {
return BASE_TYPE_AUDIO.equals(getTopLevelType(mimeType)); return BASE_TYPE_AUDIO.equals(getTopLevelType(mimeType));
} }
/** Returns whether the given string is a video mime type. */ /** Returns whether the given string is a video MIME type. */
public static boolean isVideo(@Nullable String mimeType) { public static boolean isVideo(@Nullable String mimeType) {
return BASE_TYPE_VIDEO.equals(getTopLevelType(mimeType)); return BASE_TYPE_VIDEO.equals(getTopLevelType(mimeType));
} }
/** Returns whether the given string is a text mime type. */ /** Returns whether the given string is a text MIME type. */
public static boolean isText(@Nullable String mimeType) { public static boolean isText(@Nullable String mimeType) {
return BASE_TYPE_TEXT.equals(getTopLevelType(mimeType)); return BASE_TYPE_TEXT.equals(getTopLevelType(mimeType));
} }
/** Returns whether the given string is an application mime type. */ /** Returns whether the given string is an application MIME type. */
public static boolean isApplication(@Nullable String mimeType) { public static boolean isApplication(@Nullable String mimeType) {
return BASE_TYPE_APPLICATION.equals(getTopLevelType(mimeType)); return BASE_TYPE_APPLICATION.equals(getTopLevelType(mimeType));
} }
@ -174,13 +174,14 @@ public final class MimeTypes {
* @param codecs The codecs attribute. * @param codecs The codecs attribute.
* @return The derived video mimeType, or null if it could not be derived. * @return The derived video mimeType, or null if it could not be derived.
*/ */
public static @Nullable String getVideoMediaMimeType(@Nullable String codecs) { @Nullable
public static String getVideoMediaMimeType(@Nullable String codecs) {
if (codecs == null) { if (codecs == null) {
return null; return null;
} }
String[] codecList = Util.splitCodecs(codecs); String[] codecList = Util.splitCodecs(codecs);
for (String codec : codecList) { for (String codec : codecList) {
String mimeType = getMediaMimeType(codec); @Nullable String mimeType = getMediaMimeType(codec);
if (mimeType != null && isVideo(mimeType)) { if (mimeType != null && isVideo(mimeType)) {
return mimeType; return mimeType;
} }
@ -194,13 +195,14 @@ public final class MimeTypes {
* @param codecs The codecs attribute. * @param codecs The codecs attribute.
* @return The derived audio mimeType, or null if it could not be derived. * @return The derived audio mimeType, or null if it could not be derived.
*/ */
public static @Nullable String getAudioMediaMimeType(@Nullable String codecs) { @Nullable
public static String getAudioMediaMimeType(@Nullable String codecs) {
if (codecs == null) { if (codecs == null) {
return null; return null;
} }
String[] codecList = Util.splitCodecs(codecs); String[] codecList = Util.splitCodecs(codecs);
for (String codec : codecList) { for (String codec : codecList) {
String mimeType = getMediaMimeType(codec); @Nullable String mimeType = getMediaMimeType(codec);
if (mimeType != null && isAudio(mimeType)) { if (mimeType != null && isAudio(mimeType)) {
return mimeType; return mimeType;
} }
@ -214,7 +216,8 @@ public final class MimeTypes {
* @param codec The codec identifier to derive. * @param codec The codec identifier to derive.
* @return The mimeType, or null if it could not be derived. * @return The mimeType, or null if it could not be derived.
*/ */
public static @Nullable String getMediaMimeType(@Nullable String codec) { @Nullable
public static String getMediaMimeType(@Nullable String codec) {
if (codec == null) { if (codec == null) {
return null; return null;
} }
@ -235,7 +238,7 @@ public final class MimeTypes {
} else if (codec.startsWith("vp8") || codec.startsWith("vp08")) { } else if (codec.startsWith("vp8") || codec.startsWith("vp08")) {
return MimeTypes.VIDEO_VP8; return MimeTypes.VIDEO_VP8;
} else if (codec.startsWith("mp4a")) { } else if (codec.startsWith("mp4a")) {
String mimeType = null; @Nullable String mimeType = null;
if (codec.startsWith("mp4a.")) { if (codec.startsWith("mp4a.")) {
String objectTypeString = codec.substring(5); // remove the 'mp4a.' prefix String objectTypeString = codec.substring(5); // remove the 'mp4a.' prefix
if (objectTypeString.length() >= 2) { if (objectTypeString.length() >= 2) {
@ -244,7 +247,7 @@ public final class MimeTypes {
int objectTypeInt = Integer.parseInt(objectTypeHexString, 16); int objectTypeInt = Integer.parseInt(objectTypeHexString, 16);
mimeType = getMimeTypeFromMp4ObjectType(objectTypeInt); mimeType = getMimeTypeFromMp4ObjectType(objectTypeInt);
} catch (NumberFormatException ignored) { } catch (NumberFormatException ignored) {
// ignored // Ignored.
} }
} }
} }
@ -267,6 +270,10 @@ public final class MimeTypes {
return MimeTypes.AUDIO_VORBIS; return MimeTypes.AUDIO_VORBIS;
} else if (codec.startsWith("flac")) { } else if (codec.startsWith("flac")) {
return MimeTypes.AUDIO_FLAC; return MimeTypes.AUDIO_FLAC;
} else if (codec.startsWith("stpp")) {
return MimeTypes.APPLICATION_TTML;
} else if (codec.startsWith("wvtt")) {
return MimeTypes.TEXT_VTT;
} else { } else {
return getCustomMimeTypeForCodec(codec); return getCustomMimeTypeForCodec(codec);
} }
@ -406,7 +413,8 @@ public final class MimeTypes {
* Returns the top-level type of {@code mimeType}, or null if {@code mimeType} is null or does not * Returns the top-level type of {@code mimeType}, or null if {@code mimeType} is null or does not
* contain a forward slash character ({@code '/'}). * contain a forward slash character ({@code '/'}).
*/ */
private static @Nullable String getTopLevelType(@Nullable String mimeType) { @Nullable
private static String getTopLevelType(@Nullable String mimeType) {
if (mimeType == null) { if (mimeType == null) {
return null; return null;
} }
@ -417,7 +425,8 @@ public final class MimeTypes {
return mimeType.substring(0, indexOfSlash); return mimeType.substring(0, indexOfSlash);
} }
private static @Nullable String getCustomMimeTypeForCodec(String codec) { @Nullable
private static String getCustomMimeTypeForCodec(String codec) {
int customMimeTypeCount = customMimeTypes.size(); int customMimeTypeCount = customMimeTypes.size();
for (int i = 0; i < customMimeTypeCount; i++) { for (int i = 0; i < customMimeTypeCount; i++) {
CustomMimeType customMimeType = customMimeTypes.get(i); CustomMimeType customMimeType = customMimeTypes.get(i);

View File

@ -73,6 +73,10 @@ public final class MimeTypesTest {
assertThat(MimeTypes.getMediaMimeType("mp4a.AA")).isEqualTo(MimeTypes.AUDIO_DTS_HD); assertThat(MimeTypes.getMediaMimeType("mp4a.AA")).isEqualTo(MimeTypes.AUDIO_DTS_HD);
assertThat(MimeTypes.getMediaMimeType("mp4a.AB")).isEqualTo(MimeTypes.AUDIO_DTS_HD); assertThat(MimeTypes.getMediaMimeType("mp4a.AB")).isEqualTo(MimeTypes.AUDIO_DTS_HD);
assertThat(MimeTypes.getMediaMimeType("mp4a.AD")).isEqualTo(MimeTypes.AUDIO_OPUS); assertThat(MimeTypes.getMediaMimeType("mp4a.AD")).isEqualTo(MimeTypes.AUDIO_OPUS);
assertThat(MimeTypes.getMediaMimeType("wvtt")).isEqualTo(MimeTypes.TEXT_VTT);
assertThat(MimeTypes.getMediaMimeType("stpp.")).isEqualTo(MimeTypes.APPLICATION_TTML);
assertThat(MimeTypes.getMediaMimeType("stpp.ttml.im1t")).isEqualTo(MimeTypes.APPLICATION_TTML);
} }
@Test @Test

View File

@ -458,7 +458,18 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
} }
break; break;
case TYPE_SUBTITLES: case TYPE_SUBTITLES:
formatBuilder.setSampleMimeType(MimeTypes.TEXT_VTT).setMetadata(metadata); sampleMimeType = null;
variant = getVariantWithSubtitleGroup(variants, groupId);
if (variant != null) {
@Nullable
String codecs = Util.getCodecsOfType(variant.format.codecs, C.TRACK_TYPE_TEXT);
formatBuilder.setCodecs(codecs);
sampleMimeType = MimeTypes.getMediaMimeType(codecs);
}
if (sampleMimeType == null) {
sampleMimeType = MimeTypes.TEXT_VTT;
}
formatBuilder.setSampleMimeType(sampleMimeType).setMetadata(metadata);
subtitles.add(new Rendition(uri, formatBuilder.build(), groupId, name)); subtitles.add(new Rendition(uri, formatBuilder.build(), groupId, name));
break; break;
case TYPE_CLOSED_CAPTIONS: case TYPE_CLOSED_CAPTIONS:
@ -527,6 +538,17 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
return null; return null;
} }
@Nullable
private static Variant getVariantWithSubtitleGroup(ArrayList<Variant> variants, String groupId) {
for (int i = 0; i < variants.size(); i++) {
Variant variant = variants.get(i);
if (groupId.equals(variant.subtitleGroupId)) {
return variant;
}
}
return null;
}
private static HlsMediaPlaylist parseMediaPlaylist( private static HlsMediaPlaylist parseMediaPlaylist(
HlsMasterPlaylist masterPlaylist, LineIterator iterator, String baseUri) throws IOException { HlsMasterPlaylist masterPlaylist, LineIterator iterator, String baseUri) throws IOException {
@HlsMediaPlaylist.PlaylistType int playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_UNKNOWN; @HlsMediaPlaylist.PlaylistType int playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_UNKNOWN;

View File

@ -194,6 +194,19 @@ public class HlsMasterPlaylistParserTest {
+ "#EXT-X-MEDIA:TYPE=SUBTITLES," + "#EXT-X-MEDIA:TYPE=SUBTITLES,"
+ "GROUP-ID=\"sub1\",NAME=\"English\",URI=\"s1/en/prog_index.m3u8\"\n"; + "GROUP-ID=\"sub1\",NAME=\"English\",URI=\"s1/en/prog_index.m3u8\"\n";
private static final String PLAYLIST_WITH_TTML_SUBTITLE =
" #EXTM3U\n"
+ "\n"
+ "#EXT-X-VERSION:6\n"
+ "\n"
+ "#EXT-X-INDEPENDENT-SEGMENTS\n"
+ "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"stpp.ttml.im1t,mp4a.40.2,avc1.66.30\",RESOLUTION=304x128,AUDIO=\"aud1\",SUBTITLES=\"sub1\"\n"
+ "http://example.com/low.m3u8\n"
+ "\n"
+ "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud1\",NAME=\"English\",URI=\"a1/index.m3u8\"\n"
+ "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"sub1\",NAME=\"English\",AUTOSELECT=YES,DEFAULT=YES,URI=\"s1/en/prog_index.m3u8\"\n";
@Test @Test
public void parseMasterPlaylist_withSimple_success() throws IOException { public void parseMasterPlaylist_withSimple_success() throws IOException {
HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE); HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE);
@ -321,6 +334,7 @@ public class HlsMasterPlaylistParserTest {
Format firstTextFormat = playlist.subtitles.get(0).format; Format firstTextFormat = playlist.subtitles.get(0).format;
assertThat(firstTextFormat.id).isEqualTo("sub1:Eng"); assertThat(firstTextFormat.id).isEqualTo("sub1:Eng");
assertThat(firstTextFormat.sampleMimeType).isEqualTo(MimeTypes.TEXT_VTT);
} }
@Test @Test
@ -345,6 +359,18 @@ public class HlsMasterPlaylistParserTest {
.isEqualTo(Uri.parse("http://example.com/This/{$nested}/reference/shouldnt/work")); .isEqualTo(Uri.parse("http://example.com/This/{$nested}/reference/shouldnt/work"));
} }
@Test
public void parseMasterPlaylist_withTtmlSubtitle() throws IOException {
HlsMasterPlaylist playlistWithTtmlSubtitle =
parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_TTML_SUBTITLE);
HlsMasterPlaylist.Variant variant = playlistWithTtmlSubtitle.variants.get(0);
Format firstTextFormat = playlistWithTtmlSubtitle.subtitles.get(0).format;
assertThat(firstTextFormat.id).isEqualTo("sub1:English");
assertThat(firstTextFormat.containerMimeType).isEqualTo(MimeTypes.APPLICATION_M3U8);
assertThat(firstTextFormat.sampleMimeType).isEqualTo(MimeTypes.APPLICATION_TTML);
assertThat(variant.format.codecs).isEqualTo("stpp.ttml.im1t,mp4a.40.2,avc1.66.30");
}
@Test @Test
public void parseMasterPlaylist_withMatchingStreamInfUrls_success() throws IOException { public void parseMasterPlaylist_withMatchingStreamInfUrls_success() throws IOException {
HlsMasterPlaylist playlist = HlsMasterPlaylist playlist =