Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -850,6 +856,16 @@ private void initializePlayerSource(Source runningSource) {
MediaSource mediaSourceWithAds = initializeAds(videoSource, runningSource);
MediaSource mediaSource = Objects.requireNonNullElse(mediaSourceWithAds, videoSource);

List<MediaSource> 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 {
Expand Down Expand Up @@ -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);

Expand All @@ -1006,12 +1023,6 @@ private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessi
mediaItemBuilder.setMediaMetadata(customMetadata);
}

// Add external subtitles to MediaItem
List<MediaItem.SubtitleConfiguration> subtitleConfigurations = buildSubtitleConfigurations();
if (subtitleConfigurations != null) {
mediaItemBuilder.setSubtitleConfigurations(subtitleConfigurations);
}

if (source.getAdsProps() != null) {
Uri adTagUrl = source.getAdsProps().getAdTagUrl();
if (adTagUrl != null) {
Expand Down Expand Up @@ -1146,14 +1157,16 @@ private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessi
}

@Nullable
private List<MediaItem.SubtitleConfiguration> buildSubtitleConfigurations() {
private List<MediaSource> buildSubtitleConfigurations() {
if (source.getSideLoadedTextTracks() == null || source.getSideLoadedTextTracks().getTracks().isEmpty()) {
return null;
}

List<MediaItem.SubtitleConfiguration> subtitleConfigurations = new ArrayList<>();
List<MediaSource> 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
Expand All @@ -1166,39 +1179,48 @@ private List<MediaItem.SubtitleConfiguration> 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++;
} catch (Exception e) {
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() {
Expand Down Expand Up @@ -1686,19 +1708,16 @@ private ArrayList<Track> 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));
}
}

Expand Down Expand Up @@ -1765,12 +1784,10 @@ private ArrayList<Track> 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));
Expand Down Expand Up @@ -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++) {
Expand All @@ -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;
}
}
Expand All @@ -2150,6 +2167,7 @@ private void selectTextTrackInternal(String type, String value) {
}
}
if (trackFound) break;
realIndex++;
}

if (!trackFound) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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() {}
}