Skip to content

Commit

Permalink
fix: refactor side loaded text tracks management (#4158)
Browse files Browse the repository at this point in the history
* fix: refactor side loaded text tracks management

More textTracks in source.
android/ios: ensure text tracks are not selected by default
android/ios make textTrack field not nullable
clean up doc
check compatibility with the old api
Add comments on deprecated JS apis
Apply API change on basic sample

* chore: fix linter

* fix(ios): fix build with caching & remove warnings
  • Loading branch information
freeboub authored Sep 13, 2024
1 parent 7118ba6 commit 84a27f3
Show file tree
Hide file tree
Showing 14 changed files with 179 additions and 131 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@ import com.facebook.react.bridge.ReadableMap
class SideLoadedTextTrackList {
var tracks = ArrayList<SideLoadedTextTrack>()

/** return true if this and src are equals */
override fun equals(other: Any?): Boolean {
if (other == null || other !is SideLoadedTextTrackList) return false
return tracks == other.tracks
}

companion object {
fun parse(src: ReadableArray?): SideLoadedTextTrackList? {
if (src == null) {
return null
}
var sideLoadedTextTrackList = SideLoadedTextTrackList()
val sideLoadedTextTrackList = SideLoadedTextTrackList()
for (i in 0 until src.size()) {
val textTrack: ReadableMap = src.getMap(i)
sideLoadedTextTrackList.tracks.add(SideLoadedTextTrack.parse(textTrack))
Expand Down
10 changes: 9 additions & 1 deletion android/src/main/java/com/brentvatne/common/api/Source.kt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ class Source {
*/
var cmcdProps: CMCDProps? = null

/**
* The list of sideLoaded text tracks
*/
var sideLoadedTextTracks: SideLoadedTextTrackList? = null

override fun hashCode(): Int = Objects.hash(uriString, uri, startPositionMs, cropStartMs, cropEndMs, extension, metadata, headers)

/** return true if this and src are equals */
Expand All @@ -74,7 +79,8 @@ class Source {
startPositionMs == other.startPositionMs &&
extension == other.extension &&
drmProps == other.drmProps &&
cmcdProps == other.cmcdProps
cmcdProps == other.cmcdProps &&
sideLoadedTextTracks == other.sideLoadedTextTracks
)
}

Expand Down Expand Up @@ -139,6 +145,7 @@ class Source {
private const val PROP_SRC_DRM = "drm"
private const val PROP_SRC_CMCD = "cmcd"
private const val PROP_SRC_TEXT_TRACKS_ALLOW_CHUNKLESS_PREPARATION = "textTracksAllowChunklessPreparation"
private const val PROP_SRC_TEXT_TRACKS = "textTracks"

@SuppressLint("DiscouragedApi")
private fun getUriFromAssetId(context: Context, uriString: String): Uri? {
Expand Down Expand Up @@ -198,6 +205,7 @@ class Source {
source.drmProps = parse(safeGetMap(src, PROP_SRC_DRM))
source.cmcdProps = CMCDProps.parse(safeGetMap(src, PROP_SRC_CMCD))
source.textTracksAllowChunklessPreparation = safeGetBool(src, PROP_SRC_TEXT_TRACKS_ALLOW_CHUNKLESS_PREPARATION, true)
source.sideLoadedTextTracks = SideLoadedTextTrackList.parse(safeGetArray(src, PROP_SRC_TEXT_TRACKS))

val propSrcHeadersArray = safeGetArray(src, PROP_SRC_HEADERS)
if (propSrcHeadersArray != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.brentvatne.common.toolbox
import com.facebook.react.bridge.Dynamic
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import java.util.HashMap

/*
* Toolbox to safe parsing of <Video props
Expand Down Expand Up @@ -54,6 +53,17 @@ object ReactBridgeUtils {

@JvmStatic fun safeGetFloat(map: ReadableMap?, key: String?): Float = safeGetFloat(map, key, 0.0f)

@JvmStatic fun safeParseInt(value: String?, default: Int): Int {
if (value == null) {
return default
}
return try {
value.toInt()
} catch (e: java.lang.Exception) {
default
}
}

/**
* toStringMap converts a [ReadableMap] into a HashMap.
*
Expand Down
113 changes: 55 additions & 58 deletions android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,14 @@
import com.brentvatne.common.api.DRMProps;
import com.brentvatne.common.api.ResizeMode;
import com.brentvatne.common.api.SideLoadedTextTrack;
import com.brentvatne.common.api.SideLoadedTextTrackList;
import com.brentvatne.common.api.Source;
import com.brentvatne.common.api.SubtitleStyle;
import com.brentvatne.common.api.TimedMetadata;
import com.brentvatne.common.api.Track;
import com.brentvatne.common.api.VideoTrack;
import com.brentvatne.common.react.VideoEventEmitter;
import com.brentvatne.common.toolbox.DebugLog;
import com.brentvatne.common.toolbox.ReactBridgeUtils;
import com.brentvatne.react.BuildConfig;
import com.brentvatne.react.R;
import com.brentvatne.react.ReactNativeVideoManager;
Expand Down Expand Up @@ -230,9 +230,8 @@ public class ReactExoplayerView extends FrameLayout implements
private String audioTrackValue;
private String videoTrackType;
private String videoTrackValue;
private String textTrackType;
private String textTrackType = "disabled";
private String textTrackValue;
private SideLoadedTextTrackList textTracks;
private boolean disableFocus;
private boolean focusable = true;
private BufferingStrategy.BufferingStrategyEnum bufferingStrategy;
Expand Down Expand Up @@ -1126,11 +1125,11 @@ private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessi

private ArrayList<MediaSource> buildTextSources() {
ArrayList<MediaSource> textSources = new ArrayList<>();
if (textTracks == null) {
if (source.getSideLoadedTextTracks() == null) {
return textSources;
}

for (SideLoadedTextTrack track : textTracks.getTracks()) {
for (SideLoadedTextTrack track : source.getSideLoadedTextTracks().getTracks()) {
MediaSource textSource = buildTextSource(track.getTitle(),
track.getUri(),
track.getType(),
Expand Down Expand Up @@ -1844,11 +1843,6 @@ public void setAdLanguage(final String language) {
adLanguage = language;
}

public void setTextTracks(SideLoadedTextTrackList textTracks) {
this.textTracks = textTracks;
reloadSource(); // FIXME Shall be moved inside source
}

private void reloadSource() {
playerNeedsSource = true;
initializePlayer();
Expand Down Expand Up @@ -1928,64 +1922,67 @@ public void setSelectedTrack(int trackType, String type, String value) {
}
}
} else if ("index".equals(type)) {
int iValue = Integer.parseInt(value);

if (trackType == C.TRACK_TYPE_VIDEO && groups.length == 1) {
groupIndex = 0;
if (iValue < groups.get(groupIndex).length) {
tracks.set(0, iValue);
int iValue = ReactBridgeUtils.safeParseInt(value, -1);
if (iValue != -1) {
if (trackType == C.TRACK_TYPE_VIDEO && groups.length == 1) {
groupIndex = 0;
if (iValue < groups.get(groupIndex).length) {
tracks.set(0, iValue);
}
} else if (iValue < groups.length) {
groupIndex = iValue;
}
} else if (iValue < groups.length) {
groupIndex = iValue;
}
} else if ("resolution".equals(type)) {
int height = Integer.parseInt(value);
for (int i = 0; i < groups.length; ++i) { // Search for the exact height
TrackGroup group = groups.get(i);
Format closestFormat = null;
int closestTrackIndex = -1;
boolean usingExactMatch = false;
for (int j = 0; j < group.length; j++) {
Format format = group.getFormat(j);
if (format.height == height) {
groupIndex = i;
tracks.set(0, j);
closestFormat = null;
closestTrackIndex = -1;
usingExactMatch = true;
break;
} else if (isUsingContentResolution) {
// When using content resolution rather than ads, we need to try and find the closest match if there is no exact match
if (closestFormat != null) {
if ((format.bitrate > closestFormat.bitrate || format.height > closestFormat.height) && format.height < height) {
// Higher quality match
int height = ReactBridgeUtils.safeParseInt(value, -1);
if (height != -1) {
for (int i = 0; i < groups.length; ++i) { // Search for the exact height
TrackGroup group = groups.get(i);
Format closestFormat = null;
int closestTrackIndex = -1;
boolean usingExactMatch = false;
for (int j = 0; j < group.length; j++) {
Format format = group.getFormat(j);
if (format.height == height) {
groupIndex = i;
tracks.set(0, j);
closestFormat = null;
closestTrackIndex = -1;
usingExactMatch = true;
break;
} else if (isUsingContentResolution) {
// When using content resolution rather than ads, we need to try and find the closest match if there is no exact match
if (closestFormat != null) {
if ((format.bitrate > closestFormat.bitrate || format.height > closestFormat.height) && format.height < height) {
// Higher quality match
closestFormat = format;
closestTrackIndex = j;
}
} else if (format.height < height) {
closestFormat = format;
closestTrackIndex = j;
}
} else if(format.height < height) {
closestFormat = format;
closestTrackIndex = j;
}
}
}
// This is a fallback if the new period contains only higher resolutions than the user has selected
if (closestFormat == null && isUsingContentResolution && !usingExactMatch) {
// No close match found - so we pick the lowest quality
int minHeight = Integer.MAX_VALUE;
for (int j = 0; j < group.length; j++) {
Format format = group.getFormat(j);
if (format.height < minHeight) {
minHeight = format.height;
groupIndex = i;
tracks.set(0, j);
// This is a fallback if the new period contains only higher resolutions than the user has selected
if (closestFormat == null && isUsingContentResolution && !usingExactMatch) {
// No close match found - so we pick the lowest quality
int minHeight = Integer.MAX_VALUE;
for (int j = 0; j < group.length; j++) {
Format format = group.getFormat(j);
if (format.height < minHeight) {
minHeight = format.height;
groupIndex = i;
tracks.set(0, j);
}
}
}
}
// Selecting the closest match found
if (closestFormat != null && closestTrackIndex != -1) {
// We found the closest match instead of an exact one
groupIndex = i;
tracks.set(0, closestTrackIndex);
// Selecting the closest match found
if (closestFormat != null && closestTrackIndex != -1) {
// We found the closest match instead of an exact one
groupIndex = i;
tracks.set(0, closestTrackIndex);
}
}
}
} else if (trackType == C.TRACK_TYPE_TEXT && Util.SDK_INT > 18) { // Text default
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,13 @@ import com.brentvatne.common.api.BufferConfig
import com.brentvatne.common.api.BufferingStrategy
import com.brentvatne.common.api.ControlsConfig
import com.brentvatne.common.api.ResizeMode
import com.brentvatne.common.api.SideLoadedTextTrackList
import com.brentvatne.common.api.Source
import com.brentvatne.common.api.SubtitleStyle
import com.brentvatne.common.api.ViewType
import com.brentvatne.common.react.EventTypes
import com.brentvatne.common.toolbox.DebugLog
import com.brentvatne.common.toolbox.ReactBridgeUtils
import com.brentvatne.react.ReactNativeVideoManager
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewGroupManager
Expand All @@ -38,7 +36,6 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
private const val PROP_SELECTED_TEXT_TRACK = "selectedTextTrack"
private const val PROP_SELECTED_TEXT_TRACK_TYPE = "type"
private const val PROP_SELECTED_TEXT_TRACK_VALUE = "value"
private const val PROP_TEXT_TRACKS = "textTracks"
private const val PROP_PAUSED = "paused"
private const val PROP_MUTED = "muted"
private const val PROP_AUDIO_OUTPUT = "audioOutput"
Expand Down Expand Up @@ -180,12 +177,6 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
videoView.setSelectedTextTrack(typeString, value)
}

@ReactProp(name = PROP_TEXT_TRACKS)
fun setTextTracks(videoView: ReactExoplayerView, textTracks: ReadableArray?) {
val sideLoadedTextTracks = SideLoadedTextTrackList.parse(textTracks)
videoView.setTextTracks(sideLoadedTextTracks)
}

@ReactProp(name = PROP_PAUSED, defaultBoolean = false)
fun setPaused(videoView: ReactExoplayerView, paused: Boolean) {
videoView.setPausedModifier(paused)
Expand Down
43 changes: 43 additions & 0 deletions docs/pages/component/props.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,46 @@ source={{
}}
```

#### `textTracks`

<PlatformsList types={['Android', 'iOS', 'visionOS']} />

Load one or more "sidecar" text tracks. This takes an array of objects representing each track. Each object should have the format:

> ⚠️ This feature does not work with HLS playlists (e.g m3u8) on iOS

| Property | Description |
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| title | Descriptive name for the track |
| language | 2 letter [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) representing the language |
| type | Mime type of the track _ TextTrackType.SRT - SubRip (.srt) _ TextTrackType.TTML - TTML (.ttml) \* TextTrackType.VTT - WebVTT (.vtt)iOS only supports VTT, Android supports all 3 |
| uri | URL for the text track. Currently, only tracks hosted on a webserver are supported |

On iOS, sidecar text tracks are only supported for individual files, not HLS playlists. For HLS, you should include the text tracks as part of the playlist.

Note: Due to iOS limitations, sidecar text tracks are not compatible with Airplay. If textTracks are specified, AirPlay support will be automatically disabled.

Example:

```javascript
import { TextTrackType }, Video from 'react-native-video';
textTracks={[
{
title: "English CC",
language: "en",
type: TextTrackType.VTT, // "text/vtt"
uri: "https://bitdash-a.akamaihd.net/content/sintel/subtitles/subtitles_en.vtt"
},
{
title: "Spanish Subtitles",
language: "es",
type: TextTrackType.SRT, // "application/x-subrip"
uri: "https://durian.blender.org/wp-content/content/subtitles/sintel_es.srt"
}
]}
```

### `subtitleStyle`

<PlatformsList types={['Android', 'iOS']} />
Expand Down Expand Up @@ -892,6 +932,9 @@ This prop can be changed on runtime.

### `textTracks`

> [!WARNING]
> deprecated, use source.textTracks instead. changing text tracks will restart playback

<PlatformsList types={['Android', 'iOS', 'visionOS']} />

Load one or more "sidecar" text tracks. This takes an array of objects representing each track. Each object should have the format:
Expand Down
1 change: 0 additions & 1 deletion examples/basic/src/VideoPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,6 @@ const VideoPlayer: FC<Props> = ({}) => {
showNotificationControls={showNotificationControls}
ref={videoRef}
source={currentSrc as ReactVideoSource}
textTracks={additional?.textTracks}
adTagUrl={additional?.adTagUrl}
drm={additional?.drm}
style={viewStyle}
Expand Down
4 changes: 4 additions & 0 deletions ios/Video/DataStructures/SelectedTrackCriteria.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@ struct SelectedTrackCriteria {
self.type = json["type"] as? String ?? ""
self.value = json["value"] as? String
}

static func none() -> SelectedTrackCriteria {
return SelectedTrackCriteria(["type": "none", "value": ""])
}
}
4 changes: 4 additions & 0 deletions ios/Video/DataStructures/VideoSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ struct VideoSource {
let customMetadata: CustomMetadata?
/* DRM */
let drm: DRMParams?
var textTracks: [TextTrack] = []

let json: NSDictionary?

Expand Down Expand Up @@ -52,5 +53,8 @@ struct VideoSource {
self.cropEnd = (json["cropEnd"] as? Float64).flatMap { Int64(round($0)) }
self.customMetadata = CustomMetadata(json["metadata"] as? NSDictionary)
self.drm = DRMParams(json["drm"] as? NSDictionary)
self.textTracks = (json["textTracks"] as? NSArray)?.map { trackDict in
return TextTrack(trackDict as? NSDictionary)
} ?? []
}
}
Loading

0 comments on commit 84a27f3

Please sign in to comment.