diff --git a/__mocks__/expo-video.tsx b/__mocks__/expo-video.tsx new file mode 100644 index 0000000000000..906a364e932dd --- /dev/null +++ b/__mocks__/expo-video.tsx @@ -0,0 +1,111 @@ +/* eslint-disable no-underscore-dangle */ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// eslint-disable-next-line no-restricted-imports +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 117465e641d6f..93bae202d1cca 100644 --- a/config/webpack/webpack.common.ts +++ b/config/webpack/webpack.common.ts @@ -56,6 +56,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 5cc365f37ff12..d2a0a053a73d1 100644 --- a/cspell.json +++ b/cspell.json @@ -725,6 +725,7 @@ "unhold", "Unlaminated", "Unmigrated", + "unmuting", "unredacted", "unscrollable", "unsubmitted", diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 6b4596498c5f9..e40d7aeb93bb5 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -173,6 +173,8 @@ PODS: - Yoga - ExpoSecureStore (14.2.4): - ExpoModulesCore + - ExpoVideo (3.0.12): + - ExpoModulesCore - ExpoStoreReview (9.0.8): - ExpoModulesCore - fast_float (8.0.0) @@ -4135,6 +4137,7 @@ DEPENDENCIES: - ExpoLocation (from `../node_modules/expo-location/ios`) - ExpoModulesCore (from `../node_modules/expo-modules-core`) - ExpoSecureStore (from `../node_modules/expo-secure-store/ios`) + - ExpoVideo (from `../node_modules/expo-video/ios`) - ExpoStoreReview (from `../node_modules/expo-store-review/ios`) - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) @@ -4349,6 +4352,8 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-modules-core" ExpoSecureStore: :path: "../node_modules/expo-secure-store/ios" + ExpoVideo: + :path: "../node_modules/expo-video/ios" ExpoStoreReview: :path: "../node_modules/expo-store-review/ios" fast_float: @@ -4626,6 +4631,7 @@ SPEC CHECKSUMS: ExpoLocation: 93d7faa0c2adbd5a04686af0c1a61bc6ed3ee2f7 ExpoModulesCore: e1b5401a7af4c7dbf4fe26b535918a72c6ed8a7b ExpoSecureStore: 3f1b632d6d40bcc62b4983ef9199cd079592a50a + ExpoVideo: 6907c4872886dce2720d3af20782eb6ee7734110 ExpoStoreReview: 15f19e0d6cb6e00330ba1b356485bf47ef19c39a fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6 FBLazyVector: 941bef1c8eeabd9fe1f501e30a5220beee913886 diff --git a/package-lock.json b/package-lock.json index 9c5479aa1074f..f10c5617d76ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,6 +74,7 @@ "expo-modules-core": "3.0.18", "expo-secure-store": "~14.2.4", "expo-store-review": "~9.0.8", + "expo-video": "^3.0.12", "expo-task-manager": "~14.0.9", "fast-equals": "^5.2.2", "focus-trap-react": "^11.0.3", @@ -23594,6 +23595,17 @@ "react-native": "*" } }, + "node_modules/expo-video": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/expo-video/-/expo-video-3.0.12.tgz", + "integrity": "sha512-L+E+zmNp3RxUBk2ugMSjxVposP70uIgGCZio5PiiUXme2KQ1eAEta2vcUPWrf4a+udrp6Xzr7bO1H9vkUXF3pg==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, "node_modules/expo-task-manager": { "version": "14.0.9", "resolved": "https://registry.npmjs.org/expo-task-manager/-/expo-task-manager-14.0.9.tgz", diff --git a/package.json b/package.json index 557f84ddc7f81..0d3da0b17e30d 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "test:debug": "TZ=utc NODE_OPTIONS='--inspect-brk --experimental-vm-modules' jest --runInBand", "perf-test": "NODE_OPTIONS=--experimental-vm-modules npx reassure", "typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc", - "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=384 --cache --cache-location=node_modules/.cache/eslint --cache-strategy content --concurrency=auto", + "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=398 --cache --cache-location=node_modules/.cache/eslint --cache-strategy content --concurrency=auto", "lint-changed": "NODE_OPTIONS=--max_old_space_size=8192 ./scripts/lintChanged.sh", "check-lazy-loading": "ts-node scripts/checkLazyLoading.ts", "lint-watch": "npx eslint-watch --watch --changed", @@ -142,6 +142,7 @@ "expo-location": "^19.0.7", "expo-modules-core": "3.0.18", "expo-secure-store": "~14.2.4", + "expo-video": "^3.0.12", "expo-store-review": "~9.0.8", "expo-task-manager": "~14.0.9", "fast-equals": "^5.2.2", 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..ede6026ad2a92 --- /dev/null +++ b/patches/expo-video/details.md @@ -0,0 +1,13 @@ +# `expo-video` patches + +### [expo-video+3.0.12+001+prevent_double_pause.patch](expo-video+3.0.12+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.12+001+prevent_double_pause.patch b/patches/expo-video/expo-video+3.0.12+001+prevent_double_pause.patch new file mode 100644 index 0000000000000..d1a996d780519 --- /dev/null +++ b/patches/expo-video/expo-video+3.0.12+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 84b2981a351c0..795083cdc9f65 100644 --- a/src/components/AttachmentPreview.tsx +++ b/src/components/AttachmentPreview.tsx @@ -1,14 +1,16 @@ import {Str} from 'expensify-common'; -import {ResizeMode, Video} from 'expo-av'; -import React, {useMemo, useState} from 'react'; +import {useEvent} from 'expo'; +import type {SourceLoadEventPayload, VideoThumbnail} from 'expo-video'; +import {useVideoPlayer} from 'expo-video'; +import React, {useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useThemeStyles from '@hooks/useThemeStyles'; import {cleanFileName, getFileName} from '@libs/fileDownload/FileUtils'; import variables from '@styles/variables'; import {checkIsFileImage} from './Attachments/AttachmentView'; import DefaultAttachmentView from './Attachments/AttachmentView/DefaultAttachmentView'; import Icon from './Icon'; -import {Play} from './Icon/Expensicons'; import Image from './Image'; import PDFThumbnail from './PDFThumbnail'; import {PressableWithFeedback} from './Pressable'; @@ -29,6 +31,7 @@ type AttachmentPreviewProps = { function AttachmentPreview({source, aspectRatio = 1, onPress, onLoadError}: AttachmentPreviewProps) { const styles = useThemeStyles(); + const icons = useMemoizedLazyExpensifyIcons(['Play']); const fillStyle = aspectRatio < 1 ? styles.h100 : styles.w100; const [isEncryptedPDF, setIsEncryptedPDF] = useState(false); @@ -37,6 +40,18 @@ function AttachmentPreview({source, aspectRatio = 1, onPress, onLoadError}: Atta return cleanFileName(rawFileName); }, [source]); + const [thumbnail, setThumbnail] = useState(null); + const videoPlayer = useVideoPlayer(source); + + const {videoSource} = useEvent(videoPlayer, 'sourceLoad', {videoSource: null} as SourceLoadEventPayload); + + useEffect(() => { + if (!videoSource) { + return; + } + videoPlayer.generateThumbnailsAsync(1).then((thumbnails) => setThumbnail(thumbnails.at(0) ?? null)); + }, [videoPlayer, videoSource]); + if (typeof source === 'string' && Str.isVideo(source)) { return ( -