Compare commits
61 Commits
master
...
async-queu
Author | SHA1 | Date | |
---|---|---|---|
7a6afd52a3 | |||
d7977241c9 | |||
921ead0f05 | |||
20397d32e6 | |||
e3900e794d | |||
4dc7bf465f | |||
e5f182cda9 | |||
9138c3249d | |||
7a1d0e8b10 | |||
9cbba8f95e | |||
2cfb26d51f | |||
4f18e9b238 | |||
bd64379837 | |||
47151c7119 | |||
a16275b003 | |||
56c129aa4f | |||
68cbe3c4b1 | |||
81e864c0e1 | |||
0e9ac4d125 | |||
9191a06600 | |||
dc61c3efea | |||
11f480f206 | |||
9619e7517b | |||
3ddc6e931a | |||
6b5831dc1c | |||
3fc002f3fd | |||
edb5c6bcfa | |||
5bc975b2c9 | |||
d79b5c9a83 | |||
f72b44d4df | |||
d2ab22b99f | |||
2dcde42fd6 | |||
c7a45d421b | |||
f0db0a6868 | |||
01b3322e03 | |||
13beae1401 | |||
f3deabd75e | |||
d69729dc04 | |||
|
6768c22139 | ||
|
2b369df57d | ||
|
8542c8f7d1 | ||
|
fc5b2d4563 | ||
|
ffb4631854 | ||
|
29cf7c97c3 | ||
|
491ed77a32 | ||
|
5b199b52b4 | ||
|
9d19157654 | ||
|
3dabf5f16f | ||
|
e610a274d5 | ||
|
27880f5212 | ||
|
39dd30b762 | ||
|
edf5d0c613 | ||
|
975fc2f303 | ||
|
aa85d71b87 | ||
|
cce24cd829 | ||
|
cad63d465d | ||
|
f5fa063bc0 | ||
|
c6abcdeb2f | ||
|
a72ab331dc | ||
|
fa126de97f | ||
|
ca2452edb6 |
@ -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)
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
BIN
docs/bun.lockb
BIN
docs/bun.lockb
Binary file not shown.
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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": {
|
||||
|
@ -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',
|
||||
|
@ -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
@ -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
|
||||
@ -1682,3 +1699,4 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
@objc
|
||||
func setOnClick(_: Any) {}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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",
|
||||
@ -32,9 +32,12 @@
|
||||
"react-native": "0.73.2",
|
||||
"react-native-windows": "^0.61.0-0",
|
||||
"release-it": "^16.2.1",
|
||||
"typescript": "5.1.6"
|
||||
"typescript": "5.1.6",
|
||||
"patch-package": "^8.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"shaka-player": "^4.11.7"
|
||||
},
|
||||
"dependencies": {},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
|
39
patches/shaka-player+4.11.7.patch
Normal file
39
patches/shaka-player+4.11.7.patch
Normal file
@ -0,0 +1,39 @@
|
||||
diff --git a/node_modules/shaka-player/dist/shaka-player.compiled.d.ts b/node_modules/shaka-player/dist/shaka-player.compiled.d.ts
|
||||
index 19c0930..cc0a3fd 100644
|
||||
--- a/node_modules/shaka-player/dist/shaka-player.compiled.d.ts
|
||||
+++ b/node_modules/shaka-player/dist/shaka-player.compiled.d.ts
|
||||
@@ -5117,3 +5117,5 @@ declare namespace shaka.extern {
|
||||
declare namespace shaka.extern {
|
||||
type TransmuxerPlugin = ( ) => shaka.extern.Transmuxer ;
|
||||
}
|
||||
+
|
||||
+export default shaka;
|
||||
diff --git a/node_modules/shaka-player/dist/shaka-player.ui.d.ts b/node_modules/shaka-player/dist/shaka-player.ui.d.ts
|
||||
index 1618ca0..a6076c6 100644
|
||||
--- a/node_modules/shaka-player/dist/shaka-player.ui.d.ts
|
||||
+++ b/node_modules/shaka-player/dist/shaka-player.ui.d.ts
|
||||
@@ -5830,3 +5830,5 @@ declare namespace shaka.extern {
|
||||
declare namespace shaka.extern {
|
||||
type UIVolumeBarColors = { base : string , level : string } ;
|
||||
}
|
||||
+
|
||||
+export default shaka;
|
||||
diff --git a/node_modules/shaka-player/index.d.ts b/node_modules/shaka-player/index.d.ts
|
||||
new file mode 100644
|
||||
index 0000000..3ebfd96
|
||||
--- /dev/null
|
||||
+++ b/node_modules/shaka-player/index.d.ts
|
||||
@@ -0,0 +1,2 @@
|
||||
+/// <reference path="./dist/shaka-player.compiled.d.ts" />
|
||||
+/// <reference path="./dist/shaka-player.ui.d.ts" />
|
||||
\ No newline at end of file
|
||||
diff --git a/node_modules/shaka-player/ui.d.ts b/node_modules/shaka-player/ui.d.ts
|
||||
new file mode 100644
|
||||
index 0000000..84a3be0
|
||||
--- /dev/null
|
||||
+++ b/node_modules/shaka-player/ui.d.ts
|
||||
@@ -0,0 +1,3 @@
|
||||
+import shaka from 'shaka-player/dist/shaka-player.ui'
|
||||
+export * from 'shaka-player/dist/shaka-player.ui'
|
||||
+export default shaka;
|
||||
\ No newline at end of file
|
13
shell.nix
Normal file
13
shell.nix
Normal 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; })
|
||||
];
|
||||
}
|
||||
|
@ -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);
|
||||
@ -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={
|
||||
|
609
src/Video.web.tsx
Normal file
609
src/Video.web.tsx
Normal file
@ -0,0 +1,609 @@
|
||||
import React, {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
type RefObject,
|
||||
} from 'react';
|
||||
//@ts-ignore
|
||||
import shaka from 'shaka-player';
|
||||
import type {VideoRef, ReactVideoProps, VideoMetadata} from './types';
|
||||
|
||||
// Action Queue Class
|
||||
class ActionQueue {
|
||||
private queue: { action: () => Promise<void>; name: string }[] = [];
|
||||
private isRunning = false;
|
||||
|
||||
enqueue(action: () => Promise<void>, name: string) {
|
||||
this.queue.push({ action, name });
|
||||
this.runNext();
|
||||
}
|
||||
|
||||
private async runNext() {
|
||||
if (this.isRunning || this.queue.length === 0) {
|
||||
console.log("Refusing to run in runNext", this.queue.length, this.isRunning);
|
||||
return;
|
||||
}
|
||||
this.isRunning = true;
|
||||
const { action, name } = this.queue.shift()!;
|
||||
console.log(`Running action: ${name}`);
|
||||
|
||||
const actionPromise = action();
|
||||
const timeoutPromise = new Promise<void>((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`Action ${name} timed out`)), 2000)
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.race([actionPromise, timeoutPromise]);
|
||||
} catch (e) {
|
||||
console.error('Error in queued action:', e);
|
||||
} finally {
|
||||
this.isRunning = false;
|
||||
this.runNext();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function shallowEqual(obj1: any, obj2: any) {
|
||||
// If both are strictly equal (covers primitive types and identical object references)
|
||||
if (obj1 === obj2) return true;
|
||||
|
||||
// If one is not an object (meaning it's a primitive), they must be strictly equal
|
||||
if (typeof obj1 !== 'object' || typeof obj2 !== 'object' || obj1 === null || obj2 === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the keys of both objects
|
||||
const keys1 = Object.keys(obj1);
|
||||
const keys2 = Object.keys(obj2);
|
||||
|
||||
// If the number of keys is different, the objects are not equal
|
||||
if (keys1.length !== keys2.length) return false;
|
||||
|
||||
// Check that all keys and their corresponding values are the same
|
||||
return keys1.every(key => {
|
||||
// If the value is an object, we fall back to reference equality (shallow comparison)
|
||||
return obj1[key] === obj2[key];
|
||||
});
|
||||
}
|
||||
|
||||
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 shakaPlayerRef = useRef<shaka.Player | null>(null);
|
||||
const [currentSource, setCurrentSource] = useState<object | null>(null);
|
||||
const actionQueue = useRef(new ActionQueue());
|
||||
|
||||
const isSeeking = useRef(false);
|
||||
|
||||
const seek = useCallback(
|
||||
(time: number, _tolerance?: number) => {
|
||||
actionQueue.current.enqueue(async () => {
|
||||
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,
|
||||
});
|
||||
}, 'seek');
|
||||
},
|
||||
[onSeek],
|
||||
);
|
||||
|
||||
const pause = useCallback(() => {
|
||||
actionQueue.current.enqueue(async () => {
|
||||
if (!nativeRef.current) {
|
||||
return;
|
||||
}
|
||||
await nativeRef.current.pause();
|
||||
}, 'pause');
|
||||
}, []);
|
||||
|
||||
const resume = useCallback(() => {
|
||||
actionQueue.current.enqueue(async () => {
|
||||
if (!nativeRef.current) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await nativeRef.current.play();
|
||||
} catch (e) {
|
||||
console.error('Error playing video:', e);
|
||||
}
|
||||
}, 'resume');
|
||||
}, []);
|
||||
|
||||
const setVolume = useCallback((vol: number) => {
|
||||
actionQueue.current.enqueue(async () => {
|
||||
if (!nativeRef.current) {
|
||||
return;
|
||||
}
|
||||
nativeRef.current.volume = Math.max(0, Math.min(vol, 100)) / 100;
|
||||
}, 'setVolume');
|
||||
}, []);
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
actionQueue.current.enqueue(run, 'setFullScreen');
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
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]);
|
||||
|
||||
const makeNewShaka = useCallback(() => {
|
||||
console.log("makeNewShaka");
|
||||
actionQueue.current.enqueue(async () => {
|
||||
console.log("makeNewShaka actionQueue");
|
||||
if (!nativeRef.current) {
|
||||
console.warn('No video element to attach Shaka Player');
|
||||
return;
|
||||
}
|
||||
|
||||
// Pause the video before changing the source
|
||||
nativeRef.current.pause();
|
||||
|
||||
// Unload the previous Shaka player if it exists
|
||||
if (shakaPlayerRef.current) {
|
||||
await shakaPlayerRef.current.unload();
|
||||
shakaPlayerRef.current = null;
|
||||
}
|
||||
|
||||
// Create a new Shaka player and attach it to the video element
|
||||
shakaPlayerRef.current = new shaka.Player();
|
||||
|
||||
shakaPlayerRef.current.attach(nativeRef.current);
|
||||
|
||||
if (source?.cropStart) {
|
||||
shakaPlayerRef.current.configure({
|
||||
playRangeStart: source?.cropStart / 1000,
|
||||
});
|
||||
}
|
||||
if (source?.cropEnd) {
|
||||
shakaPlayerRef.current.configure({
|
||||
playRangeEnd: source?.cropEnd / 1000,
|
||||
});
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
shakaPlayerRef.current.addEventListener('error', event => {
|
||||
//@ts-ignore
|
||||
const shakaError = event.detail;
|
||||
console.error('Shaka Player Error', shakaError);
|
||||
onError?.({
|
||||
error: {
|
||||
errorString: shakaError.message,
|
||||
code: shakaError.code,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
console.log('Initializing and attaching shaka');
|
||||
|
||||
// Load the new source
|
||||
try {
|
||||
//@ts-ignore
|
||||
await shakaPlayerRef.current.load(source?.uri);
|
||||
console.log(`${source?.uri} finished loading`);
|
||||
|
||||
// Optionally resume playback if not paused
|
||||
if (!paused) {
|
||||
try {
|
||||
await nativeRef.current.play();
|
||||
} catch (e) {
|
||||
console.error('Error playing video:', e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading video with Shaka Player', e);
|
||||
onError?.({
|
||||
error: {
|
||||
//@ts-ignore
|
||||
errorString: e.message,
|
||||
//@ts-ignore
|
||||
code: e.code,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, 'makeNewShaka');
|
||||
}, [source, paused, onError]);
|
||||
|
||||
const nativeRefDefined = !!nativeRef.current;
|
||||
|
||||
useEffect(() => {
|
||||
if (!nativeRef.current) {
|
||||
console.log('Not starting shaka yet because video element is undefined');
|
||||
return;
|
||||
}
|
||||
if (!shallowEqual(source, currentSource)) {
|
||||
console.log(
|
||||
'Making new shaka, Old source: ',
|
||||
currentSource,
|
||||
'New source',
|
||||
source,
|
||||
);
|
||||
//@ts-ignore
|
||||
setCurrentSource(source);
|
||||
makeNewShaka();
|
||||
}
|
||||
}, [source, nativeRefDefined, currentSource, makeNewShaka]);
|
||||
|
||||
useMediaSession(source?.metadata, nativeRef, showNotificationControls);
|
||||
|
||||
const cropStartSeconds = (source?.cropStart || 0) / 1000;
|
||||
|
||||
return (
|
||||
<video
|
||||
ref={nativeRef}
|
||||
muted={muted}
|
||||
autoPlay={!paused}
|
||||
controls={controls}
|
||||
loop={repeat}
|
||||
playsInline
|
||||
//@ts-ignore
|
||||
poster={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 - cropStartSeconds,
|
||||
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)
|
||||
|
||||
onSeekComplete?.({
|
||||
currentTime: (nativeRef.current?.currentTime || 0.0) - cropStartSeconds,
|
||||
seekTime: 0.0,
|
||||
target: 0.0,
|
||||
|
||||
})
|
||||
}}
|
||||
onVolumeChange={() => {
|
||||
if (!nativeRef.current) {
|
||||
return;
|
||||
}
|
||||
onVolumeChange?.({volume: nativeRef.current.volume});
|
||||
}}
|
||||
onEnded={onEnd}
|
||||
style={videoStyle}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const videoStyle = {
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
objectFit: 'contain',
|
||||
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;
|
34
src/VideoDecoderProperties.web.ts
Normal file
34
src/VideoDecoderProperties.web.ts
Normal 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;
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
21
src/types/video-ref.ts
Normal 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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user