fix: refactor side loaded text tracks management (#4158)
* 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
This commit is contained in:
		| @@ -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)) | ||||
|   | ||||
| @@ -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  */ | ||||
| @@ -74,7 +79,8 @@ class Source { | ||||
|                 startPositionMs == other.startPositionMs && | ||||
|                 extension == other.extension && | ||||
|                 drmProps == other.drmProps && | ||||
|                 cmcdProps == other.cmcdProps | ||||
|                 cmcdProps == other.cmcdProps && | ||||
|                 sideLoadedTextTracks == other.sideLoadedTextTracks | ||||
|             ) | ||||
|     } | ||||
|  | ||||
| @@ -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? { | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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 | ||||
| @@ -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. | ||||
|      * | ||||
|   | ||||
| @@ -108,7 +108,6 @@ import com.brentvatne.common.api.ControlsConfig; | ||||
| 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; | ||||
| @@ -116,6 +115,7 @@ 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; | ||||
| @@ -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; | ||||
| @@ -1126,11 +1125,11 @@ public class ReactExoplayerView extends FrameLayout implements | ||||
|  | ||||
|     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(), | ||||
| @@ -1844,11 +1843,6 @@ public class ReactExoplayerView extends FrameLayout implements | ||||
|         adLanguage = language; | ||||
|     } | ||||
|  | ||||
|     public void setTextTracks(SideLoadedTextTrackList textTracks) { | ||||
|         this.textTracks = textTracks; | ||||
|         reloadSource(); // FIXME Shall be moved inside source | ||||
|     } | ||||
|  | ||||
|     private void reloadSource() { | ||||
|         playerNeedsSource = true; | ||||
|         initializePlayer(); | ||||
| @@ -1928,64 +1922,67 @@ public class ReactExoplayerView extends FrameLayout implements | ||||
|                 } | ||||
|             } | ||||
|         } 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 | ||||
|   | ||||
| @@ -8,7 +8,6 @@ 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 | ||||
| @@ -16,7 +15,6 @@ 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 | ||||
| @@ -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" | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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']} /> | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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} | ||||
|   | ||||
| @@ -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": ""]) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -11,6 +11,7 @@ struct VideoSource { | ||||
|     let customMetadata: CustomMetadata? | ||||
|     /* DRM */ | ||||
|     let drm: DRMParams? | ||||
|     var textTracks: [TextTrack] = [] | ||||
|  | ||||
|     let json: NSDictionary? | ||||
|  | ||||
| @@ -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) | ||||
|         } ?? [] | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -42,9 +42,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | ||||
|     private var _repeat = false | ||||
|     private var _isPlaying = false | ||||
|     private var _allowsExternalPlayback = true | ||||
|     private var _textTracks: [TextTrack] = [] | ||||
|     private var _selectedTextTrackCriteria: SelectedTrackCriteria? | ||||
|     private var _selectedAudioTrackCriteria: SelectedTrackCriteria? | ||||
|     private var _selectedTextTrackCriteria: SelectedTrackCriteria = .none() | ||||
|     private var _selectedAudioTrackCriteria: SelectedTrackCriteria = .none() | ||||
|     private var _playbackStalled = false | ||||
|     private var _playInBackground = false | ||||
|     private var _preventsDisplaySleepDuringVideoPlayback = true | ||||
| @@ -428,7 +427,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | ||||
|  | ||||
|         if let uri = source.uri, uri.starts(with: "ph://") { | ||||
|             let photoAsset = await RCTVideoUtils.preparePHAsset(uri: uri) | ||||
|             return await playerItemPrepareText(asset: photoAsset, assetOptions: nil, uri: source.uri ?? "") | ||||
|             return await playerItemPrepareText(source: source, asset: photoAsset, assetOptions: nil, uri: source.uri ?? "") | ||||
|         } | ||||
|  | ||||
|         guard let assetResult = RCTVideoUtils.prepareAsset(source: source), | ||||
| @@ -454,8 +453,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | ||||
|         } | ||||
|  | ||||
|         #if USE_VIDEO_CACHING | ||||
|             if _videoCache.shouldCache(source: source, textTracks: _textTracks) { | ||||
|                 return try await _videoCache.playerItemForSourceUsingCache(uri: source.uri, assetOptions: assetOptions) | ||||
|             if _videoCache.shouldCache(source: source) { | ||||
|                 return try await _videoCache.playerItemForSourceUsingCache(source: source, assetOptions: assetOptions) | ||||
|             } | ||||
|         #endif | ||||
|  | ||||
| @@ -470,7 +469,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         return await playerItemPrepareText(asset: asset, assetOptions: assetOptions, uri: source.uri ?? "") | ||||
|         return await playerItemPrepareText(source: source, asset: asset, assetOptions: assetOptions, uri: source.uri ?? "") | ||||
|     } | ||||
|  | ||||
|     func setupPlayer(playerItem: AVPlayerItem) async throws { | ||||
| @@ -600,8 +599,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | ||||
|         _localSourceEncryptionKeyScheme = keyScheme | ||||
|     } | ||||
|  | ||||
|     func playerItemPrepareText(asset: AVAsset!, assetOptions: NSDictionary?, uri: String) async -> AVPlayerItem { | ||||
|         if self._textTracks.isEmpty == true || (uri.hasSuffix(".m3u8")) { | ||||
|     func playerItemPrepareText(source: VideoSource, asset: AVAsset!, assetOptions: NSDictionary?, uri: String) async -> AVPlayerItem { | ||||
|         if source.textTracks.isEmpty != true || uri.hasSuffix(".m3u8") { | ||||
|             return await self.playerItemPropegateMetadata(AVPlayerItem(asset: asset)) | ||||
|         } | ||||
|  | ||||
| @@ -612,15 +611,15 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | ||||
|             asset: asset, | ||||
|             assetOptions: assetOptions, | ||||
|             mixComposition: mixComposition, | ||||
|             textTracks: self._textTracks | ||||
|             textTracks: source.textTracks | ||||
|         ) | ||||
|  | ||||
|         if validTextTracks.isEmpty { | ||||
|             DebugLog("Strange state, not valid textTrack") | ||||
|         } | ||||
|  | ||||
|         if validTextTracks.count != self._textTracks.count { | ||||
|             self.setTextTracks(validTextTracks) | ||||
|         if validTextTracks.count != source.textTracks.count { | ||||
|             setSelectedTextTrack(_selectedTextTrackCriteria) | ||||
|         } | ||||
|  | ||||
|         return await self.playerItemPropegateMetadata(AVPlayerItem(asset: mixComposition)) | ||||
| @@ -935,10 +934,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | ||||
|             setMaxBitRate(_maxBitRate) | ||||
|         } | ||||
|  | ||||
|         if _selectedTextTrackCriteria != nil { | ||||
|             setSelectedTextTrack(_selectedTextTrackCriteria) | ||||
|         } | ||||
|  | ||||
|         setSelectedTextTrack(_selectedTextTrackCriteria) | ||||
|         setAudioOutput(_audioOutput) | ||||
|         setSelectedAudioTrack(_selectedAudioTrackCriteria) | ||||
|         setResizeMode(_resizeMode) | ||||
| @@ -959,7 +955,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | ||||
|     } | ||||
|  | ||||
|     func setSelectedAudioTrack(_ selectedAudioTrack: SelectedTrackCriteria?) { | ||||
|         _selectedAudioTrackCriteria = selectedAudioTrack | ||||
|         _selectedAudioTrackCriteria = selectedAudioTrack ?? SelectedTrackCriteria.none() | ||||
|         Task { | ||||
|             await RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(player: _player, characteristic: AVMediaCharacteristic.audible, | ||||
|                                                                               criteria: _selectedAudioTrackCriteria) | ||||
| @@ -972,9 +968,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | ||||
|     } | ||||
|  | ||||
|     func setSelectedTextTrack(_ selectedTextTrack: SelectedTrackCriteria?) { | ||||
|         _selectedTextTrackCriteria = selectedTextTrack | ||||
|         if !_textTracks.isEmpty { // sideloaded text tracks | ||||
|             RCTPlayerOperations.setSideloadedText(player: _player, textTracks: _textTracks, criteria: _selectedTextTrackCriteria) | ||||
|         _selectedTextTrackCriteria = selectedTextTrack ?? SelectedTrackCriteria.none() | ||||
|         guard let source = _source else { return } | ||||
|         if !source.textTracks.isEmpty { // sideloaded text tracks | ||||
|             RCTPlayerOperations.setSideloadedText(player: _player, textTracks: source.textTracks, criteria: _selectedTextTrackCriteria) | ||||
|         } else { // text tracks included in the HLS playlist§ | ||||
|             Task { | ||||
|                 await RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(player: _player, characteristic: AVMediaCharacteristic.legible, | ||||
| @@ -983,21 +980,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @objc | ||||
|     func setTextTracks(_ textTracks: [NSDictionary]?) { | ||||
|         setTextTracks(textTracks?.map { TextTrack($0) }) | ||||
|     } | ||||
|  | ||||
|     func setTextTracks(_ textTracks: [TextTrack]?) { | ||||
|         if textTracks == nil { | ||||
|             _textTracks = [] | ||||
|         } else { | ||||
|             _textTracks = textTracks! | ||||
|         } | ||||
|         // in case textTracks was set after selectedTextTrack | ||||
|         if _selectedTextTrackCriteria != nil { setSelectedTextTrack(_selectedTextTrackCriteria) } | ||||
|     } | ||||
|  | ||||
|     @objc | ||||
|     func setChapters(_ chapters: [NSDictionary]?) { | ||||
|         setChapters(chapters?.map { Chapter($0) }) | ||||
| @@ -1307,9 +1289,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | ||||
|         _playerItem = nil | ||||
|         _source = nil | ||||
|         _chapters = nil | ||||
|         _textTracks = [] | ||||
|         _selectedTextTrackCriteria = nil | ||||
|         _selectedAudioTrackCriteria = nil | ||||
|         _selectedTextTrackCriteria = SelectedTrackCriteria.none() | ||||
|         _selectedAudioTrackCriteria = SelectedTrackCriteria.none() | ||||
|         _presentingViewController = nil | ||||
|  | ||||
|         ReactNativeVideoManager.shared.onInstanceRemoved(id: instanceId, player: _player as Any) | ||||
| @@ -1419,7 +1400,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | ||||
|  | ||||
|     func handleReadyToPlay() { | ||||
|         guard let _playerItem else { return } | ||||
|  | ||||
|         guard let source = _source else { return } | ||||
|         Task { | ||||
|             if self._pendingSeek { | ||||
|                 self.setSeek(NSNumber(value: self._pendingSeekTime), NSNumber(value: 100)) | ||||
| @@ -1475,7 +1456,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | ||||
|                                        "orientation": orientation, | ||||
|                                    ], | ||||
|                                    "audioTracks": audioTracks, | ||||
|                                    "textTracks": extractJsonWithIndex(from: _textTracks) ?? textTracks.map(\.json), | ||||
|                                    "textTracks": extractJsonWithIndex(from: source.textTracks) ?? textTracks.map(\.json), | ||||
|                                    "target": self.reactTag as Any]) | ||||
|             } | ||||
|  | ||||
| @@ -1672,10 +1653,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH | ||||
|     } | ||||
|  | ||||
|     func handleTracksChange(playerItem _: AVPlayerItem, change _: NSKeyValueObservedChange<[AVPlayerItemTrack]>) { | ||||
|         guard let source = _source else { return } | ||||
|         if onTextTracks != nil { | ||||
|             Task { | ||||
|                 let textTracks = await RCTVideoUtils.getTextTrackInfo(self._player) | ||||
|                 self.onTextTracks?(["textTracks": extractJsonWithIndex(from: _textTracks) ?? textTracks.compactMap(\.json)]) | ||||
|                 self.onTextTracks?(["textTracks": extractJsonWithIndex(from: source.textTracks) ?? textTracks.compactMap(\.json)]) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -4,20 +4,20 @@ import Foundation | ||||
|  | ||||
| class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate { | ||||
|     private var _videoCache: RCTVideoCache! = RCTVideoCache.sharedInstance() | ||||
|     var playerItemPrepareText: ((AVAsset?, NSDictionary?, String) async -> AVPlayerItem)? | ||||
|     var playerItemPrepareText: ((VideoSource, AVAsset?, NSDictionary?, String) async -> AVPlayerItem)? | ||||
|  | ||||
|     override init() { | ||||
|         super.init() | ||||
|     } | ||||
|  | ||||
|     func shouldCache(source: VideoSource, textTracks: [TextTrack]?) -> Bool { | ||||
|         if source.isNetwork && source.shouldCache && ((textTracks == nil) || (textTracks!.isEmpty)) { | ||||
|     func shouldCache(source: VideoSource) -> Bool { | ||||
|         if source.isNetwork && source.shouldCache && source.textTracks.isEmpty { | ||||
|             /* The DVURLAsset created by cache doesn't have a tracksWithMediaType property, so trying | ||||
|              * to bring in the text track code will crash. I suspect this is because the asset hasn't fully loaded. | ||||
|              * Until this is fixed, we need to bypass caching when text tracks are specified. | ||||
|              */ | ||||
|             DebugLog(""" | ||||
|               Caching is not supported for uri '\(source.uri)' because text tracks are not compatible with the cache. | ||||
|               Caching is not supported for uri '\(source.uri ?? "NO URI")' because text tracks are not compatible with the cache. | ||||
|               Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md | ||||
|             """) | ||||
|             return true | ||||
| @@ -25,7 +25,8 @@ class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate { | ||||
|         return false | ||||
|     } | ||||
|  | ||||
|     func playerItemForSourceUsingCache(uri: String!, assetOptions options: NSDictionary!) async throws -> AVPlayerItem { | ||||
|     func playerItemForSourceUsingCache(source: VideoSource, assetOptions options: NSDictionary) async throws -> AVPlayerItem { | ||||
|         let uri = source.uri! | ||||
|         let url = URL(string: uri) | ||||
|         let (videoCacheStatus, cachedAsset) = await getItemForUri(uri) | ||||
|  | ||||
| @@ -36,33 +37,33 @@ class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate { | ||||
|         switch videoCacheStatus { | ||||
|         case .missingFileExtension: | ||||
|             DebugLog(""" | ||||
|               Could not generate cache key for uri '\(uri ?? "NO_URI")'. | ||||
|               Could not generate cache key for uri '\(uri)'. | ||||
|               It is currently not supported to cache urls that do not include a file extension. | ||||
|               The video file will not be cached. | ||||
|               Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md | ||||
|             """) | ||||
|             let asset: AVURLAsset! = AVURLAsset(url: url!, options: options as! [String: Any]) | ||||
|             return await playerItemPrepareText(asset, options, "") | ||||
|             let asset: AVURLAsset! = AVURLAsset(url: url!, options: options as? [String: Any]) | ||||
|             return await playerItemPrepareText(source, asset, options, "") | ||||
|  | ||||
|         case .unsupportedFileExtension: | ||||
|             DebugLog(""" | ||||
|               Could not generate cache key for uri '\(uri ?? "NO_URI")'. | ||||
|               Could not generate cache key for uri '\(uri)'. | ||||
|               The file extension of that uri is currently not supported. | ||||
|               The video file will not be cached. | ||||
|               Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md | ||||
|             """) | ||||
|             let asset: AVURLAsset! = AVURLAsset(url: url!, options: options as! [String: Any]) | ||||
|             return await playerItemPrepareText(asset, options, "") | ||||
|             let asset: AVURLAsset! = AVURLAsset(url: url!, options: options as? [String: Any]) | ||||
|             return await playerItemPrepareText(source, asset, options, "") | ||||
|  | ||||
|         default: | ||||
|             if let cachedAsset { | ||||
|                 DebugLog("Playing back uri '\(uri ?? "NO_URI")' from cache") | ||||
|                 DebugLog("Playing back uri '\(uri)' from cache") | ||||
|                 // See note in playerItemForSource about not being able to support text tracks & caching | ||||
|                 return AVPlayerItem(asset: cachedAsset) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         let asset: DVURLAsset! = DVURLAsset(url: url, options: options as! [String: Any], networkTimeout: 10000) | ||||
|         let asset: DVURLAsset! = DVURLAsset(url: url, options: options as? [String: Any], networkTimeout: 10000) | ||||
|         asset.loaderDelegate = self | ||||
|  | ||||
|         /* More granular code to have control over the DVURLAsset | ||||
|   | ||||
| @@ -166,6 +166,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>( | ||||
|       ); | ||||
|  | ||||
|       const selectedDrm = source.drm || drm; | ||||
|       const _textTracks = source.textTracks || textTracks; | ||||
|       const _drm = !selectedDrm | ||||
|         ? undefined | ||||
|         : { | ||||
| @@ -218,10 +219,11 @@ const Video = forwardRef<VideoRef, ReactVideoProps>( | ||||
|         metadata: resolvedSource.metadata, | ||||
|         drm: _drm, | ||||
|         cmcd: _cmcd, | ||||
|         textTracks: _textTracks, | ||||
|         textTracksAllowChunklessPreparation: | ||||
|           resolvedSource.textTracksAllowChunklessPreparation, | ||||
|       }; | ||||
|     }, [drm, source]); | ||||
|     }, [drm, source, textTracks]); | ||||
|  | ||||
|     const _selectedTextTrack = useMemo(() => { | ||||
|       if (!selectedTextTrack) { | ||||
| @@ -727,7 +729,6 @@ const Video = forwardRef<VideoRef, ReactVideoProps>( | ||||
|           restoreUserInterfaceForPIPStopCompletionHandler={ | ||||
|             _restoreUserInterfaceForPIPStopCompletionHandler | ||||
|           } | ||||
|           textTracks={textTracks} | ||||
|           selectedTextTrack={_selectedTextTrack} | ||||
|           selectedAudioTrack={_selectedAudioTrack} | ||||
|           selectedVideoTrack={_selectedVideoTrack} | ||||
|   | ||||
| @@ -42,6 +42,7 @@ export type VideoSrc = Readonly<{ | ||||
|   drm?: Drm; | ||||
|   cmcd?: NativeCmcdConfiguration; // android | ||||
|   textTracksAllowChunklessPreparation?: boolean; // android | ||||
|   textTracks?: TextTracks; | ||||
| }>; | ||||
|  | ||||
| type DRMType = WithDefault<string, 'widevine'>; | ||||
| @@ -317,7 +318,6 @@ export interface VideoNativeProps extends ViewProps { | ||||
|   automaticallyWaitsToMinimizeStalling?: boolean; | ||||
|   shutterColor?: Int32; | ||||
|   audioOutput?: WithDefault<string, 'speaker'>; | ||||
|   textTracks?: TextTracks; | ||||
|   selectedTextTrack?: SelectedTextTrack; | ||||
|   selectedAudioTrack?: SelectedAudioTrack; | ||||
|   selectedVideoTrack?: SelectedVideoTrack; // android | ||||
|   | ||||
| @@ -35,6 +35,7 @@ export type ReactVideoSourceProperties = { | ||||
|   drm?: Drm; | ||||
|   cmcd?: Cmcd; // android | ||||
|   textTracksAllowChunklessPreparation?: boolean; | ||||
|   textTracks?: TextTracks; | ||||
| }; | ||||
|  | ||||
| export type ReactVideoSource = Readonly< | ||||
| @@ -254,7 +255,7 @@ export type ControlsStyles = { | ||||
|  | ||||
| export interface ReactVideoProps extends ReactVideoEvents, ViewProps { | ||||
|   source?: ReactVideoSource; | ||||
|   /** @deprecated */ | ||||
|   /** @deprecated Use source.drm */ | ||||
|   drm?: Drm; | ||||
|   style?: StyleProp<ViewStyle>; | ||||
|   adTagUrl?: string; | ||||
| @@ -302,12 +303,13 @@ export interface ReactVideoProps extends ReactVideoEvents, ViewProps { | ||||
|   selectedVideoTrack?: SelectedVideoTrack; // android | ||||
|   subtitleStyle?: SubtitleStyle; // android | ||||
|   shutterColor?: string; // Android | ||||
|   /** @deprecated Use source.textTracks */ | ||||
|   textTracks?: TextTracks; | ||||
|   testID?: string; | ||||
|   viewType?: ViewType; | ||||
|   /** @deprecated */ | ||||
|   /** @deprecated Use viewType */ | ||||
|   useTextureView?: boolean; // Android | ||||
|   /** @deprecated */ | ||||
|   /** @deprecated Use viewType*/ | ||||
|   useSecureView?: boolean; // Android | ||||
|   volume?: number; | ||||
|   localSourceEncryptionKeyScheme?: string; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user