66 Commits

Author SHA1 Message Date
b01e55488e Replace Shaka Player with hls.js on web
Some checks failed
Check JS / Check TS (tsc) (pull_request) Has been cancelled
Check JS / Lint JS (eslint, prettier) (pull_request) Has been cancelled
The web player carried a Shaka Player integration plus the scaffolding
built up around it while debugging: an ActionQueue that serialized every
play/pause/seek/volume call behind a 2s timeout, a shallowEqual source
comparison, and a large amount of debug logging. All of it existed to
work around Shaka's async attach/load lifecycle.

Every source this fork's consumer plays is HLS, and the only thing Shaka
was providing is HLS playback in browsers without native support. hls.js
does that in a fraction of the code, so drive the video element directly:
set `src` when the browser plays HLS natively (Safari/WebKit) or the
source isn't HLS at all, and attach hls.js otherwise.

Dropping the queue restores the original direct play/pause/seek/volume
implementations. `resume()` now swallows play() rejections, which the
queue used to absorb -- these are routine when play() lands before the
manifest is parsed or when autoplay is denied.

The source effect keys on the URI string rather than the source object.
Consumers pass `source={{uri}}` as a fresh object literal every render,
which is what shallowEqual was defending against.

Carried forward from the Shaka implementation: the onSeekComplete event
and objectFit: 'fill'. Also resolves `poster` to a URL string instead of
suppressing the type error with @ts-ignore.

