diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3c5bf13eae..0bfe39b1ce 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -88,6 +88,8 @@ `OfflineLicenseHelper` ([#7078](https://github.com/google/ExoPlayer/issues/7078)). * 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 download speed ([#5978](https://github.com/google/ExoPlayer/issues/5978)). * MP3: Add `IndexSeeker` for accurate seeks in VBR streams diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index 43335cf51c..50930c6992 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -123,22 +123,22 @@ public final class MimeTypes { 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) { 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) { 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) { 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) { return BASE_TYPE_APPLICATION.equals(getTopLevelType(mimeType)); } @@ -174,13 +174,14 @@ public final class MimeTypes { * @param codecs The codecs attribute. * @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) { return null; } String[] codecList = Util.splitCodecs(codecs); for (String codec : codecList) { - String mimeType = getMediaMimeType(codec); + @Nullable String mimeType = getMediaMimeType(codec); if (mimeType != null && isVideo(mimeType)) { return mimeType; } @@ -194,13 +195,14 @@ public final class MimeTypes { * @param codecs The codecs attribute. * @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) { return null; } String[] codecList = Util.splitCodecs(codecs); for (String codec : codecList) { - String mimeType = getMediaMimeType(codec); + @Nullable String mimeType = getMediaMimeType(codec); if (mimeType != null && isAudio(mimeType)) { return mimeType; } @@ -214,7 +216,8 @@ public final class MimeTypes { * @param codec The codec identifier to derive. * @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) { return null; } @@ -235,7 +238,7 @@ public final class MimeTypes { } else if (codec.startsWith("vp8") || codec.startsWith("vp08")) { return MimeTypes.VIDEO_VP8; } else if (codec.startsWith("mp4a")) { - String mimeType = null; + @Nullable String mimeType = null; if (codec.startsWith("mp4a.")) { String objectTypeString = codec.substring(5); // remove the 'mp4a.' prefix if (objectTypeString.length() >= 2) { @@ -244,7 +247,7 @@ public final class MimeTypes { int objectTypeInt = Integer.parseInt(objectTypeHexString, 16); mimeType = getMimeTypeFromMp4ObjectType(objectTypeInt); } catch (NumberFormatException ignored) { - // ignored + // Ignored. } } } @@ -267,6 +270,10 @@ public final class MimeTypes { return MimeTypes.AUDIO_VORBIS; } else if (codec.startsWith("flac")) { return MimeTypes.AUDIO_FLAC; + } else if (codec.startsWith("stpp")) { + return MimeTypes.APPLICATION_TTML; + } else if (codec.startsWith("wvtt")) { + return MimeTypes.TEXT_VTT; } else { 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 * 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) { return null; } @@ -417,7 +425,8 @@ public final class MimeTypes { return mimeType.substring(0, indexOfSlash); } - private static @Nullable String getCustomMimeTypeForCodec(String codec) { + @Nullable + private static String getCustomMimeTypeForCodec(String codec) { int customMimeTypeCount = customMimeTypes.size(); for (int i = 0; i < customMimeTypeCount; i++) { CustomMimeType customMimeType = customMimeTypes.get(i); diff --git a/library/common/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java b/library/common/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java index bf89b100b5..6224b7b60e 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java @@ -73,6 +73,10 @@ public final class MimeTypesTest { assertThat(MimeTypes.getMediaMimeType("mp4a.AA")).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("wvtt")).isEqualTo(MimeTypes.TEXT_VTT); + assertThat(MimeTypes.getMediaMimeType("stpp.")).isEqualTo(MimeTypes.APPLICATION_TTML); + assertThat(MimeTypes.getMediaMimeType("stpp.ttml.im1t")).isEqualTo(MimeTypes.APPLICATION_TTML); } @Test diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index bcf06c5d2e..14fc420ef3 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -458,7 +458,18 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser 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( HlsMasterPlaylist masterPlaylist, LineIterator iterator, String baseUri) throws IOException { @HlsMediaPlaylist.PlaylistType int playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_UNKNOWN; diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java index 40ba379a71..55aa4b3c43 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java @@ -194,6 +194,19 @@ public class HlsMasterPlaylistParserTest { + "#EXT-X-MEDIA:TYPE=SUBTITLES," + "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 public void parseMasterPlaylist_withSimple_success() throws IOException { HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE); @@ -321,6 +334,7 @@ public class HlsMasterPlaylistParserTest { Format firstTextFormat = playlist.subtitles.get(0).format; assertThat(firstTextFormat.id).isEqualTo("sub1:Eng"); + assertThat(firstTextFormat.sampleMimeType).isEqualTo(MimeTypes.TEXT_VTT); } @Test @@ -345,6 +359,18 @@ public class HlsMasterPlaylistParserTest { .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 public void parseMasterPlaylist_withMatchingStreamInfUrls_success() throws IOException { HlsMasterPlaylist playlist =