5 Commits

Author SHA1 Message Date
d7563cfd93 Fix video rendering with React Native 0.86
Fold the mobile repository's react-native-video patch into the maintained fork. Avoid the bridgeless RCTBridge.current() trap, preserve the TypeScript install build, and use the supported absolute-fill style so legacy-interoperability video views receive their layout.
2026-08-03 23:30:41 -07:00
69a9159a43 Load video duration asynchronously
Some checks failed
Build Android / Build Android Example App (pull_request) Has been cancelled
Build Android / Build Android Example App Without Ads (pull_request) Has been cancelled
Build iOS / Build iOS Example App (pull_request) Has been cancelled
Build iOS / Build iOS Example App With Ads (pull_request) Has been cancelled
Build iOS / Build iOS Example App With Caching (pull_request) Has been cancelled
Check Android / Kotlin-Lint (pull_request) Has been cancelled
Check CLang / CLang-Format (pull_request) Has been cancelled
Check iOS / Swift-Lint (pull_request) Has been cancelled
Check iOS / Swift-Format (pull_request) Has been cancelled
Check JS / Check TS (tsc) (pull_request) Has been cancelled
Check JS / Lint JS (eslint, prettier) (pull_request) Has been cancelled
Test Docs build / build-docs (pull_request) Has been cancelled
2026-07-23 13:20:28 -07:00
c6bd3e3ee4 use object fill 2026-01-21 11:35:19 -08:00
2d953c4c46 fix null-safety-checks and f bounded polymorphism 2025-11-26 13:17:41 -07:00
7a6afd52a3 Handle seek completion
Some checks failed
Build Android / Build Android Example App (pull_request) Has been cancelled
Build Android / Build Android Example App Without Ads (pull_request) Has been cancelled
Build iOS / Build iOS Example App (pull_request) Has been cancelled
Build iOS / Build iOS Example App With Ads (pull_request) Has been cancelled
Build iOS / Build iOS Example App With Caching (pull_request) Has been cancelled
Check Android / Kotlin-Lint (pull_request) Has been cancelled
Check CLang / CLang-Format (pull_request) Has been cancelled
Check iOS / Swift-Lint (pull_request) Has been cancelled
Check iOS / Swift-Format (pull_request) Has been cancelled
Check JS / Check TS (tsc) (pull_request) Has been cancelled
Check JS / Lint JS (eslint, prettier) (pull_request) Has been cancelled
Test Docs build / build-docs (pull_request) Has been cancelled
2024-12-04 12:51:28 -07:00
11 changed files with 68 additions and 18 deletions

View File

@@ -24,7 +24,7 @@ class SideLoadedTextTrackList {
}
val sideLoadedTextTrackList = SideLoadedTextTrackList()
for (i in 0 until src.size()) {
val textTrack: ReadableMap = src.getMap(i)
val textTrack: ReadableMap = src.getMap(i) ?: continue
sideLoadedTextTrackList.tracks.add(SideLoadedTextTrack.parse(textTrack))
}
return sideLoadedTextTrackList

View File