Dropped: web-side cropStart/cropEnd handling, which was implemented with
Shaka's playRangeStart/playRangeEnd. No consumer passes those props, and
neither upstream's web player nor this file before Shaka supported them.
2026-08-04 01:06:45 -07:00
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
d7977241c9 Account for crop in progress 2024-10-18 03:25:50 -06:00
921ead0f05 Timeout actions 2024-10-17 19:21:06 -06:00
20397d32e6 More logging 2024-10-17 19:11:22 -06:00
e3900e794d What is going on 2024-10-17 19:07:39 -06:00
4dc7bf465f Typescript 2024-10-17 18:59:42 -06:00
e5f182cda9 Use an async queue 2024-10-17 18:56:38 -06:00
9138c3249d Try to fix double load 2024-10-17 18:34:34 -06:00
7a1d0e8b10 Try to fix source quality issue 2024-10-17 18:28:16 -06:00
9cbba8f95e Typescript again 2024-10-17 18:22:26 -06:00
2cfb26d51f Typescript 2024-10-17 18:20:28 -06:00
4f18e9b238 Fix never ending loading, try to respect crops 2024-10-17 18:19:05 -06:00
bd64379837 Merge pull request 'Fix android build' (#7) from volodymyr/fix-android-build into feat/shaka
Reviewed-on: #7
Reviewed-by: Ivan Malison <ivanmalison@gmail.com>
2024-10-14 07:01:12 -06:00
47151c7119 Fix android build
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
Check Android / Kotlin-Lint (pull_request) Has been cancelled
2024-10-14 11:02:21 +02:00
a16275b003 ts-ignores 2024-10-13 22:48:51 -06:00
56c129aa4f Don't patch package anymore 2024-10-13 22:45:06 -06:00
68cbe3c4b1 Don't require rate to be defined 2024-10-13 20:45:25 -06:00
81e864c0e1 Update shaka when nativeRef changes 2024-10-13 19:17:01 -06:00
0e9ac4d125 Shaka loggin 2024-10-13 15:14:48 -06:00
9191a06600 Simple shaka 2024-10-13 15:04:44 -06:00
dc61c3efea Fix tsc 2024-10-13 14:18:49 -06:00
11f480f206 Start muted 2024-10-13 14:15:28 -06:00
9619e7517b Try again 2024-10-13 02:06:12 -06:00
3ddc6e931a More hacks 2024-10-13 01:09:57 -06:00
6b5831dc1c Hacks 2024-10-13 01:07:48 -06:00
3fc002f3fd Fix ts issues 2024-10-13 00:44:54 -06:00
edb5c6bcfa Merge remote-tracking branch 'railbird/master' into feat/shaka 2024-10-12 23:52:26 -06:00
5bc975b2c9 Shaka? 2024-10-12 23:48:55 -06:00
d79b5c9a83 Merge remote-tracking branch 'origin/master' into feat/web 2024-10-12 23:40:00 -06:00
f72b44d4df Merge pull request 'Implement onSeekComplete for Android' (#5) from kat/temp-4 into master
Some checks failed
Build Android / Build Android Example App (push) Has been cancelled
Build Android / Build Android Example App Without Ads (push) Has been cancelled
Check Android / Kotlin-Lint (push) Has been cancelled
Reviewed-on: #5
2024-08-26 17:23:32 -06:00
d2ab22b99f wip
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
Check Android / Kotlin-Lint (pull_request) Has been cancelled
2024-08-26 17:22:38 -06:00
2dcde42fd6 Add logs 2024-08-26 17:22:38 -06:00
c7a45d421b Only complete seek if seek was in progress 2024-08-26 17:22:38 -06:00
f0db0a6868 kat wip 2024-08-26 17:22:38 -06:00
01b3322e03 Log in seek 2024-08-26 17:22:38 -06:00
13beae1401 Merge pull request 'Implement ios onSeekComplete' (#3) from kat/seek-complete-ios into master
Some checks failed
Build iOS / Build iOS Example App (push) Has been cancelled
Build iOS / Build iOS Example App With Ads (push) Has been cancelled
Build iOS / Build iOS Example App With Caching (push) Has been cancelled
Check CLang / CLang-Format (push) Has been cancelled
Check iOS / Swift-Lint (push) Has been cancelled
Check iOS / Swift-Format (push) Has been cancelled
Reviewed-on: #3
2024-08-20 22:37:10 -06:00
f3deabd75e Implement ios onSeekComplete
Some checks failed
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 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
2024-08-20 22:24:00 -06:00
d69729dc04 expose-on-seek-complete (#1)
Some checks failed
Build iOS / Build iOS Example App (push) Has been cancelled
Build iOS / Build iOS Example App With Ads (push) Has been cancelled
Build iOS / Build iOS Example App With Caching (push) Has been cancelled
Check CLang / CLang-Format (push) Has been cancelled
Check iOS / Swift-Lint (push) Has been cancelled
Check iOS / Swift-Format (push) Has been cancelled
Check JS / Check TS (tsc) (push) Has been cancelled
Check JS / Lint JS (eslint, prettier) (push) Has been cancelled
Reviewed-on: #1
2024-08-06 00:11:17 -06:00
Zoe Roux
6768c22139 Add nativeHtmlVideoRef doc 2024-07-16 12:31:32 +07:00
Zoe Roux
2b369df57d Fix native import on web 2024-07-16 12:31:32 +07:00
Zoe Roux
8542c8f7d1 Add isSeeking on web 2024-07-16 12:31:32 +07:00
Zoe Roux
fc5b2d4563 Add web support for fullscreen 2024-07-16 11:33:50 +07:00
Zoe Roux
ffb4631854 Remove unused errorHandler ref 2024-07-16 11:33:50 +07:00
Zoe Roux
29cf7c97c3 Update doc for web 2024-07-16 11:33:50 +07:00
Zoe Roux
491ed77a32 Renamve nativeHtmlRef to nativeHtmlVideoRef 2024-07-16 11:33:50 +07:00
Zoe Roux
5b199b52b4 Move video style to const var 2024-07-16 11:33:50 +07:00
Zoe Roux
9d19157654 Add web command in basic example 2024-07-16 11:33:50 +07:00
Zoe Roux
3dabf5f16f Fix and improve VideoNativeComponent (canPlay/isWidewine supportd) for web 2024-07-16 11:33:50 +07:00
Zoe Roux
e610a274d5 Prevent playback state change loop on web 2024-07-16 11:33:50 +07:00
Zoe Roux
27880f5212 Update the doc for web things 2024-07-16 11:33:50 +07:00
Zoe Roux
39dd30b762 Cleanup media session handling on the web 2024-07-16 11:33:50 +07:00
Zoe Roux
edf5d0c613 Test notifications on web 2024-07-16 11:33:50 +07:00
Zoe Roux
975fc2f303 Fix web bugs 2024-07-16 11:33:49 +07:00
Zoe Roux
aa85d71b87 Make the basic example app work on web 2024-07-16 11:33:18 +07:00
Zoe Roux
cce24cd829 Add media session support 2024-07-16 11:33:18 +07:00
Zoe Roux
cad63d465d Add most properties 2024-07-16 11:33:18 +07:00
Zoe Roux
f5fa063bc0 Add most events 2024-07-16 11:33:18 +07:00
Zoe Roux
c6abcdeb2f Create ref handling and basics stollen from Kyoo 2024-07-16 11:33:18 +07:00
Zoe Roux
a72ab331dc Move video ref type to its own file 2024-07-16 11:33:15 +07:00
Zoe Roux
fa126de97f Add VideoDecoderProperties for the web 2024-07-16 11:24:52 +07:00
Zoe Roux
ca2452edb6 Add shell.nix for nix users 2024-07-16 11:20:12 +07:00
32 changed files with 2149 additions and 1470 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

@@ -22,6 +22,7 @@ enum class EventTypes(val eventName: String) {
EVENT_BANDWIDTH("onVideoBandwidthUpdate"),
EVENT_CONTROLS_VISIBILITY_CHANGE("onControlsVisibilityChange"),
EVENT_SEEK("onVideoSeek"),
EVENT_SEEK_COMPLETE("onVideoSeekComplete"),
EVENT_END("onVideoEnd"),
EVENT_FULLSCREEN_WILL_PRESENT("onVideoFullscreenPlayerWillPresent"),
EVENT_FULLSCREEN_DID_PRESENT("onVideoFullscreenPlayerDidPresent"),
@@ -71,6 +72,7 @@ class VideoEventEmitter {
lateinit var onVideoBandwidthUpdate: (bitRateEstimate: Long, height: Int, width: Int, trackId: String?) -> Unit
lateinit var onVideoPlaybackStateChanged: (isPlaying: Boolean, isSeeking: Boolean) -> Unit
lateinit var onVideoSeek: (currentPosition: Long, seekTime: Long) -> Unit
lateinit var onVideoSeekComplete: (currentPosition: Long) -> Unit
lateinit var onVideoEnd: () -> Unit
lateinit var onVideoFullscreenPlayerWillPresent: () -> Unit
lateinit var onVideoFullscreenPlayerDidPresent: () -> Unit
@@ -174,6 +176,11 @@ class VideoEventEmitter {
putDouble("seekTime", seekTime / 1000.0)
}
}
onVideoSeekComplete = { currentPosition ->
event.dispatch(EventTypes.EVENT_SEEK_COMPLETE) {
putDouble("currentTime", currentPosition / 1000.0)
}
}
onVideoEnd = {
event.dispatch(EventTypes.EVENT_END)
}
@@ -281,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

@@ -23,6 +23,7 @@ import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.accessibility.CaptioningManager;
import android.widget.FrameLayout;
@@ -224,6 +225,7 @@ public class ReactExoplayerView extends FrameLayout implements
*/
private boolean isSeeking = false;
private long seekPosition = -1;
private boolean isSeekInProgress = false;
// Props from React
private Source source = new Source();
@@ -303,6 +305,16 @@ public class ReactExoplayerView extends FrameLayout implements
}
};
private void handleSeekCompletion() {
if (player != null && player.getPlaybackState() == Player.STATE_READY && isSeekInProgress) {
Log.d("ReactExoplayerView", "handleSeekCompletion: currentPosition=" + player.getCurrentPosition());
eventEmitter.onVideoSeekComplete.invoke(player.getCurrentPosition());
isSeeking = false;
seekPosition = -1;
isSeekInProgress = false;
}
}
public double getPositionInFirstPeriodMsForCurrentWindow(long currentPosition) {
Timeline.Window window = new Timeline.Window();
if(!player.getCurrentTimeline().isEmpty()) {
@@ -521,12 +533,21 @@ public class ReactExoplayerView extends FrameLayout implements
builder.setSingleChoiceItems(speedOptions, selectedSpeedIndex, (dialog, which) -> {
selectedSpeedIndex = which;
float speed = switch (which) {
case 0 -> 0.5f;
case 2 -> 1.5f;
case 3 -> 2.0f;
default -> 1.0f;
};
float speed;
switch (which) {
case 0:
speed = 0.5f;
break;
case 1:
speed = 1.0f;
break;
case 2:
speed = 1.5f;
break;
default:
speed = 1.0f;
break;
}
setRateModifier(speed);
});
builder.show();
@@ -852,7 +873,8 @@ public class ReactExoplayerView extends FrameLayout implements
.setBandwidthMeter(bandwidthMeter)
.setLoadControl(loadControl)
.setMediaSourceFactory(mediaSourceFactory)
.build();
.build();
player.addListener(self);
ReactNativeVideoManager.Companion.getInstance().onInstanceCreated(instanceId, player);
refreshDebugState();
player.addListener(self);
@@ -1437,6 +1459,7 @@ public class ReactExoplayerView extends FrameLayout implements
if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED) || events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) {
int playbackState = player.getPlaybackState();
boolean playWhenReady = player.getPlayWhenReady();
Log.d("ReactExoplayerView", "onEvents: playbackState=" + playbackState + ", playWhenReady=" + playWhenReady);
String text = "onStateChanged: playWhenReady=" + playWhenReady + ", playbackState=";
eventEmitter.onPlaybackRateChange.invoke(playWhenReady && playbackState == ExoPlayer.STATE_READY ? 1.0f : 0.0f);
switch (playbackState) {
@@ -1470,6 +1493,10 @@ public class ReactExoplayerView extends FrameLayout implements
playerControlView.show();
}
setKeepScreenOn(preventsDisplaySleepDuringVideoPlayback);
Log.d("ReactExoplayerView", "Player STATE_READY: currentPosition=" + player.getCurrentPosition());
if (isSeekInProgress) {
handleSeekCompletion();
}
break;
case Player.STATE_ENDED:
text += "ended";
@@ -1734,6 +1761,7 @@ public class ReactExoplayerView extends FrameLayout implements
@Override
public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, @Player.DiscontinuityReason int reason) {
Log.d("ReactExoplayerView", "onPositionDiscontinuity: reason=" + reason + ", oldPosition=" + oldPosition.positionMs + ", newPosition=" + newPosition.positionMs);
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
isSeeking = true;
seekPosition = newPosition.positionMs;
@@ -2249,6 +2277,10 @@ public class ReactExoplayerView extends FrameLayout implements
public void seekTo(long positionMs) {
if (player != null) {
Log.d("ReactExoplayerView", "seekTo: positionMs=" + positionMs);
isSeekInProgress = true;
isSeeking = true;
seekPosition = positionMs;
player.seekTo(positionMs);
}
}

Binary file not shown.

View File

@@ -103,7 +103,7 @@ Note: On Android, you must set the [reportBandwidth](#reportbandwidth) prop to e
### `onBuffer`
<PlatformsList types={['Android', 'iOS']} />
<PlatformsList types={['Android', 'iOS', 'web']} />
Callback function that is called when the player buffers.
@@ -219,6 +219,9 @@ Payload: none
Callback function that is called when the media is loaded and ready to play.
NOTE: tracks (`audioTracks`, `textTracks` & `videoTracks`) are not available on the web.
Payload:
| Property | Type | Description |
@@ -292,7 +295,7 @@ Example:
### `onPlaybackStateChanged`
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
<PlatformsList types={['Android', 'iOS', 'visionOS', 'web']} />
Callback function that is called when the playback state changes.
@@ -463,7 +466,7 @@ Payload: none
### `onSeek`
<PlatformsList types={['Android', 'iOS', 'Windows UWP']} />
<PlatformsList types={['Android', 'iOS', 'Windows UWP', 'web']} />
Callback function that is called when a seek completes.
@@ -604,7 +607,7 @@ Example:
### `onVolumeChange`
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
<PlatformsList types={['Android', 'iOS', 'visionOS', 'web']} />
Callback function that is called when the volume of player changes.

View File

@@ -6,7 +6,7 @@ This page shows the list of available methods
### `dismissFullscreenPlayer`
<PlatformsList types={['Android', 'iOS']} />
<PlatformsList types={['Android', 'iOS', 'web']} />
`dismissFullscreenPlayer(): Promise<void>`
@@ -17,7 +17,7 @@ Take the player out of fullscreen mode.
### `pause`
<PlatformsList types={['Android', 'iOS']} />
<PlatformsList types={['Android', 'iOS', 'web']} />
`pause(): Promise<void>`
@@ -25,7 +25,7 @@ Pause the video.
### `presentFullscreenPlayer`
<PlatformsList types={['Android', 'iOS']} />
<PlatformsList types={['Android', 'iOS', 'web']} />
`presentFullscreenPlayer(): Promise<void>`
@@ -40,7 +40,7 @@ On Android, this puts the navigation controls in fullscreen mode. It is not a co
### `resume`
<PlatformsList types={['Android', 'iOS']} />
<PlatformsList types={['Android', 'iOS', 'web']} />
`resume(): Promise<void>`
@@ -100,7 +100,7 @@ tolerance is the max distance in milliseconds from the seconds position that's a
### `setVolume`
<PlatformsList types={['Android', 'iOS']} />
<PlatformsList types={['Android', 'iOS', 'web']} />
`setVolume(value): Promise<void>`
@@ -108,7 +108,7 @@ This function will change the volume exactly like [volume](./props#volume) prope
### `getCurrentPosition`
<PlatformsList types={['Android', 'iOS']} />
<PlatformsList types={['Android', 'iOS', 'web']} />
`getCurrentPosition(): Promise<number>`
@@ -127,7 +127,7 @@ Changing source with this function will overide source provided as props.
### `setFullScreen`
<PlatformsList types={['Android', 'iOS']} />
<PlatformsList types={['Android', 'iOS', 'web']} />
`setFullScreen(fullscreen): Promise<void>`
@@ -137,6 +137,13 @@ On iOS, this displays the video in a fullscreen view controller with controls.
On Android, this puts the navigation controls in fullscreen mode. It is not a complete fullscreen implementation, so you will still need to apply a style that makes the width and height match your screen dimensions to get a fullscreen video.
### `nativeHtmlVideoRef`
<PlatformsList types={['web']} />
A ref to the underlying html video element. This can be used if you need to integrate a 3d party, web only video library (like hls.js, shaka, video.js...).
### Example Usage
```tsx
@@ -188,7 +195,7 @@ Possible values are:
### `isCodecSupported`
<PlatformsList types={['Android']} />
<PlatformsList types={['Android', 'web']} />
Indicates whether the provided codec is supported level supported by device.

View File

@@ -131,7 +131,7 @@ When playing an HLS live stream with a `EXT-X-PROGRAM-DATE-TIME` tag configured,
### `controls`
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
<PlatformsList types={['Android', 'iOS', 'visionOS', 'web']} />
Determines whether to show player controls.
@@ -300,7 +300,7 @@ Whether this video view should be focusable with a non-touch input device, eg. r
### `fullscreen`
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
<PlatformsList types={['Android', 'iOS', 'visionOS', 'web']} />
Controls whether the player enters fullscreen on play.
See [presentFullscreenPlayer](#presentfullscreenplayer) for details.
@@ -316,7 +316,7 @@ If a preferred [fullscreenOrientation](#fullscreenorientation) is set, causes th
### `fullscreenOrientation`
<PlatformsList types={['iOS', 'visionOS']} />
<PlatformsList types={['iOS', 'visionOS', 'web']} />
- **all (default)** -
- **landscape**
@@ -707,6 +707,8 @@ The docs for this prop are incomplete and will be updated as each option is inve
> ⚠️ on iOS, you file name must not contain spaces eg. `my video.mp4` will not work, use `my-video.mp4` instead
<PlatformsList types={['Android', 'iOS', 'visionOS', 'Windows UWP']} />
Example:
Pass directly the asset to play (deprecated)
@@ -818,7 +820,7 @@ Example:
#### Start playback at a specific point in time
<PlatformsList types={['Android', 'iOS']} />
<PlatformsList types={['Android', 'iOS', 'web']} />
Provide an optional `startPosition` for video. Value is in milliseconds. If the `cropStart` prop is applied, it will be applied from that point forward.
(If it is negative or undefined or null, it is ignored)
@@ -1029,7 +1031,7 @@ textTracks={[
### `showNotificationControls`
<PlatformsList types={['Android', 'iOS']} />
<PlatformsList types={['Android', 'iOS', 'web']} />
Controls whether to show media controls in the notification area.
For Android each Video component will have its own notification controls and for iOS only one notification control will be shown for the last Active Video component.

View File

@@ -8,6 +8,7 @@ It allows to stream video files (m3u, mpd, mp4, ...) inside your react native ap
- Exoplayer for android
- AVplayer for iOS, tvOS and visionOS
- Windows UWP for windows
- HTML5 for web
- Trick mode support
- Subtitles (embeded or side loaded)
- DRM support

View File

@@ -181,3 +181,12 @@ Select RCTVideo-tvOS
Run `pod install` in the `visionos` directory of your project
</details>
<details>
<summary>web</summary>
Nothing to do, everything should work out of the box.
Note that only basic video support is present, no hls/dash or ads/drm for now.
</details>

View File

@@ -3,6 +3,7 @@
"version": "1.0.0",
"private": true,
"scripts": {
"web": "expo start --web",
"android": "expo run:android",
"ios": "expo run:ios",
"windows": "react-native run-windows",
@@ -13,6 +14,7 @@
"pod-install:newarch": "cd ios && RCT_NEW_ARCH_ENABLED=1 bundle exec pod install && cd .."
},
"dependencies": {
"@expo/metro-runtime": "~3.2.1",
"@react-native-picker/picker": "2.7.5",
"expo": "^51.0.32",
"expo-asset": "~10.0.10",
@@ -20,6 +22,8 @@
"expo-navigation-bar": "~3.0.7",
"react": "18.2.0",
"react-native": "0.74.5",
"react-dom": "18.2.0",
"react-native-web": "~0.19.10",
"react-native-windows": "0.74.19"
},
"devDependencies": {

View File

@@ -78,6 +78,14 @@ export const srcAllPlatformList = [
description: 'another bunny (can be saved)',
uri: 'https://rawgit.com/mediaelement/mediaelement-files/master/big_buck_bunny.mp4',
headers: {referer: 'www.github.com', 'User-Agent': 'react.native.video'},
metadata: {
title: 'Custom Title',
subtitle: 'Custom Subtitle',
artist: 'Custom Artist',
description: 'Custom Description',
imageUri:
'https://pbs.twimg.com/profile_images/1498641868397191170/6qW2XkuI_400x400.png',
},
},
{
description: 'sintel with subtitles',

View File

@@ -1,5 +1,4 @@
import {AppRegistry} from 'react-native';
import {registerRootComponent} from 'expo';
import VideoPlayer from './VideoPlayer';
import {name as appName} from '../app.json';
AppRegistry.registerComponent(appName, () => VideoPlayer);
registerRootComponent(VideoPlayer);

File diff suppressed because it is too large Load Diff

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

@@ -113,6 +113,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
@objc var onVideoProgress: RCTDirectEventBlock?
@objc var onVideoBandwidthUpdate: RCTDirectEventBlock?
@objc var onVideoSeek: RCTDirectEventBlock?
@objc var onVideoSeekComplete: RCTDirectEventBlock?
@objc var onVideoEnd: RCTDirectEventBlock?
@objc var onTimedMetadata: RCTDirectEventBlock?
@objc var onVideoAudioBecomingNoisy: RCTDirectEventBlock?
@@ -782,34 +783,50 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
_paused = paused
}
@objc
func setSeek(_ time: NSNumber, _ tolerance: NSNumber) {
let item: AVPlayerItem? = _player?.currentItem
_pendingSeek = true
guard item != nil, let player = _player, let item, item.status == AVPlayerItem.Status.readyToPlay else {
_pendingSeek = true
_pendingSeekTime = time.floatValue
return
}
RCTPlayerOperations.seek(
player: player,
playerItem: item,
paused: _paused,
seekTime: time.floatValue,
seekTolerance: tolerance.floatValue
) { [weak self] (_: Bool) in
guard let self else { return }
let wasPaused = _paused
let seekTime = CMTimeMakeWithSeconds(Float64(time.floatValue), preferredTimescale: Int32(NSEC_PER_SEC))
let toleranceTime = CMTimeMakeWithSeconds(Float64(tolerance.floatValue), preferredTimescale: Int32(NSEC_PER_SEC))
let currentTimeBeforeSeek = CMTimeGetSeconds(item.currentTime())
// Call onVideoSeek before starting the seek operation
let currentTime = NSNumber(value: Float(currentTimeBeforeSeek))
self.onVideoSeek?(["currentTime": currentTime,
"seekTime": time,
"target": self.reactTag])
_pendingSeek = true
let seekCompletionHandler: (Bool) -> Void = { [weak self] finished in
guard let self = self else { return }
self._pendingSeek = false
guard finished else {
return
}
self._playerObserver.addTimeObserverIfNotSet()
self.setPaused(self._paused)
self.onVideoSeek?(["currentTime": NSNumber(value: Float(CMTimeGetSeconds(item.currentTime()))),
"seekTime": time,
"target": self.reactTag as Any])
let newCurrentTime = NSNumber(value: Float(CMTimeGetSeconds(item.currentTime())))
self.onVideoSeekComplete?(["currentTime": newCurrentTime,
"seekTime": time,
"target": self.reactTag as Any])
}
_pendingSeek = false
player.seek(to: seekTime, toleranceBefore: toleranceTime, toleranceAfter: toleranceTime, completionHandler: seekCompletionHandler)
}
@objc
@@ -1403,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.
@@ -1432,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),

View File

@@ -45,6 +45,7 @@ RCT_EXPORT_VIEW_PROPERTY(onVideoError, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onVideoProgress, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onVideoBandwidthUpdate, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onVideoSeek, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onVideoSeekComplete, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onVideoEnd, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onTimedMetadata, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onVideoAudioBecomingNoisy, RCTDirectEventBlock);

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

@@ -3,7 +3,7 @@
"version": "6.6.4",
"description": "A <Video /> element for react-native",
"main": "lib/index",
"source": "src/index",
"source": "src/index.ts",
"react-native": "src/index",
"license": "MIT",
"author": "Community Contributors",
@@ -34,12 +34,15 @@
"release-it": "^16.2.1",
"typescript": "5.1.6"
},
"dependencies": {},
"dependencies": {
"hls.js": "^1.6.16"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
},
"scripts": {
"install": "tsc --noCheck",
"lint": "yarn eslint .",
"build": "yarn tsc",
"prepare": "yarn build",
@@ -58,6 +61,8 @@
"windows",
"src",
"lib",
"tsconfig.json",
"tsconfig.build.json",
"react-native-video.podspec",
"app.plugin.js",
"!android/build",

13
shell.nix Normal file
View File

@@ -0,0 +1,13 @@
{pkgs ? import <nixpkgs> {}}:
pkgs.mkShell {
packages = with pkgs; [
nodejs-18_x
nodePackages.yarn
bun
eslint_d
prettierd
jdk11
(jdt-language-server.override { jdk = jdk11; })
];
}

View File

@@ -32,6 +32,7 @@ import type {
OnPlaybackStateChangedData,
OnProgressData,
OnSeekData,
OnSeekCompleteData,
OnTextTrackDataChangedData,
OnTimedMetadataData,
OnVideoAspectRatioData,
@@ -45,8 +46,7 @@ import {
resolveAssetSourceForVideo,
} from './utils';
import NativeVideoManager from './specs/NativeVideoManager';
import type {VideoSaveData} from './specs/NativeVideoManager';
import {CmcdMode, ViewType} from './types';
import {type VideoSaveData, CmcdMode, ViewType} from './types';
import type {
OnLoadData,
OnTextTracksData,
@@ -98,6 +98,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
onError,
onProgress,
onSeek,
onSeekComplete,
onEnd,
onBuffer,
onBandwidthUpdate,
@@ -470,6 +471,13 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
[onSeek],
);
const onVideoSeekComplete = useCallback(
(e: NativeSyntheticEvent<OnSeekCompleteData>) => {
onSeekComplete?.(e.nativeEvent);
},
[onSeekComplete]
);
const onVideoPlaybackStateChanged = useCallback(
(e: NativeSyntheticEvent<OnPlaybackStateChangedData>) => {
onPlaybackStateChanged?.(e.nativeEvent);
@@ -726,7 +734,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
// poster style
const baseStyle: StyleProp<ImageStyle> = {
...StyleSheet.absoluteFillObject,
...StyleSheet.absoluteFill,
resizeMode: _posterResizeMode,
};
@@ -779,7 +787,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
const _style: StyleProp<ViewStyle> = useMemo(
() => ({
...StyleSheet.absoluteFillObject,
...StyleSheet.absoluteFill,
...(showPoster ? {display: 'none'} : {}),
}),
[showPoster],
@@ -812,6 +820,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
onVideoError={onError ? onVideoError : undefined}
onVideoProgress={onProgress ? onVideoProgress : undefined}
onVideoSeek={onSeek ? onVideoSeek : undefined}
onVideoSeekComplete={onSeekComplete ? onVideoSeekComplete : undefined}
onVideoEnd={onEnd}
onVideoBuffer={onBuffer ? onVideoBuffer : undefined}
onVideoPlaybackStateChanged={

498
src/Video.web.tsx Normal file
View File

@@ -0,0 +1,498 @@
import React, {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useRef,
type RefObject,
} from 'react';
import Hls from 'hls.js';
import type {VideoRef, ReactVideoProps, VideoMetadata} from './types';
const HLS_MIME = 'application/vnd.apple.mpegurl';
const isHlsSource = (uri: string) => /\.m3u8(\?|#|$)/i.test(uri);
// `poster` is either a (deprecated) plain URI string or a poster object whose
// `source` follows the RN image-source shape. The DOM only takes a URL string.
const resolvePosterUri = (
poster: ReactVideoProps['poster'],
): string | undefined => {
if (typeof poster === 'string') {
return poster;
}
const source = poster?.source;
return typeof source === 'object' && source !== null ? source.uri : undefined;
};
const Video = forwardRef<VideoRef, ReactVideoProps>(
(
{
source,
paused,
muted,
volume,
rate,
repeat,
controls,
showNotificationControls = false,
poster,
fullscreen,
fullscreenAutorotate,
fullscreenOrientation,
onBuffer,
onLoad,
onProgress,
onPlaybackRateChange,
onError,
onReadyForDisplay,
onSeek,
onSeekComplete,
onVolumeChange,
onEnd,
onPlaybackStateChanged,
},
ref,
) => {
const nativeRef = useRef<HTMLVideoElement>(null);
const isSeeking = useRef(false);
const seek = useCallback(
async (time: number, _tolerance?: number) => {
if (isNaN(time)) {
throw new Error('Specified time is not a number');
}
if (!nativeRef.current) {
console.warn('Video Component is not mounted');
return;
}
time = Math.max(0, Math.min(time, nativeRef.current.duration));
nativeRef.current.currentTime = time;
onSeek?.({seekTime: time, currentTime: nativeRef.current.currentTime});
},
[onSeek],
);
const pause = useCallback(() => {
if (!nativeRef.current) {
return;
}
nativeRef.current.pause();
}, []);
const resume = useCallback(() => {
if (!nativeRef.current) {
return;
}
// play() rejects if it is called before the stream has data (common while
// hls.js is still parsing the manifest) or if autoplay is denied. Neither
// is fatal, so swallow it rather than surfacing an unhandled rejection.
nativeRef.current.play()?.catch(() => {});
}, []);
const setVolume = useCallback((vol: number) => {
if (!nativeRef.current) {
return;
}
nativeRef.current.volume = Math.max(0, Math.min(vol, 100)) / 100;
}, []);
const getCurrentPosition = useCallback(async () => {
if (!nativeRef.current) {
throw new Error('Video Component is not mounted');
}
return nativeRef.current.currentTime;
}, []);
const unsupported = useCallback(() => {
throw new Error('This is unsupported on the web');
}, []);
// Stock this in a ref to not invalidate memoization when those changes.
const fsPrefs = useRef({
fullscreenAutorotate,
fullscreenOrientation,
});
fsPrefs.current = {
fullscreenOrientation,
fullscreenAutorotate,
};
const setFullScreen = useCallback(
(
newVal: boolean,
orientation?: ReactVideoProps['fullscreenOrientation'],
autorotate?: boolean,
) => {
orientation ??= fsPrefs.current.fullscreenOrientation;
autorotate ??= fsPrefs.current.fullscreenAutorotate;
const run = async () => {
try {
if (newVal) {
await nativeRef.current?.requestFullscreen({
navigationUI: 'hide',
});
if (orientation === 'all' || !orientation || autorotate) {
screen.orientation.unlock();
} else {
await screen.orientation.lock(orientation);
}
} else {
if (document.fullscreenElement) {
await document.exitFullscreen();
}
screen.orientation.unlock();
}
} catch (e) {
// Changing fullscreen status without a button click is not allowed so it throws.
// Some browsers also used to throw when locking screen orientation was not supported.
console.error('Could not toggle fullscreen/screen lock status', e);
}
};
run();
},
[],
);
useEffect(() => {
setFullScreen(
fullscreen || false,
fullscreenOrientation,
fullscreenAutorotate,
);
}, [
setFullScreen,
fullscreen,
fullscreenAutorotate,
fullscreenOrientation,
]);
const presentFullscreenPlayer = useCallback(
() => setFullScreen(true),
[setFullScreen],
);
const dismissFullscreenPlayer = useCallback(
() => setFullScreen(false),
[setFullScreen],
);
useImperativeHandle(
ref,
() => ({
seek,
pause,
resume,
setVolume,
getCurrentPosition,
presentFullscreenPlayer,
dismissFullscreenPlayer,
setFullScreen,
save: unsupported,
restoreUserInterfaceForPictureInPictureStopCompleted: unsupported,
nativeHtmlVideoRef: nativeRef,
}),
[
seek,
pause,
resume,
unsupported,
setVolume,
getCurrentPosition,
nativeRef,
presentFullscreenPlayer,
dismissFullscreenPlayer,
setFullScreen,
],
);
useEffect(() => {
if (paused) {
pause();
} else {
resume();
}
}, [paused, pause, resume]);
useEffect(() => {
if (volume === undefined) {
return;
}
setVolume(volume);
}, [volume, setVolume]);
// we use a ref to prevent triggerring the useEffect when the component rerender with a non-stable `onPlaybackStateChanged`.
const playbackStateRef = useRef(onPlaybackStateChanged);
playbackStateRef.current = onPlaybackStateChanged;
useEffect(() => {
// Not sure about how to do this but we want to wait for nativeRef to be initialized
setTimeout(() => {
if (!nativeRef.current) {
return;
}
// Set play state to the player's value (if autoplay is denied)
// This is useful if our UI is in a play state but autoplay got denied so
// the video is actaully in a paused state.
playbackStateRef.current?.({
isPlaying: !nativeRef.current.paused,
isSeeking: isSeeking.current,
});
}, 500);
}, []);
useEffect(() => {
if (!nativeRef.current || rate === undefined) {
return;
}
nativeRef.current.playbackRate = rate;
}, [rate]);
// `onError` is typically a fresh closure on every render. Keep it in a ref so
// the source effect below can depend on the URI alone -- callers pass
// `source={{uri}}` as a new object literal each render, so keying the effect
// on anything object-shaped would tear down and reload the stream constantly.
const onErrorRef = useRef(onError);
onErrorRef.current = onError;
const uri = source?.uri as string | undefined;
useEffect(() => {
const video = nativeRef.current;
if (!video || !uri) {
return;
}
// Safari (and iOS WebKit) plays HLS natively; everywhere else the browser
// has no HLS support at all, so an .m3u8 has to go through hls.js.
const needsMse =
isHlsSource(uri) && !video.canPlayType(HLS_MIME) && Hls.isSupported();
if (!needsMse) {
video.src = uri;
return () => {
video.removeAttribute('src');
video.load();
};
}
const hls = new Hls();
hls.on(Hls.Events.ERROR, (_event, data) => {
// hls.js surfaces plenty of recoverable warnings; only fatal errors are
// worth propagating as a video error.
if (!data.fatal) {
return;
}
onErrorRef.current?.({
error: {
errorString: `${data.type}: ${data.details}`,
},
});
});
hls.loadSource(uri);
hls.attachMedia(video);
return () => {
hls.destroy();
};
}, [uri]);
useMediaSession(source?.metadata, nativeRef, showNotificationControls);
return (
<video
ref={nativeRef}
muted={muted}
autoPlay={!paused}
controls={controls}
loop={repeat}
playsInline
poster={resolvePosterUri(poster)}
onCanPlay={() => onBuffer?.({isBuffering: false})}
onWaiting={() => onBuffer?.({isBuffering: true})}
onRateChange={() => {
if (!nativeRef.current) {
return;
}
onPlaybackRateChange?.({
playbackRate: nativeRef.current?.playbackRate,
});
}}
onDurationChange={() => {
if (!nativeRef.current) {
return;
}
onLoad?.({
currentTime: nativeRef.current.currentTime,
duration: nativeRef.current.duration,
videoTracks: [],
textTracks: [],
audioTracks: [],
naturalSize: {
width: nativeRef.current.videoWidth,
height: nativeRef.current.videoHeight,
orientation: 'landscape',
},
});
}}
onTimeUpdate={() => {
if (!nativeRef.current) {
return;
}
onProgress?.({
currentTime: nativeRef.current.currentTime,
playableDuration: nativeRef.current.buffered.length
? nativeRef.current.buffered.end(
nativeRef.current.buffered.length - 1,
)
: 0,
seekableDuration: 0,
});
}}
onLoadedData={() => onReadyForDisplay?.()}
onError={() => {
if (!nativeRef.current?.error) {
return;
}
onError?.({
error: {
errorString: nativeRef.current.error.message ?? 'Unknown error',
code: nativeRef.current.error.code,
},
});
}}
onLoadedMetadata={() => {
if (source?.startPosition) {
seek(source.startPosition / 1000);
}
}}
onPlay={() =>
onPlaybackStateChanged?.({
isPlaying: true,
isSeeking: isSeeking.current,
})
}
onPause={() =>
onPlaybackStateChanged?.({
isPlaying: false,
isSeeking: isSeeking.current,
})
}
onSeeking={() => (isSeeking.current = true)}
onSeeked={() => {
isSeeking.current = false;
const currentTime = nativeRef.current?.currentTime ?? 0;
onSeekComplete?.({
currentTime,
seekTime: currentTime,
target: 0,
});
}}
onVolumeChange={() => {
if (!nativeRef.current) {
return;
}
onVolumeChange?.({volume: nativeRef.current.volume});
}}
onEnded={onEnd}
style={videoStyle}
/>
);
},
);
const videoStyle = {
position: 'absolute',
inset: 0,
// 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;
const useMediaSession = (
metadata: VideoMetadata | undefined,
nativeRef: RefObject<HTMLVideoElement>,
showNotification: boolean,
) => {
const isPlaying = !nativeRef.current?.paused ?? false;
const progress = nativeRef.current?.currentTime ?? 0;
const duration = Number.isFinite(nativeRef.current?.duration)
? nativeRef.current?.duration
: undefined;
const playbackRate = nativeRef.current?.playbackRate ?? 1;
const enabled = 'mediaSession' in navigator && showNotification;
useEffect(() => {
if (enabled) {
navigator.mediaSession.metadata = new MediaMetadata({
title: metadata?.title,
artist: metadata?.artist,
artwork: metadata?.imageUri ? [{src: metadata.imageUri}] : undefined,
});
}
}, [enabled, metadata]);
useEffect(() => {
if (!enabled) {
return;
}
const seekTo = (time: number) => {
if (nativeRef.current) {
nativeRef.current.currentTime = time;
}
};
const seekRelative = (offset: number) => {
if (nativeRef.current) {
nativeRef.current.currentTime = nativeRef.current.currentTime + offset;
}
};
const mediaActions: [
MediaSessionAction,
MediaSessionActionHandler | null,
][] = [
['play', () => nativeRef.current?.play()],
['pause', () => nativeRef.current?.pause()],
[
'seekbackward',
(evt: MediaSessionActionDetails) =>
seekRelative(evt.seekOffset ? -evt.seekOffset : -10),
],
[
'seekforward',
(evt: MediaSessionActionDetails) =>
seekRelative(evt.seekOffset ? evt.seekOffset : 10),
],
['seekto', (evt: MediaSessionActionDetails) => seekTo(evt.seekTime!)],
];
for (const [action, handler] of mediaActions) {
try {
navigator.mediaSession.setActionHandler(action, handler);
} catch {
// ignored
}
}
}, [enabled, nativeRef]);
useEffect(() => {
if (enabled) {
navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused';
}
}, [isPlaying, enabled]);
useEffect(() => {
if (enabled && duration !== undefined) {
navigator.mediaSession.setPositionState({
position: Math.min(progress, duration),
duration,
playbackRate: playbackRate,
});
}
}, [progress, duration, playbackRate, enabled]);
};
Video.displayName = 'Video';
export default Video;

View File

@@ -0,0 +1,34 @@
/// <reference lib="dom" />
import type {VideoDecoderInfoModuleType} from './specs/NativeVideoDecoderInfoModule';
const canPlay = (codec: string): boolean => {
// most chrome based browser (and safari I think) supports matroska but reports they do not.
// for those browsers, only check the codecs and not the container.
if (navigator.userAgent.search('Firefox') === -1) {
codec = codec.replace('video/x-matroska', 'video/mp4');
}
return !!MediaSource.isTypeSupported(codec);
};
export const VideoDecoderProperties = {
async getWidevineLevel() {
return 0;
},
async isCodecSupported(
mimeType: string,
_width: number,
_height: number,
): Promise<'unsupported' | 'hardware' | 'software'> {
// TODO: Figure out if we can get hardware support information
return canPlay(mimeType) ? 'software' : 'unsupported';
},
async isHEVCSupported(): Promise<'unsupported' | 'hardware' | 'software'> {
// Just a dummy vidoe mime type codec with HEVC to check.
return canPlay('video/x-matroska; codecs="hvc1.1.4.L96.BO"')
? 'software'
: 'unsupported';
},
} satisfies VideoDecoderInfoModuleType;

View File

@@ -1,6 +1,5 @@
import Video from './Video';
export {VideoDecoderProperties} from './VideoDecoderProperties';
export * from './types';
export type {VideoRef} from './Video';
export {Video};
export default Video;

View File

@@ -2,7 +2,7 @@ import {NativeModules} from 'react-native';
import type {Int32} from 'react-native/Libraries/Types/CodegenTypes';
// @TODO rename to "Spec" when applying new arch
interface VideoDecoderInfoModuleType {
export interface VideoDecoderInfoModuleType {
getWidevineLevel: () => Promise<Int32>;
isCodecSupported: (
mimeType: string,

View File

@@ -4,10 +4,7 @@ import type {
Float,
UnsafeObject,
} from 'react-native/Libraries/Types/CodegenTypes';
export type VideoSaveData = {
uri: string;
};
import type {VideoSaveData} from '../types/video-ref';
// @TODO rename to "Spec" when applying new arch
export interface VideoManagerType {

View File

@@ -202,6 +202,12 @@ export type OnSeekData = Readonly<{
seekTime: Float;
}>;
export type OnSeekCompleteData = Readonly<{
currentTime: number;
seekTime: number;
target: number;
}>;
export type OnPlaybackStateChangedData = Readonly<{
isPlaying: boolean;
isSeeking: boolean;
@@ -284,12 +290,12 @@ type OnReceiveAdEventData = Readonly<{
export type OnVideoErrorData = Readonly<{
error: Readonly<{
errorString?: string; // android
errorString?: string; // android | web
errorException?: string; // android
errorStackTrace?: string; // android
errorCode?: string; // android
error?: string; // ios
code?: Int32; // ios
code?: Int32; // ios | web
localizedDescription?: string; // ios
localizedFailureReason?: string; // ios
localizedRecoverySuggestion?: string; // ios
@@ -377,6 +383,7 @@ export interface VideoNativeProps extends ViewProps {
onVideoProgress?: DirectEventHandler<OnProgressData>;
onVideoBandwidthUpdate?: DirectEventHandler<OnBandwidthUpdateData>;
onVideoSeek?: DirectEventHandler<OnSeekData>;
onVideoSeekComplete?: DirectEventHandler<OnSeekCompleteData>;
onVideoEnd?: DirectEventHandler<{}>; // all
onVideoAudioBecomingNoisy?: DirectEventHandler<{}>;
onVideoFullscreenPlayerWillPresent?: DirectEventHandler<{}>; // ios, android

View File

@@ -12,6 +12,7 @@ import type {
OnPlaybackStateChangedData,
OnProgressData,
OnSeekData,
OnSeekCompleteData,
OnTextTrackDataChangedData,
OnTimedMetadataData,
OnVideoAspectRatioData,
@@ -20,6 +21,8 @@ import type {
OnVolumeChangeData,
} from '../specs/VideoNativeComponent';
export type * from '../specs/VideoNativeComponent';
export type AudioTrack = OnAudioTracksData['audioTracks'][number];
export type TextTrack = OnTextTracksData['textTracks'][number];
export type VideoTrack = OnVideoTracksData['videoTracks'][number];
@@ -258,6 +261,7 @@ export interface ReactVideoEvents {
onReceiveAdEvent?: (e: OnReceiveAdEventData) => void; //Android, iOS
onRestoreUserInterfaceForPictureInPictureStop?: () => void; //iOS
onSeek?: (e: OnSeekData) => void; //Android, iOS, Windows UWP
onSeekComplete?: (e: OnSeekCompleteData) => void; // iOS
onPlaybackStateChanged?: (e: OnPlaybackStateChangedData) => void; // Android, iOS
onTimedMetadata?: (e: OnTimedMetadataData) => void; //Android, iOS
onAudioTracks?: (e: OnAudioTracksData) => void; // Android

View File

@@ -7,4 +7,4 @@ export {default as ResizeMode} from './ResizeMode';
export {default as TextTrackType} from './TextTrackType';
export {default as ViewType} from './ViewType';
export * from './video';
export * from '../specs/VideoNativeComponent';
export * from './video-ref';

21
src/types/video-ref.ts Normal file
View File

@@ -0,0 +1,21 @@
import type {RefObject} from 'react';
export type VideoSaveData = {
uri: string;
};
export interface VideoRef {
seek: (time: number, tolerance?: number) => void;
resume: () => void;
pause: () => void;
presentFullscreenPlayer: () => void;
dismissFullscreenPlayer: () => void;
restoreUserInterfaceForPictureInPictureStopCompleted: (
restore: boolean,
) => void;
save: (options: object) => Promise<VideoSaveData>;
setVolume: (volume: number) => void;
getCurrentPosition: () => Promise<number>;
setFullScreen: (fullScreen: boolean) => void;
nativeHtmlVideoRef?: RefObject<HTMLVideoElement>; // web only
}

View File

@@ -1,5 +1,4 @@
{
"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,