Fix Railbird regressions found during v6.19.2 review
Some checks failed
Build Android / Build Android Example App (pull_request) Has been cancelled
Build Android / Build Android Example App With 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 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
Some checks failed
Build Android / Build Android Example App (pull_request) Has been cancelled
Build Android / Build Android Example App With 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 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
This commit is contained in:
@@ -73,7 +73,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 onVideoSeekComplete: (currentPosition: Long, seekTime: Long) -> Unit
|
||||
lateinit var onVideoEnd: () -> Unit
|
||||
lateinit var onVideoFullscreenPlayerWillPresent: () -> Unit
|
||||
lateinit var onVideoFullscreenPlayerDidPresent: () -> Unit
|
||||
@@ -202,9 +202,11 @@ class VideoEventEmitter {
|
||||
putDouble("seekTime", seekTime / 1000.0)
|
||||
}
|
||||
}
|
||||
onVideoSeekComplete = { currentPosition ->
|
||||
onVideoSeekComplete = { currentPosition, seekTime ->
|
||||
event.dispatch(EventTypes.EVENT_SEEK_COMPLETE) {
|
||||
putDouble("currentTime", currentPosition / 1000.0)
|
||||
putDouble("seekTime", seekTime / 1000.0)
|
||||
putInt("target", view.id)
|
||||
}
|
||||
}
|
||||
onVideoEnd = {
|
||||
|
||||
@@ -319,7 +319,10 @@ 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());
|
||||
if (isSeeking) {
|
||||
eventEmitter.onVideoSeek.invoke(player.getCurrentPosition(), seekPosition);
|
||||
}
|
||||
eventEmitter.onVideoSeekComplete.invoke(player.getCurrentPosition(), seekPosition);
|
||||
isSeeking = false;
|
||||
seekPosition = -1;
|
||||
isSeekInProgress = false;
|
||||
@@ -1847,6 +1850,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
// We need to update the selected track to make sure that it still matches user selection if track list has changed in this period
|
||||
setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue);
|
||||
}
|
||||
handleSeekCompletion();
|
||||
}
|
||||
|
||||
if (playerNeedsSource) {
|
||||
|
||||
@@ -801,8 +801,6 @@ The documentation for this prop is incomplete and will be updated as each option
|
||||
|
||||
<PlatformsList types={['Android', 'iOS', 'visionOS', 'Windows UWP']} />
|
||||
|
||||
<PlatformsList types={['Android', 'iOS', 'visionOS', 'Windows UWP']} />
|
||||
|
||||
Example:
|
||||
|
||||
Pass the asset directly (deprecated):
|
||||
|
||||
@@ -22,6 +22,7 @@ Then follow the instructions for your platform to link `react-native-video` into
|
||||
## iOS
|
||||
|
||||
### Standard Method
|
||||
|
||||
Run `pod install` in the `ios` directory of your project.
|
||||
|
||||
⚠️ From version `6.0.0`, the minimum iOS version required is `13.0`. For more information, see the [updating section](updating.md).
|
||||
@@ -100,6 +101,7 @@ You can enable or disable the following features by setting the corresponding va
|
||||
Each enabled feature increases the APK size, so only enable what you need.
|
||||
|
||||
By default, the enabled features are:
|
||||
|
||||
- `useExoplayerSmoothStreaming`
|
||||
- `useExoplayerDash`
|
||||
- `useExoplayerHls`
|
||||
@@ -222,17 +224,10 @@ Run `pod install` in the `visionos` directory of your project.
|
||||
|
||||
## Web
|
||||
|
||||
No additional setup is required. Everything should work out of the box.
|
||||
No additional setup is required.
|
||||
|
||||
However, only basic video support is available. HLS, Dash, ads, and DRM are not currently supported.
|
||||
|
||||
</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.
|
||||
The Railbird fork uses Shaka Player for browser playback, including HLS and DASH
|
||||
sources. The native ads and DRM props are not currently integrated with the web
|
||||
player.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -66,39 +66,25 @@ class ActionQueue {
|
||||
}
|
||||
}
|
||||
|
||||
function shallowEqual(obj1: unknown, obj2: unknown): boolean {
|
||||
// If both are strictly equal (covers primitive types and identical object references)
|
||||
if (obj1 === obj2) {
|
||||
function isDeepEqual<T>(first: T, second: T): boolean {
|
||||
if (first === second) {
|
||||
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
|
||||
) {
|
||||
const bothAreObjects =
|
||||
first && second && typeof first === 'object' && typeof second === 'object';
|
||||
if (!bothAreObjects) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const first = obj1 as Record<string, unknown>;
|
||||
const second = obj2 as Record<string, unknown>;
|
||||
|
||||
// Get the keys of both objects
|
||||
const keys1 = Object.keys(first);
|
||||
const keys2 = Object.keys(second);
|
||||
|
||||
// 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 first[key] === second[key];
|
||||
});
|
||||
const firstObject = first as Record<string, unknown>;
|
||||
const secondObject = second as Record<string, unknown>;
|
||||
return (
|
||||
Object.keys(firstObject).length === Object.keys(secondObject).length &&
|
||||
Object.entries(firstObject).every(([key, value]) =>
|
||||
isDeepEqual(value, secondObject[key]),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
@@ -136,10 +122,17 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
const shakaPlayerRef = useRef<shaka.Player | null>(null);
|
||||
const [activeSource, setActiveSource] = useState(source);
|
||||
const sourceProp = useRef(source);
|
||||
const [loadedSource, setLoadedSource] = useState<object | null>(null);
|
||||
const [loadedSource, setLoadedSource] = useState<{
|
||||
source: ReactVideoSource | undefined;
|
||||
} | null>(null);
|
||||
const actionQueue = useRef(new ActionQueue());
|
||||
const mountedRef = useRef(true);
|
||||
const sourceGenerationRef = useRef(0);
|
||||
const pausedRef = useRef(paused);
|
||||
pausedRef.current = paused;
|
||||
|
||||
const isSeeking = useRef(false);
|
||||
const isProgrammaticSeek = useRef(false);
|
||||
|
||||
const seek = useCallback(
|
||||
(time: number, _tolerance?: number) => {
|
||||
@@ -152,7 +145,13 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
return;
|
||||
}
|
||||
time = Math.max(0, Math.min(time, nativeRef.current.duration));
|
||||
nativeRef.current.currentTime = time;
|
||||
isProgrammaticSeek.current = true;
|
||||
try {
|
||||
nativeRef.current.currentTime = time;
|
||||
} catch (error) {
|
||||
isProgrammaticSeek.current = false;
|
||||
throw error;
|
||||
}
|
||||
onSeek?.({
|
||||
seekTime: time,
|
||||
currentTime: nativeRef.current.currentTime,
|
||||
@@ -210,7 +209,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (shallowEqual(source, sourceProp.current)) {
|
||||
if (isDeepEqual(source, sourceProp.current)) {
|
||||
return;
|
||||
}
|
||||
sourceProp.current = source;
|
||||
@@ -286,22 +285,22 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
);
|
||||
|
||||
const enterPictureInPicture = useCallback(() => {
|
||||
actionQueue.current.enqueue(async () => {
|
||||
if (!nativeRef.current) {
|
||||
console.warn('Video Component is not mounted');
|
||||
return;
|
||||
}
|
||||
await nativeRef.current.requestPictureInPicture();
|
||||
}, 'enterPictureInPicture');
|
||||
if (!nativeRef.current) {
|
||||
console.warn('Video Component is not mounted');
|
||||
return;
|
||||
}
|
||||
nativeRef.current.requestPictureInPicture().catch((error: unknown) => {
|
||||
console.error('Could not enter Picture-in-Picture', error);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const exitPictureInPicture = useCallback(() => {
|
||||
actionQueue.current.enqueue(async () => {
|
||||
if (nativeRef.current !== document.pictureInPictureElement) {
|
||||
return;
|
||||
}
|
||||
await document.exitPictureInPicture();
|
||||
}, 'exitPictureInPicture');
|
||||
if (nativeRef.current !== document.pictureInPictureElement) {
|
||||
return;
|
||||
}
|
||||
document.exitPictureInPicture().catch((error: unknown) => {
|
||||
console.error('Could not exit Picture-in-Picture', error);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(
|
||||
@@ -382,46 +381,76 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
}, [rate]);
|
||||
|
||||
const makeNewShaka = useCallback(() => {
|
||||
const sourceToLoad = activeSource;
|
||||
const generation = ++sourceGenerationRef.current;
|
||||
isSeeking.current = false;
|
||||
isProgrammaticSeek.current = false;
|
||||
|
||||
actionQueue.current.enqueue(async () => {
|
||||
if (!nativeRef.current) {
|
||||
const video = nativeRef.current;
|
||||
if (
|
||||
!mountedRef.current ||
|
||||
!video ||
|
||||
generation !== sourceGenerationRef.current
|
||||
) {
|
||||
console.warn('No video element to attach Shaka Player');
|
||||
return;
|
||||
}
|
||||
|
||||
// Pause the video before changing the source
|
||||
nativeRef.current.pause();
|
||||
video.pause();
|
||||
|
||||
// Unload the previous Shaka player if it exists
|
||||
if (shakaPlayerRef.current) {
|
||||
await shakaPlayerRef.current.unload();
|
||||
await shakaPlayerRef.current.destroy();
|
||||
const previousPlayer = shakaPlayerRef.current;
|
||||
if (previousPlayer) {
|
||||
shakaPlayerRef.current = null;
|
||||
await previousPlayer.unload().catch((error: unknown) => {
|
||||
console.error('Error unloading previous Shaka Player', error);
|
||||
});
|
||||
await previousPlayer.destroy().catch((error: unknown) => {
|
||||
console.error('Error destroying previous Shaka Player', error);
|
||||
});
|
||||
}
|
||||
|
||||
const sourceUri = activeSource?.uri;
|
||||
if (
|
||||
!mountedRef.current ||
|
||||
nativeRef.current !== video ||
|
||||
generation !== sourceGenerationRef.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceUri = sourceToLoad?.uri;
|
||||
if (typeof sourceUri !== 'string' || !sourceUri) {
|
||||
nativeRef.current.removeAttribute('src');
|
||||
nativeRef.current.load();
|
||||
video.removeAttribute('src');
|
||||
video.load();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new Shaka player and attach it to the video element
|
||||
shakaPlayerRef.current = new shaka.Player();
|
||||
const player = new shaka.Player();
|
||||
shakaPlayerRef.current = player;
|
||||
|
||||
await shakaPlayerRef.current.attach(nativeRef.current);
|
||||
const isCurrentPlayer = () =>
|
||||
mountedRef.current &&
|
||||
nativeRef.current === video &&
|
||||
generation === sourceGenerationRef.current &&
|
||||
shakaPlayerRef.current === player;
|
||||
|
||||
if (activeSource.cropStart) {
|
||||
shakaPlayerRef.current.configure({
|
||||
playRangeStart: activeSource.cropStart / 1000,
|
||||
const destroyStalePlayer = async () => {
|
||||
if (shakaPlayerRef.current !== player) {
|
||||
return;
|
||||
}
|
||||
shakaPlayerRef.current = null;
|
||||
await player.destroy().catch((error: unknown) => {
|
||||
console.error('Error destroying stale Shaka Player', error);
|
||||
});
|
||||
}
|
||||
if (activeSource.cropEnd) {
|
||||
shakaPlayerRef.current.configure({
|
||||
playRangeEnd: activeSource.cropEnd / 1000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
shakaPlayerRef.current.addEventListener('error', (event) => {
|
||||
player.addEventListener('error', (event) => {
|
||||
if (!isCurrentPlayer()) {
|
||||
return;
|
||||
}
|
||||
const shakaError = (
|
||||
event as CustomEvent<{message?: string; code?: number}>
|
||||
).detail;
|
||||
@@ -438,18 +467,43 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
|
||||
// Load the new source
|
||||
try {
|
||||
await shakaPlayerRef.current.load(sourceUri);
|
||||
await player.attach(video);
|
||||
if (!isCurrentPlayer()) {
|
||||
await destroyStalePlayer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (sourceToLoad.cropStart) {
|
||||
player.configure({
|
||||
playRangeStart: sourceToLoad.cropStart / 1000,
|
||||
});
|
||||
}
|
||||
if (sourceToLoad.cropEnd) {
|
||||
player.configure({
|
||||
playRangeEnd: sourceToLoad.cropEnd / 1000,
|
||||
});
|
||||
}
|
||||
|
||||
await player.load(sourceUri);
|
||||
if (!isCurrentPlayer()) {
|
||||
await destroyStalePlayer();
|
||||
return;
|
||||
}
|
||||
console.log(`${sourceUri} finished loading`);
|
||||
|
||||
// Optionally resume playback if not paused
|
||||
if (!paused) {
|
||||
if (!pausedRef.current) {
|
||||
try {
|
||||
await nativeRef.current.play();
|
||||
await video.play();
|
||||
} catch (e) {
|
||||
console.error('Error playing video:', e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (!isCurrentPlayer()) {
|
||||
await destroyStalePlayer();
|
||||
return;
|
||||
}
|
||||
console.error('Error loading video with Shaka Player', e);
|
||||
const shakaError = e as {message?: string; code?: number};
|
||||
onError?.({
|
||||
@@ -460,7 +514,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
});
|
||||
}
|
||||
}, 'makeNewShaka');
|
||||
}, [activeSource, paused, onError]);
|
||||
}, [activeSource, onError]);
|
||||
|
||||
const nativeRefDefined = !!nativeRef.current;
|
||||
|
||||
@@ -471,20 +525,26 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!shallowEqual(activeSource, loadedSource)) {
|
||||
if (
|
||||
loadedSource === null ||
|
||||
!isDeepEqual(activeSource, loadedSource.source)
|
||||
) {
|
||||
console.log(
|
||||
'Making new shaka, Old source: ',
|
||||
loadedSource,
|
||||
loadedSource?.source,
|
||||
'New source',
|
||||
activeSource,
|
||||
);
|
||||
setLoadedSource(activeSource ?? null);
|
||||
setLoadedSource({source: activeSource});
|
||||
makeNewShaka();
|
||||
}
|
||||
}, [activeSource, nativeRefDefined, loadedSource, makeNewShaka]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
sourceGenerationRef.current += 1;
|
||||
const player = shakaPlayerRef.current;
|
||||
shakaPlayerRef.current = null;
|
||||
if (player) {
|
||||
@@ -492,9 +552,8 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
console.error('Error destroying Shaka Player', error);
|
||||
});
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -612,11 +671,19 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
}
|
||||
onSeeking={() => (isSeeking.current = true)}
|
||||
onSeeked={() => {
|
||||
const currentTime = nativeRef.current?.currentTime || 0.0;
|
||||
if (isSeeking.current && !isProgrammaticSeek.current) {
|
||||
onSeek?.({
|
||||
currentTime,
|
||||
seekTime: currentTime,
|
||||
});
|
||||
}
|
||||
|
||||
isSeeking.current = false;
|
||||
isProgrammaticSeek.current = false;
|
||||
|
||||
onSeekComplete?.({
|
||||
currentTime:
|
||||
(nativeRef.current?.currentTime || 0.0) - cropStartSeconds,
|
||||
currentTime: currentTime - cropStartSeconds,
|
||||
seekTime: 0.0,
|
||||
target: 0.0,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user