Commit 49858b82 by Oliver Woodman

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

PiperOrigin-RevId: 305137114
parent cc29798d
...@@ -37,6 +37,8 @@ ...@@ -37,6 +37,8 @@
* Update the manifest URI to avoid repeated HTTP redirects * Update the manifest URI to avoid repeated HTTP redirects
([#6907](https://github.com/google/ExoPlayer/issues/6907)). ([#6907](https://github.com/google/ExoPlayer/issues/6907)).
* Parse period `AssetIdentifier` elements. * Parse period `AssetIdentifier` elements.
* HLS: Recognize IMSC subtitles
([#7185](https://github.com/google/ExoPlayer/issues/7185)).
* UI: Add an option to set whether to use the orientation sensor for rotation * UI: Add an option to set whether to use the orientation sensor for rotation
in spherical playbacks in spherical playbacks
([#6761](https://github.com/google/ExoPlayer/issues/6761)). ([#6761](https://github.com/google/ExoPlayer/issues/6761)).
......
...@@ -122,22 +122,22 @@ public final class MimeTypes { ...@@ -122,22 +122,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));
} }
...@@ -173,13 +173,14 @@ public final class MimeTypes { ...@@ -173,13 +173,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;
} }
...@@ -193,13 +194,14 @@ public final class MimeTypes { ...@@ -193,13 +194,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;
} }
...@@ -213,7 +215,8 @@ public final class MimeTypes { ...@@ -213,7 +215,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;
} }
...@@ -234,7 +237,7 @@ public final class MimeTypes { ...@@ -234,7 +237,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) {
...@@ -243,7 +246,7 @@ public final class MimeTypes { ...@@ -243,7 +246,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.
} }
} }
} }
...@@ -266,6 +269,10 @@ public final class MimeTypes { ...@@ -266,6 +269,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);
} }
...@@ -405,7 +412,8 @@ public final class MimeTypes { ...@@ -405,7 +412,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;
} }
...@@ -416,7 +424,8 @@ public final class MimeTypes { ...@@ -416,7 +424,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);
......
...@@ -73,6 +73,10 @@ public final class MimeTypesTest { ...@@ -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
......
...@@ -480,19 +480,28 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli ...@@ -480,19 +480,28 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
} }
break; break;
case TYPE_SUBTITLES: case TYPE_SUBTITLES:
codecs = null;
sampleMimeType = null;
variant = getVariantWithSubtitleGroup(variants, groupId);
if (variant != null) {
codecs = Util.getCodecsOfType(variant.format.codecs, C.TRACK_TYPE_TEXT);
sampleMimeType = MimeTypes.getMediaMimeType(codecs);
}
if (sampleMimeType == null) {
sampleMimeType = MimeTypes.TEXT_VTT;
}
format = format =
Format.createTextContainerFormat( Format.createTextContainerFormat(
/* id= */ formatId, /* id= */ formatId,
/* label= */ name, /* label= */ name,
/* containerMimeType= */ MimeTypes.APPLICATION_M3U8, /* containerMimeType= */ MimeTypes.APPLICATION_M3U8,
/* sampleMimeType= */ MimeTypes.TEXT_VTT, sampleMimeType,
/* codecs= */ null, codecs,
/* bitrate= */ Format.NO_VALUE, /* bitrate= */ Format.NO_VALUE,
selectionFlags, selectionFlags,
roleFlags, roleFlags,
language) language)
.copyWithMetadata(metadata); .copyWithMetadata(metadata);
subtitles.add(new Rendition(uri, format, groupId, name));
break; break;
case TYPE_CLOSED_CAPTIONS: case TYPE_CLOSED_CAPTIONS:
String instreamId = parseStringAttr(line, REGEX_INSTREAM_ID, variableDefinitions); String instreamId = parseStringAttr(line, REGEX_INSTREAM_ID, variableDefinitions);
...@@ -569,6 +578,17 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli ...@@ -569,6 +578,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;
......
...@@ -194,6 +194,19 @@ public class HlsMasterPlaylistParserTest { ...@@ -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 testParseMasterPlaylist() throws IOException { public void testParseMasterPlaylist() throws IOException {
HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE); HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE);
...@@ -380,6 +393,18 @@ public class HlsMasterPlaylistParserTest { ...@@ -380,6 +393,18 @@ public class HlsMasterPlaylistParserTest {
.isEqualTo(createExtXMediaMetadata(/* groupId= */ "aud3", /* name= */ "English")); .isEqualTo(createExtXMediaMetadata(/* groupId= */ "aud3", /* name= */ "English"));
} }
@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");
}
private static Metadata createExtXStreamInfMetadata(HlsTrackMetadataEntry.VariantInfo... infos) { private static Metadata createExtXStreamInfMetadata(HlsTrackMetadataEntry.VariantInfo... infos) {
return new Metadata( return new Metadata(
new HlsTrackMetadataEntry(/* groupId= */ null, /* name= */ null, Arrays.asList(infos))); new HlsTrackMetadataEntry(/* groupId= */ null, /* name= */ null, Arrays.asList(infos)));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment