From 66273c13053a367d968964e1ae6d75db4c327d78 Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Sun, 16 Nov 2025 21:44:23 +0100 Subject: [PATCH 01/38] Revert "Merge pull request #75219 from Expensify/revert-66793-Guccio163/expo-video" This reverts commit 930127cddf760ce2070e9628f0ef70b79a9b3e12, reversing changes made to 3efe8a00e4e467b2e42bcf453f086b3a6f34a7a3. --- __mocks__/expo-video.tsx | 114 +++++ config/webpack/webpack.common.ts | 1 + cspell.json | 1 + package-lock.json | 12 + package.json | 1 + patches/expo-av/details.md | 26 - ...+15.1.7+001+fix-blank-screen-android.patch | 221 --------- ....7+002+handle-unsupported-videos-ios.patch | 22 - patches/expo-video/details.md | 13 + ...ideo+3.0.11+001+prevent_double_pause.patch | 24 + src/components/AttachmentPreview.tsx | 29 +- .../Attachments/AttachmentView/index.tsx | 3 - src/components/EmptyStateComponent/index.tsx | 21 +- src/components/FeatureTrainingModal.tsx | 23 +- src/components/FullscreenLoadingIndicator.tsx | 2 +- .../VideoPlayer/BaseVideoPlayer.tsx | 449 ++++++++---------- .../VideoPlayer/VideoPlayerControls/index.tsx | 30 +- src/components/VideoPlayer/index.native.tsx | 2 +- src/components/VideoPlayer/index.tsx | 2 +- .../VideoPlayer/shouldReplayVideo.android.ts | 13 - .../VideoPlayer/shouldReplayVideo.ios.ts | 11 - .../VideoPlayer/shouldReplayVideo.ts | 9 - src/components/VideoPlayer/types.ts | 78 ++- .../useHandleNativeVideoControls/index.ts | 6 +- .../useHandleNativeVideoControls/types.ts | 17 +- src/components/VideoPlayer/utils.ts | 16 +- .../PlaybackContext/index.tsx | 31 +- .../PlaybackContext/types.ts | 134 +++++- .../usePlaybackContextVideoRefs.ts | 98 ++-- .../VideoPopoverMenuContext.tsx | 18 +- .../VideoPlayerContexts/VolumeContext.tsx | 4 +- src/components/VideoPlayerContexts/types.ts | 66 ++- src/components/VideoPlayerPreview/index.tsx | 16 +- 33 files changed, 767 insertions(+), 746 deletions(-) create mode 100644 __mocks__/expo-video.tsx delete mode 100644 patches/expo-av/details.md delete mode 100644 patches/expo-av/expo-av+15.1.7+001+fix-blank-screen-android.patch delete mode 100644 patches/expo-av/expo-av+15.1.7+002+handle-unsupported-videos-ios.patch create mode 100644 patches/expo-video/details.md create mode 100644 patches/expo-video/expo-video+3.0.11+001+prevent_double_pause.patch delete mode 100644 src/components/VideoPlayer/shouldReplayVideo.android.ts delete mode 100644 src/components/VideoPlayer/shouldReplayVideo.ios.ts delete mode 100644 src/components/VideoPlayer/shouldReplayVideo.ts diff --git a/__mocks__/expo-video.tsx b/__mocks__/expo-video.tsx new file mode 100644 index 0000000000000..dc6df5395365e --- /dev/null +++ b/__mocks__/expo-video.tsx @@ -0,0 +1,114 @@ +/* eslint-disable no-underscore-dangle */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +/* eslint-disable @typescript-eslint/no-unused-vars */ + +/* eslint-disable react-compiler/react-compiler */ +import React, {forwardRef, useRef} from 'react'; +import type {ForwardedRef} from 'react'; +import {View} from 'react-native'; +import type {ViewProps} from 'react-native'; + +type MutedChangeEventPayload = { + isMuted: boolean; +}; + +type PlayingChangeEventPayload = { + isPlaying: boolean; +}; + +type StatusChangeEventPayload = { + status: string; +}; + +type TimeUpdateEventPayload = { + currentTime: number; +}; + +type VideoPlayer = { + /** lightweight โ€œstateโ€ */ + isPlaying: boolean; + isMuted: boolean; + currentTime: number; + + /** controls */ + play: jest.Mock; + pause: jest.Mock; + replace: jest.Mock; + seekTo: jest.Mock; + setIsMuted: jest.Mock; + + /** simple event API */ + addListener: jest.Mock<{remove: () => void}, [string, (payload: unknown) => void]>; +}; + +function createMockPlayer(): VideoPlayer { + let _isPlaying = false; + let _isMuted = false; + let _currentTime = 0; + + return { + get isPlaying() { + return _isPlaying; + }, + get isMuted() { + return _isMuted; + }, + get currentTime() { + return _currentTime; + }, + + play: jest.fn(() => { + _isPlaying = true; + }), + pause: jest.fn(() => { + _isPlaying = false; + }), + replace: jest.fn((_opts?: unknown) => { + // no-op; exist to satisfy code that calls it + }), + seekTo: jest.fn((time: number) => { + _currentTime = time; + }), + setIsMuted: jest.fn((muted: boolean) => { + _isMuted = muted; + }), + + addListener: jest.fn((_event: string, _cb: (payload: unknown) => void) => { + // minimal, enough for code that calls .remove() + return {remove: () => {}}; + }), + }; +} + +/** + * Mocked hook โ€“ returns a stable mock player instance. + * Signature accepts any args to match real API calls. + */ +function useVideoPlayer(..._args: unknown[]): VideoPlayer { + const ref = useRef(null); + if (!ref.current) { + ref.current = createMockPlayer(); + } + return ref.current; +} + +/** + * Simple stand-in for the native VideoView. + * Accepts a `player` prop to mirror the real component. + */ +type VideoViewProps = ViewProps & {player?: VideoPlayer}; + +const VideoView = forwardRef((props: VideoViewProps, ref: ForwardedRef) => ( + // eslint-disable-next-line react/jsx-props-no-spreading + +)); + +export {useVideoPlayer, VideoView}; +export type {MutedChangeEventPayload, PlayingChangeEventPayload, StatusChangeEventPayload, TimeUpdateEventPayload}; diff --git a/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts index 4450bb12d66db..d8b8d683260bf 100644 --- a/config/webpack/webpack.common.ts +++ b/config/webpack/webpack.common.ts @@ -46,6 +46,7 @@ const includeModules = [ '@react-native/assets', 'expo', 'expo-av', + 'expo-video', 'expo-image-manipulator', 'expo-modules-core', ].join('|'); diff --git a/cspell.json b/cspell.json index ec608a23cfa70..744d8900db042 100644 --- a/cspell.json +++ b/cspell.json @@ -698,6 +698,7 @@ "unhold", "Unlaminated", "Unmigrated", + "unmuting", "unredacted", "unscrollable", "unsubmitted", diff --git a/package-lock.json b/package-lock.json index 1bbf17ab015ba..067b98ce8070d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,6 +73,7 @@ "expo-image-manipulator": "^13.1.5", "expo-modules-core": "3.0.18", "expo-secure-store": "~14.2.4", + "expo-video": "^3.0.11", "fast-equals": "^5.2.2", "focus-trap-react": "^11.0.3", "group-ib-fp": "file:modules/group-ib-fp", @@ -22988,6 +22989,17 @@ "expo": "*" } }, + "node_modules/expo-video": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/expo-video/-/expo-video-3.0.11.tgz", + "integrity": "sha512-k/xz8Ml/LekuD2U2LomML2mUISvkHzYDz3fXY8Au1fEaYVNTfTs7Gyfo1lvF6S1X7u3XutoAfew8e8e1ZUR2fg==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, "node_modules/expo/node_modules/@expo/cli": { "version": "54.0.8", "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-54.0.8.tgz", diff --git a/package.json b/package.json index ca7a5816fc6b4..4d51a84c06d57 100644 --- a/package.json +++ b/package.json @@ -144,6 +144,7 @@ "expo-image-manipulator": "^13.1.5", "expo-modules-core": "3.0.18", "expo-secure-store": "~14.2.4", + "expo-video": "^3.0.11", "fast-equals": "^5.2.2", "focus-trap-react": "^11.0.3", "group-ib-fp": "file:modules/group-ib-fp", diff --git a/patches/expo-av/details.md b/patches/expo-av/details.md deleted file mode 100644 index e24511e138a1d..0000000000000 --- a/patches/expo-av/details.md +++ /dev/null @@ -1,26 +0,0 @@ -# `expo-av` patches - -### [expo-av+15.1.7+001+fix-blank-screen-android.patch](expo-av+15.1.7+001+fix-blank-screen-android.patch) - -- Reason: - - ``` - This patch fixes blank modal after fullscreen video ends on Android. - ``` - -- Upstream PR/issue: The maintainer of the upstream lib isn't willing to accept fix for the lib because it's going to be replaced by another lib `expo-video`, see comment https://github.com/expo/expo/issues/19039#issuecomment-2688369708 -- E/App issue: https://github.com/Expensify/App/issues/53904 -- PR introducing patch: https://github.com/Expensify/App/pull/56302 - - -### [expo-av+15.1.7+002+handle-unsupported-videos-ios.patch](expo-av+15.1.7+002+handle-unsupported-videos-ios.patch) - -- Reason: - - ``` - This patch handles unsupported videos in iOS. - ``` - -- Upstream PR/issue: Probably same reason as the first patch. -- E/App issue: https://github.com/Expensify/App/issues/52673 -- PR introducing patch: https://github.com/Expensify/App/pull/57353 \ No newline at end of file diff --git a/patches/expo-av/expo-av+15.1.7+001+fix-blank-screen-android.patch b/patches/expo-av/expo-av+15.1.7+001+fix-blank-screen-android.patch deleted file mode 100644 index 3b3f307062509..0000000000000 --- a/patches/expo-av/expo-av+15.1.7+001+fix-blank-screen-android.patch +++ /dev/null @@ -1,221 +0,0 @@ -diff --git a/node_modules/expo-av/android/src/main/java/expo/modules/av/player/SimpleExoPlayerData.java b/node_modules/expo-av/android/src/main/java/expo/modules/av/player/SimpleExoPlayerData.java -index 8835a39..87e1964 100644 ---- a/node_modules/expo-av/android/src/main/java/expo/modules/av/player/SimpleExoPlayerData.java -+++ b/node_modules/expo-av/android/src/main/java/expo/modules/av/player/SimpleExoPlayerData.java -@@ -2,6 +2,7 @@ package expo.modules.av.player; - - import android.content.Context; - import android.net.Uri; -+import android.os.Build; - import android.os.Bundle; - import android.os.Looper; - -@@ -63,6 +64,8 @@ class SimpleExoPlayerData extends PlayerData - private boolean mIsLoading = true; - private final Context mReactContext; - -+ private Surface mSurface = null; -+ - SimpleExoPlayerData(final AVManagerInterface avModule, final Context context, final Uri uri, final String overridingExtension, final Map requestHeaders) { - super(avModule, uri, requestHeaders); - mReactContext = context; -@@ -228,6 +231,13 @@ class SimpleExoPlayerData extends PlayerData - - @Override - public void tryUpdateVideoSurface(final Surface surface) { -+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { -+ if (mSimpleExoPlayer != null && mSurface != surface) { -+ mSimpleExoPlayer.setVideoSurface(surface); -+ mSurface = surface; -+ } -+ return; -+ } - if (mSimpleExoPlayer != null) { - mSimpleExoPlayer.setVideoSurface(surface); - } -diff --git a/node_modules/expo-av/android/src/main/java/expo/modules/av/video/VideoTextureView.java b/node_modules/expo-av/android/src/main/java/expo/modules/av/video/VideoTextureView.java -index 41a5979..b378bf4 100644 ---- a/node_modules/expo-av/android/src/main/java/expo/modules/av/video/VideoTextureView.java -+++ b/node_modules/expo-av/android/src/main/java/expo/modules/av/video/VideoTextureView.java -@@ -4,6 +4,7 @@ import android.annotation.SuppressLint; - import android.content.Context; - import android.graphics.Matrix; - import android.graphics.SurfaceTexture; -+import android.os.Build; - import android.util.Pair; - import android.util.Size; - import android.view.Surface; -@@ -20,6 +21,10 @@ public class VideoTextureView extends TextureView implements TextureView.Surface - private boolean mIsAttachedToWindow = false; - private Surface mSurface = null; - private boolean mVisible = true; -+ private int mBufferWidth = 0; -+ private int mBufferHeight = 0; -+ private int mPositionX = 0; -+ private int mPositionY = 0; - - public VideoTextureView(final Context themedReactContext, VideoView videoView) { - super(themedReactContext, null, 0); -@@ -55,6 +60,15 @@ public class VideoTextureView extends TextureView implements TextureView.Surface - setTransform(matrix); - invalidate(); - } -+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { -+ float[] values = new float[9]; -+ matrix.getValues(values); -+ mBufferWidth = (int)(getWidth() * values[Matrix.MSCALE_X]); -+ mBufferHeight = (int)(getHeight() * values[Matrix.MSCALE_Y]); -+ mPositionX = (int)(values[Matrix.MTRANS_X]); -+ mPositionY = (int)(values[Matrix.MTRANS_Y]); -+ mVideoView.updateSurfaceView(mVisible, mBufferWidth, mBufferHeight, mPositionX, mPositionY); -+ } - } - } - -@@ -69,6 +83,9 @@ public class VideoTextureView extends TextureView implements TextureView.Surface - onVisibilityChanged(this, View.INVISIBLE); - onVisibilityChanged(this, View.VISIBLE); - } -+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { -+ mVideoView.updateSurfaceView(mVisible, mBufferWidth, mBufferHeight, mPositionX, mPositionY); -+ } - } - - // TextureView -@@ -76,7 +93,12 @@ public class VideoTextureView extends TextureView implements TextureView.Surface - @Override - public void onSurfaceTextureAvailable(final SurfaceTexture surfaceTexture, final int width, final int height) { - mSurface = new Surface(surfaceTexture); -- mVideoView.tryUpdateVideoSurface(mSurface); -+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { -+ mVideoView.tryUpdateVideoSurface(); -+ mVideoView.updateSurfaceView(mVisible, mBufferWidth, mBufferHeight, mPositionX, mPositionY); -+ } else { -+ mVideoView.tryUpdateVideoSurface(mSurface); -+ } - } - - @Override -@@ -86,13 +108,17 @@ public class VideoTextureView extends TextureView implements TextureView.Surface - - @Override - public void onSurfaceTextureUpdated(final SurfaceTexture surfaceTexture) { -- // no-op -+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { -+ mVideoView.updateSurfaceView(mVisible, mBufferWidth, mBufferHeight, mPositionX, mPositionY); -+ } - } - - @Override - public boolean onSurfaceTextureDestroyed(final SurfaceTexture surfaceTexture) { - mSurface = null; -- mVideoView.tryUpdateVideoSurface(null); -+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { -+ mVideoView.tryUpdateVideoSurface(null); -+ } - return true; - } - -@@ -108,11 +134,19 @@ public class VideoTextureView extends TextureView implements TextureView.Surface - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - mIsAttachedToWindow = true; -- mVideoView.tryUpdateVideoSurface(mSurface); -+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { -+ mVideoView.tryUpdateVideoSurface(); -+ mVideoView.updateSurfaceView(mVisible, mBufferWidth, mBufferHeight, mPositionX, mPositionY); -+ } else { -+ mVideoView.tryUpdateVideoSurface(mSurface); -+ } - } - - protected void onVisibilityChanged(View changedView, int visibility) { - super.onVisibilityChanged(changedView, visibility); - mVisible = visibility == View.VISIBLE; -+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { -+ mVideoView.updateSurfaceView(mVisible, mBufferWidth, mBufferHeight, mPositionX, mPositionY); -+ } - } - } -diff --git a/node_modules/expo-av/android/src/main/java/expo/modules/av/video/VideoView.java b/node_modules/expo-av/android/src/main/java/expo/modules/av/video/VideoView.java -index 1acb32b..6bac2bf 100644 ---- a/node_modules/expo-av/android/src/main/java/expo/modules/av/video/VideoView.java -+++ b/node_modules/expo-av/android/src/main/java/expo/modules/av/video/VideoView.java -@@ -2,10 +2,14 @@ package expo.modules.av.video; - - import android.annotation.SuppressLint; - import android.content.Context; -+import android.os.Build; - import android.os.Bundle; - import android.util.Pair; -+import android.view.Gravity; - import android.view.MotionEvent; - import android.view.Surface; -+import android.view.SurfaceControl; -+import android.view.SurfaceView; - import android.widget.FrameLayout; - - import androidx.annotation.NonNull; -@@ -62,6 +66,10 @@ public class VideoView extends FrameLayout implements AudioEventHandler, Fullscr - private boolean mShouldShowFullscreenPlayerOnLoad = false; - private FullscreenVideoPlayerPresentationChangeProgressListener mFullscreenVideoPlayerPresentationOnLoadChangeListener = null; - -+ private Surface mSurface; -+ private SurfaceControl mSurfaceControl; -+ private SurfaceView mSurfaceView; -+ - public VideoView(@NonNull Context context, VideoViewWrapper videoViewWrapper, AppContext appContext) { - super(context); - -@@ -70,6 +78,13 @@ public class VideoView extends FrameLayout implements AudioEventHandler, Fullscr - mAVModule = appContext.getLegacyModuleRegistry().getModule(AVManagerInterface.class); - mAVModule.registerVideoViewForAudioLifecycle(this); - -+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { -+ mSurfaceView = new SurfaceView(context); -+ mSurfaceControl = new SurfaceControl.Builder().setName("ExpoAVSurfaceControl").setBufferSize(0, 0).build(); -+ mSurface = new Surface(mSurfaceControl); -+ addView(mSurfaceView); -+ } -+ - mVideoTextureView = new VideoTextureView(context, this); - addView(mVideoTextureView, generateDefaultLayoutParams()); - -@@ -387,7 +402,11 @@ public class VideoView extends FrameLayout implements AudioEventHandler, Fullscr - mVideoTextureView.scaleVideoSize(mPlayerData.getVideoWidthHeight(), mResizeMode); - - if (mVideoTextureView.isAttachedToWindow()) { -- mPlayerData.tryUpdateVideoSurface(mVideoTextureView.getSurface()); -+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { -+ mPlayerData.tryUpdateVideoSurface(mSurface); -+ } else { -+ mPlayerData.tryUpdateVideoSurface(mVideoTextureView.getSurface()); -+ } - } - - if (promise != null) { -@@ -477,6 +496,24 @@ public class VideoView extends FrameLayout implements AudioEventHandler, Fullscr - } - } - -+ public void tryUpdateVideoSurface() { -+ if (mPlayerData != null) { -+ mPlayerData.tryUpdateVideoSurface(mSurface); -+ } -+ } -+ -+ public void updateSurfaceView(boolean visible, int bufferWidth, int bufferHeight, int positionX, int positionY) { -+ new SurfaceControl.Transaction() -+ .reparent(mSurfaceControl, mSurfaceView.getSurfaceControl()) -+ .setBufferSize(mSurfaceControl, bufferWidth, bufferHeight) -+ .setPosition(mSurfaceControl, 0, 0) -+ .setVisibility(mSurfaceControl, visible && bufferWidth > 0 && bufferHeight > 0) -+ .apply(); -+ mSurfaceView.layout(positionX, positionY, positionX + Math.max(bufferWidth, 1), positionY + Math.max(bufferHeight, 1)); -+ mSurfaceView.setLayoutParams(new FrameLayout.LayoutParams(Math.max(bufferWidth, 1), Math.max(bufferHeight, 1), Gravity.CENTER)); -+ mSurfaceView.requestLayout(); -+ } -+ - // AudioEventHandler - - @Override diff --git a/patches/expo-av/expo-av+15.1.7+002+handle-unsupported-videos-ios.patch b/patches/expo-av/expo-av+15.1.7+002+handle-unsupported-videos-ios.patch deleted file mode 100644 index b5e27e027e351..0000000000000 --- a/patches/expo-av/expo-av+15.1.7+002+handle-unsupported-videos-ios.patch +++ /dev/null @@ -1,22 +0,0 @@ -diff --git a/node_modules/expo-av/ios/EXAV/EXAVPlayerData.m b/node_modules/expo-av/ios/EXAV/EXAVPlayerData.m -index 99dc808..01e4bb9 100644 ---- a/node_modules/expo-av/ios/EXAV/EXAVPlayerData.m -+++ b/node_modules/expo-av/ios/EXAV/EXAVPlayerData.m -@@ -158,8 +158,16 @@ NSString *const EXAVPlayerDataObserverMetadataKeyPath = @"timedMetadata"; - // unless we preload, the asset will not necessarily load the duration by the time we try to play it. - // http://stackoverflow.com/questions/20581567/avplayer-and-avfoundationerrordomain-code-11819 - EX_WEAKIFY(self); -- [avAsset loadValuesAsynchronouslyForKeys:@[ @"duration" ] completionHandler:^{ -+ [avAsset loadValuesAsynchronouslyForKeys:@[ @"isPlayable", @"duration" ] completionHandler:^{ - EX_ENSURE_STRONGIFY(self); -+ NSError *error = nil; -+ AVKeyValueStatus status = [avAsset statusOfValueForKey:@"isPlayable" error:&error]; -+ -+ if (status == AVKeyValueStatusLoaded && !avAsset.isPlayable) { -+ NSString *errorMessage = @"Load encountered an error: [AVAsset isPlayable:] returned false. The asset does not contains a playable content or is not supported by the device."; -+ [self _finishLoadWithError:errorMessage]; -+ return; -+ } - - // We prepare three items for AVQueuePlayer, so when the first finishes playing, - // second can start playing and the third can start preparing to play. diff --git a/patches/expo-video/details.md b/patches/expo-video/details.md new file mode 100644 index 0000000000000..84dc26c374282 --- /dev/null +++ b/patches/expo-video/details.md @@ -0,0 +1,13 @@ +# `expo-video` patches + +### [expo-video+3.0.11+001+prevent_double_pause.patch](expo-video+3.0.11+001+prevent_double_pause.patch) + +- Reason: + + ``` + This patch edits the onPause eventListener in expo-video's videoPlayer. It still pauses all mounted videos, but now excludes the video that invoked the listener, preventing it from being paused twice. + ``` + +- Upstream PR/issue: https://github.com/expo/expo/issues/40743 +- E/App issue: ๐Ÿ›‘ +- PR Introducing Patch: https://github.com/Expensify/App/pull/66793 diff --git a/patches/expo-video/expo-video+3.0.11+001+prevent_double_pause.patch b/patches/expo-video/expo-video+3.0.11+001+prevent_double_pause.patch new file mode 100644 index 0000000000000..d1a996d780519 --- /dev/null +++ b/patches/expo-video/expo-video+3.0.11+001+prevent_double_pause.patch @@ -0,0 +1,24 @@ + +diff --git a/node_modules/expo-video/build/VideoPlayer.web.js b/node_modules/expo-video/build/VideoPlayer.web.js +index 2c7224a..16df18b 100644 +--- a/node_modules/expo-video/build/VideoPlayer.web.js ++++ b/node_modules/expo-video/build/VideoPlayer.web.js +@@ -305,14 +305,16 @@ export default class VideoPlayerWeb extends globalThis.expo.SharedObject { + mountedVideo.play(); + }); + }; +- video.onpause = () => { ++ video.onpause = (e) => { + this._emitOnce(video, 'playingChange', { + isPlaying: false, + oldIsPlaying: this.playing, + }); + this.playing = false; + this._mountedVideos.forEach((mountedVideo) => { +- mountedVideo.pause(); ++ if (e.target !== mountedVideo) { ++ mountedVideo.pause(); ++ } + }); + }; + video.onvolumechange = () => { diff --git a/src/components/AttachmentPreview.tsx b/src/components/AttachmentPreview.tsx index 560be3304cff0..1a07fbc49dcf1 100644 --- a/src/components/AttachmentPreview.tsx +++ b/src/components/AttachmentPreview.tsx @@ -1,6 +1,7 @@ import {Str} from 'expensify-common'; -import {ResizeMode, Video} from 'expo-av'; -import React, {useMemo, useState} from 'react'; +import type {VideoThumbnail} from 'expo-video'; +import {useVideoPlayer} from 'expo-video'; +import React, {useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import {cleanFileName, getFileName} from '@libs/fileDownload/FileUtils'; @@ -37,6 +38,12 @@ function AttachmentPreview({source, aspectRatio = 1, onPress, onLoadError}: Atta return cleanFileName(rawFileName); }, [source]); + const [thumbnail, setThumbnail] = useState(null); + const videoPlayer = useVideoPlayer({uri: source}); + useEffect(() => { + videoPlayer.generateThumbnailsAsync(1).then((thumbnails) => setThumbnail(thumbnails.at(0) ?? null)); + }, [videoPlayer]); + if (typeof source === 'string' && Str.isVideo(source)) { return ( -