@@ -228,7 +228,7 @@ class Source {
if (propSrcHeadersArray != null) {
if (propSrcHeadersArray.size() > 0) {
for (i in 0 until propSrcHeadersArray.size()) {
val current = propSrcHeadersArray.getMap(i)
val current = propSrcHeadersArray.getMap(i) ?: continue
val key = if (current.hasKey("key")) current.getString("key") else null
val value = if (current.hasKey("value")) current.getString("value") else null
if (key != null && value != null) {

View File

@@ -288,12 +288,22 @@ class VideoEventEmitter {
}
}
private class VideoEvent(
surfaceId: Int,
viewId: Int,
private val name: String,
private val data: WritableMap?
) : Event<VideoEvent>(surfaceId, viewId) {
override fun getEventName() = name
override fun getEventData() = data
}
private class EventBuilder(private val surfaceId: Int, private val viewId: Int, private val dispatcher: EventDispatcher) {
fun dispatch(event: EventTypes, paramsSetter: (WritableMap.() -> Unit)? = null) =
dispatcher.dispatchEvent(object : Event<Event<*>>(surfaceId, viewId) {
override fun getEventName() = "top${event.eventName.removePrefix("on")}"
override fun getEventData() = Arguments.createMap().apply(paramsSetter ?: {})
})
fun dispatch(event: EventTypes, paramsSetter: (WritableMap.() -> Unit)? = null) {
val eventName = "top${event.eventName.removePrefix("on")}"
val eventData = Arguments.createMap().apply(paramsSetter ?: {})
dispatcher.dispatchEvent(VideoEvent(surfaceId, viewId, eventName, eventData))
}
}
private fun audioTracksToArray(audioTracks: java.util.ArrayList<Track>?): WritableArray =

View File

@@ -34,6 +34,22 @@ enum RCTVideoAssetsUtils {
#endif
}
}
static func getDuration(asset: AVAsset) async -> CMTime? {
if #available(iOS 15, tvOS 15, visionOS 1.0, *) {
return try? await asset.load(.duration)
} else {
#if !os(visionOS)
return await withCheckedContinuation { continuation in
asset.loadValuesAsynchronously(forKeys: ["duration"]) {
var error: NSError?
let status = asset.statusOfValue(forKey: "duration", error: &error)
continuation.resume(returning: status == .loaded ? asset.duration : nil)
}
}
#endif
}
}
}
// MARK: - RCTVideoUtils

View File

@@ -1420,7 +1420,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
}
if onVideoLoad != nil, self._videoLoadStarted {
var duration = Float(CMTimeGetSeconds(_playerItem.asset.duration))
let assetDuration = await RCTVideoAssetsUtils.getDuration(asset: _playerItem.asset)
guard self._playerItem === _playerItem,
self._source?.json === source.json else { return }
var duration = Float(CMTimeGetSeconds(assetDuration ?? .invalid))
if duration.isNaN || duration == 0 {
// This is a safety check for live video.
@@ -1449,6 +1452,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
let audioTracks = await RCTVideoUtils.getAudioTrackInfo(self._player)
let textTracks = await RCTVideoUtils.getTextTrackInfo(self._player)
guard self._playerItem === _playerItem,
self._source?.json === source.json else { return }
self.onVideoLoad?(["duration": NSNumber(value: duration),
"currentTime": NSNumber(value: Float(CMTimeGetSeconds(_playerItem.currentTime()))),
"canPlayReverse": NSNumber(value: _playerItem.canPlayReverse),
@@ -1699,4 +1704,3 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
@objc
func setOnClick(_: Any) {}
}

View File

@@ -4,7 +4,13 @@ import React
@objc(RCTVideoManager)
class RCTVideoManager: RCTViewManager {
override func view() -> UIView {
return RCTVideo(eventDispatcher: (RCTBridge.current().eventDispatcher() as! RCTEventDispatcher))
// `RCTBridge.current()` returns nil under bridgeless (New Architecture), and it
// is nonnull-annotated, so Swift emits an unconditional nil-check trap around
// the call -- optional chaining does not avoid it. Every <Video> mount killed
// the app. RCTVideo only stores this dispatcher and never reads it (events go
// out through the RCT_EXPORT_VIEW_PROPERTY direct event blocks), so skip the
// lookup entirely.
return RCTVideo(eventDispatcher: nil)
}
func methodQueue() -> DispatchQueue {

View File

@@ -43,6 +43,7 @@
"react-native": "*"
},
"scripts": {
"install": "tsc --noCheck",
"lint": "yarn eslint .",
"build": "yarn tsc",
"prepare": "yarn build",
@@ -61,6 +62,8 @@
"windows",
"src",
"lib",
"tsconfig.json",
"tsconfig.build.json",
"react-native-video.podspec",
"app.plugin.js",
"!android/build",

View File

@@ -734,7 +734,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
// poster style
const baseStyle: StyleProp<ImageStyle> = {
...StyleSheet.absoluteFillObject,
...StyleSheet.absoluteFill,
resizeMode: _posterResizeMode,
};
@@ -787,7 +787,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
const _style: StyleProp<ViewStyle> = useMemo(
() => ({
...StyleSheet.absoluteFillObject,
...StyleSheet.absoluteFill,
...(showPoster ? {display: 'none'} : {}),
}),
[showPoster],

View File

@@ -91,6 +91,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
onError,
onReadyForDisplay,
onSeek,
onSeekComplete,
onVolumeChange,
onEnd,
onPlaybackStateChanged,
@@ -488,7 +489,16 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
})
}
onSeeking={() => (isSeeking.current = true)}
onSeeked={() => (isSeeking.current = false)}
onSeeked={() => {
(isSeeking.current = false)
onSeekComplete?.({
currentTime: (nativeRef.current?.currentTime || 0.0) - cropStartSeconds,
seekTime: 0.0,
target: 0.0,
})
}}
onVolumeChange={() => {
if (!nativeRef.current) {
return;
@@ -505,7 +515,9 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
const videoStyle = {
position: 'absolute',
inset: 0,
objectFit: 'contain',
// Use 'fill' instead of 'contain' to force video to match container dimensions.
// This works around browsers miscalculating intrinsic dimensions from rotation matrices.
objectFit: 'fill',
width: '100%',
height: '100%',
} satisfies React.CSSProperties;

View File

@@ -1,4 +1,3 @@
{
"extends": "./tsconfig",
"exclude": ["examples", "lib"]

View File

@@ -14,8 +14,8 @@
"forceConsistentCasingInFileNames": true,
"jsx": "react",
"lib": ["esnext"],
"module": "CommonJS",
"moduleResolution": "node",
"module": "Node16",
"moduleResolution": "Node16",
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noImplicitUseStrict": false,