Compare commits

...

47 Commits

Author SHA1 Message Date
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
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
27 changed files with 2092 additions and 1456 deletions

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)
}

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

@ -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) {}
}

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

@ -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": "*"

View 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
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);
@ -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={

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

@ -0,0 +1,453 @@
import React, {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useRef,
type RefObject,
} from 'react';
//@ts-ignore
import shaka from 'shaka-player';
import type {VideoRef, ReactVideoProps, VideoMetadata} from './types';
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,
onVolumeChange,
onEnd,
onPlaybackStateChanged,
},
ref,
) => {
const nativeRef = useRef<HTMLVideoElement>(null);
const shakaPlayerRef = useRef<shaka.Player | null>(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;
}
nativeRef.current.play();
}, []);
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]);
useEffect(() => {
if (!nativeRef.current) {
console.log("Not starting shaka yet bc undefined")
return;
}
if (shakaPlayerRef.current) {
shakaPlayerRef.current.unload()
}
shakaPlayerRef.current = new shaka.Player();
//@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")
shakaPlayerRef.current.attach(nativeRef.current, true);
//@ts-ignore
shakaPlayerRef.current.load(source?.uri).then(
() => console.log(`${source?.uri} finished loading`)
);
console.log("Started shaka loading");
}, [source, nativeRef.current])
useMediaSession(source?.metadata, nativeRef, showNotificationControls);
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,
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)}
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;

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
}