diff --git a/android/src/main/java/com/brentvatne/common/api/SideLoadedTextTrack.kt b/android/src/main/java/com/brentvatne/common/api/SideLoadedTextTrack.kt index 59408c78c4..05aa050807 100644 --- a/android/src/main/java/com/brentvatne/common/api/SideLoadedTextTrack.kt +++ b/android/src/main/java/com/brentvatne/common/api/SideLoadedTextTrack.kt @@ -13,6 +13,12 @@ class SideLoadedTextTrack { var title: String? = null var uri: Uri = Uri.EMPTY var type: String? = null + + override fun equals(other: Any?): Boolean { + if (other == null || other !is SideLoadedTextTrack) return false + return language == other.language && title == other.title && uri == other.uri && type == other.type + } + companion object { val SIDELOAD_TEXT_TRACK_LANGUAGE = "language" val SIDELOAD_TEXT_TRACK_TITLE = "title" diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 539ecfd1e8..f696ebd1fc 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -42,6 +42,7 @@ import androidx.media3.common.Format; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaMetadata; +import androidx.media3.common.MimeTypes; import androidx.media3.common.Metadata; import androidx.media3.common.PlaybackException; import androidx.media3.common.PlaybackParameters; @@ -99,6 +100,11 @@ import androidx.media3.exoplayer.upstream.DefaultAllocator; import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter; import androidx.media3.exoplayer.util.EventLogger; +import androidx.media3.extractor.Extractor; +import androidx.media3.extractor.ExtractorsFactory; +import androidx.media3.extractor.text.SubtitleExtractor; +import androidx.media3.extractor.text.SubtitleParser; +import androidx.media3.extractor.text.DefaultSubtitleParserFactory; import androidx.media3.extractor.metadata.emsg.EventMessage; import androidx.media3.extractor.metadata.id3.Id3Frame; import androidx.media3.extractor.metadata.id3.TextInformationFrame; @@ -850,6 +856,16 @@ private void initializePlayerSource(Source runningSource) { MediaSource mediaSourceWithAds = initializeAds(videoSource, runningSource); MediaSource mediaSource = Objects.requireNonNullElse(mediaSourceWithAds, videoSource); + List subtitlesSource = buildSubtitleConfigurations(); + if (subtitlesSource != null) { // null if empty + MediaSource[] mediaSources = new MediaSource[subtitlesSource.size() + 1]; + mediaSources[0] = mediaSource; + for (int i = 0; i < subtitlesSource.size(); i++) { + mediaSources[i + 1] = subtitlesSource.get(i); + } + mediaSource = new MergingMediaSource(mediaSources); + } + // wait for player to be set while (player == null) { try { @@ -992,8 +1008,9 @@ private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessi if ("rtsp".equals(overrideExtension)) { type = CONTENT_TYPE_RTSP; } else { - type = Util.inferContentType(!TextUtils.isEmpty(overrideExtension) ? "." + overrideExtension - : uri.getLastPathSegment()); + type = TextUtils.isEmpty(overrideExtension) + ? Util.inferContentType(uri) + : Util.inferContentTypeForExtension(overrideExtension); } config.setDisableDisconnectError(this.disableDisconnectError); @@ -1006,12 +1023,6 @@ private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessi mediaItemBuilder.setMediaMetadata(customMetadata); } - // Add external subtitles to MediaItem - List subtitleConfigurations = buildSubtitleConfigurations(); - if (subtitleConfigurations != null) { - mediaItemBuilder.setSubtitleConfigurations(subtitleConfigurations); - } - if (source.getAdsProps() != null) { Uri adTagUrl = source.getAdsProps().getAdTagUrl(); if (adTagUrl != null) { @@ -1146,14 +1157,16 @@ private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessi } @Nullable - private List buildSubtitleConfigurations() { + private List buildSubtitleConfigurations() { if (source.getSideLoadedTextTracks() == null || source.getSideLoadedTextTracks().getTracks().isEmpty()) { return null; } - List subtitleConfigurations = new ArrayList<>(); + List sourcesToMerge = new ArrayList<>(); int trackIndex = 0; + SubtitleParser.Factory subtitleParserFactory = new DefaultSubtitleParserFactory(); + for (SideLoadedTextTrack track : source.getSideLoadedTextTracks().getTracks()) { try { // Create a more descriptive ID that PlayerView can use @@ -1166,26 +1179,37 @@ private List buildSubtitleConfigurations() { } } - MediaItem.SubtitleConfiguration.Builder configBuilder = new MediaItem.SubtitleConfiguration.Builder(track.getUri()) - .setId(trackId) - .setMimeType(track.getType()) - .setLabel(label) - .setRoleFlags(C.ROLE_FLAG_SUBTITLE); + Format.Builder formatBuilder = new Format.Builder() + .setSampleMimeType(track.getType()) + .setId(trackId) + .setLabel(label) + .setRoleFlags(C.ROLE_FLAG_SUBTITLE); // Set language if available if (track.getLanguage() != null && !track.getLanguage().isEmpty()) { - configBuilder.setLanguage(track.getLanguage()); + formatBuilder.setLanguage(track.getLanguage()); } // Set selection flags - make first track default if no specific track is selected if (trackIndex == 0 && (textTrackType == null || "disabled".equals(textTrackType))) { - configBuilder.setSelectionFlags(C.SELECTION_FLAG_DEFAULT); + formatBuilder.setSelectionFlags(C.SELECTION_FLAG_DEFAULT); } else { - configBuilder.setSelectionFlags(0); + formatBuilder.setSelectionFlags(0); } - - MediaItem.SubtitleConfiguration subtitleConfiguration = configBuilder.build(); - subtitleConfigurations.add(subtitleConfiguration); + + Format format = formatBuilder.build(); + ExtractorsFactory extractorsFactory = + () -> + new Extractor[] { + subtitleParserFactory.supportsFormat(format) + ? new SubtitleExtractor(subtitleParserFactory.create(format), format) + : new UnknownSubtitlesExtractor(format) + }; + + ProgressiveMediaSource.Factory progressiveMediaSourceFactory = + new ProgressiveMediaSource.Factory(mediaDataSourceFactory, extractorsFactory); + + sourcesToMerge.add(progressiveMediaSourceFactory.createMediaSource(MediaItem.fromUri(track.getUri().toString()))); DebugLog.d(TAG, "Created subtitle configuration: " + trackId + " - " + label + " (" + track.getType() + ")"); trackIndex++; @@ -1193,12 +1217,10 @@ private List buildSubtitleConfigurations() { DebugLog.e(TAG, "Error creating SubtitleConfiguration for URI " + track.getUri() + ": " + e.getMessage()); } } - - if (!subtitleConfigurations.isEmpty()) { - DebugLog.d(TAG, "Built " + subtitleConfigurations.size() + " external subtitle configurations"); + if (sourcesToMerge.isEmpty()) { + return null; } - - return subtitleConfigurations.isEmpty() ? null : subtitleConfigurations; + return sourcesToMerge; } private void releasePlayer() { @@ -1686,19 +1708,16 @@ private ArrayList getTextTrackInfo() { for (int groupIndex = 0; groupIndex < groups.length; ++groupIndex) { TrackGroup group = groups.get(groupIndex); for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { + int realIndex = textTracks.size(); Format format = group.getFormat(trackIndex); - Track textTrack = exoplayerTrackToGenericTrack(format, trackIndex, selection, group); - - boolean isExternal = format.id != null && format.id.startsWith("external-subtitle-"); - boolean isSelected = isTrackSelected(selection, group, trackIndex); - - textTrack.setIndex(textTracks.size()); + Track textTrack = exoplayerTrackToGenericTrack(format, realIndex, selection, group); if (textTrack.getTitle() == null || textTrack.getTitle().isEmpty()) { + boolean isExternal = format.id != null && format.id.startsWith("external-subtitle-"); if (isExternal) { - textTrack.setTitle("External " + (trackIndex + 1)); + textTrack.setTitle("External " + (realIndex + 1)); } else { - textTrack.setTitle("Track " + (textTracks.size() + 1)); + textTrack.setTitle("Track " + (realIndex + 1)); } } @@ -1765,12 +1784,10 @@ private ArrayList getBasicTextTrackInfo() { textTrack.setIndex(textTracks.size()); if (format.sampleMimeType != null) textTrack.setMimeType(format.sampleMimeType); if (format.language != null) textTrack.setLanguage(format.language); - - boolean isExternal = format.id != null && format.id.startsWith("external-subtitle-"); - + if (format.label != null && !format.label.isEmpty()) { textTrack.setTitle(format.label); - } else if (isExternal) { + } else if (format.id != null && format.id.startsWith("external-subtitle-")) { textTrack.setTitle("External " + (trackIndex + 1)); } else { textTrack.setTitle("Track " + (textTracks.size() + 1)); @@ -2123,7 +2140,7 @@ private void selectTextTrackInternal(String type, String value) { if (textRendererIndex != C.INDEX_UNSET) { TrackGroupArray groups = info.getTrackGroups(textRendererIndex); boolean trackFound = false; - + int realIndex = 0; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup group = groups.get(groupIndex); for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { @@ -2136,7 +2153,7 @@ private void selectTextTrackInternal(String type, String value) { isMatch = true; } else if ("index".equals(type)) { int targetIndex = ReactBridgeUtils.safeParseInt(value, -1); - if (targetIndex == trackIndex) { + if (targetIndex == realIndex) { isMatch = true; } } @@ -2150,6 +2167,7 @@ private void selectTextTrackInternal(String type, String value) { } } if (trackFound) break; + realIndex++; } if (!trackFound) { diff --git a/android/src/main/java/com/brentvatne/exoplayer/UnknownSubtitlesExtractor.java b/android/src/main/java/com/brentvatne/exoplayer/UnknownSubtitlesExtractor.java new file mode 100644 index 0000000000..36d3a85563 --- /dev/null +++ b/android/src/main/java/com/brentvatne/exoplayer/UnknownSubtitlesExtractor.java @@ -0,0 +1,54 @@ +package com.brentvatne.exoplayer; + +import androidx.media3.common.Format; +import androidx.media3.common.MimeTypes; +import androidx.media3.common.C; +import androidx.media3.extractor.Extractor; +import androidx.media3.extractor.ExtractorInput; +import androidx.media3.extractor.ExtractorOutput; +import androidx.media3.extractor.PositionHolder; +import androidx.media3.extractor.SeekMap; +import androidx.media3.extractor.TrackOutput; + +import java.io.IOException; + +public final class UnknownSubtitlesExtractor implements Extractor { + private final Format format; + + public UnknownSubtitlesExtractor(Format format) { + this.format = format; + } + + @Override + public boolean sniff(ExtractorInput input) { + return true; + } + + @Override + public void init(ExtractorOutput output) { + TrackOutput trackOutput = output.track(/* id= */ 0, C.TRACK_TYPE_TEXT); + output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); + output.endTracks(); + trackOutput.format( + format + .buildUpon() + .setSampleMimeType(MimeTypes.TEXT_UNKNOWN) + .setCodecs(format.sampleMimeType) + .build()); + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException { + int skipResult = input.skip(Integer.MAX_VALUE); + if (skipResult == C.RESULT_END_OF_INPUT) { + return RESULT_END_OF_INPUT; + } + return RESULT_CONTINUE; + } + + @Override + public void seek(long position, long timeUs) {} + + @Override + public void release() {} + } \ No newline at end of file