@stepincto/expo-video 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +46 -4
  2. package/android/src/main/java/expo/modules/video/VideoModule.kt +6 -0
  3. package/android/src/main/java/expo/modules/video/VideoView.kt +13 -0
  4. package/android/src/main/java/expo/modules/video/enums/FullscreenOrientation.kt +26 -0
  5. package/android/src/main/java/expo/modules/video/records/FullscreenOptions.kt +12 -0
  6. package/android/src/main/java/expo/modules/video/utils/FullscreenActivityOrientationHelper.kt +120 -0
  7. package/build/VideoModule.d.ts.map +1 -1
  8. package/build/VideoModule.js +24 -1
  9. package/build/VideoModule.js.map +1 -1
  10. package/build/VideoView.d.ts.map +1 -1
  11. package/build/VideoView.js +10 -3
  12. package/build/VideoView.js.map +1 -1
  13. package/build/VideoView.types.d.ts +47 -0
  14. package/build/VideoView.types.d.ts.map +1 -1
  15. package/build/VideoView.types.js.map +1 -1
  16. package/build/VideoView.web.d.ts.map +1 -1
  17. package/build/VideoView.web.js +3 -2
  18. package/build/VideoView.web.js.map +1 -1
  19. package/build/index.d.ts +1 -1
  20. package/build/index.d.ts.map +1 -1
  21. package/build/index.js.map +1 -1
  22. package/expo-module.config.json +7 -1
  23. package/ios/Cache/CachableRequest.swift +2 -41
  24. package/ios/Cache/CachedResource.swift +1 -1
  25. package/ios/Cache/MediaFileHandle.swift +10 -8
  26. package/ios/Cache/MediaInfo.swift +41 -6
  27. package/ios/Cache/VideoCacheManager.swift +53 -6
  28. package/ios/Enums/FullscreenOrientation.swift +34 -0
  29. package/ios/OrientationAVPlayerViewController.swift +231 -0
  30. package/ios/Records/FullscreenOptions.swift +12 -0
  31. package/ios/VideoAsset.swift +4 -1
  32. package/ios/VideoModule.swift +8 -0
  33. package/ios/VideoView.swift +19 -50
  34. package/package.json +3 -4
  35. package/src/VideoModule.ts +28 -1
  36. package/src/VideoView.tsx +28 -3
  37. package/src/VideoView.types.ts +57 -0
  38. package/src/VideoView.web.tsx +4 -2
  39. package/src/index.ts +7 -1
@@ -0,0 +1,12 @@
1
+ // Copyright 2025-present 650 Industries. All rights reserved.
2
+
3
+ import ExpoModulesCore
4
+
5
+ internal struct FullscreenOptions: Record {
6
+ @Field
7
+ var enable: Bool = true
8
+ @Field
9
+ var orientation: FullscreenOrientation = FullscreenOrientation.default
10
+ @Field
11
+ var autoExitOnRotate: Bool = false
12
+ }
@@ -72,8 +72,11 @@ internal class VideoAsset: AVURLAsset, @unchecked Sendable {
72
72
  guard useCaching else {
73
73
  return
74
74
  }
75
- if let saveFilePath, let cachedFileUrl = URL(string: saveFilePath) {
75
+ if let saveFilePath {
76
+ // Use fileURLWithPath for proper file URL creation instead of URL(string:)
77
+ let cachedFileUrl = URL(fileURLWithPath: saveFilePath)
76
78
  VideoCacheManager.shared.unregisterOpenFile(at: cachedFileUrl)
79
+ print("[VideoAsset] Unregistering file URL: \(cachedFileUrl.path)")
77
80
  }
78
81
  VideoCacheManager.shared.ensureCacheSize()
79
82
  }
@@ -85,6 +85,14 @@ public final class VideoModule: Module {
85
85
  )
86
86
  }
87
87
 
88
+ Prop("fullscreenOptions") { (view, options: FullscreenOptions?) in
89
+ #if !os(tvOS)
90
+ view.playerViewController.fullscreenOrientation = options?.orientation.toUIInterfaceOrientationMask() ?? .all
91
+ view.playerViewController.autoExitOnRotate = options?.autoExitOnRotate ?? false
92
+ view.playerViewController.setValue(options?.enable ?? true, forKey: "allowsEnteringFullScreen")
93
+ #endif
94
+ }
95
+
88
96
  Prop("allowsFullscreen") { (view, allowsFullscreen: Bool?) in
89
97
  #if !os(tvOS)
90
98
  view.playerViewController.setValue(allowsFullscreen ?? true, forKey: "allowsEnteringFullScreen")
@@ -4,7 +4,7 @@ import AVKit
4
4
  import ExpoModulesCore
5
5
 
6
6
  public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
7
- lazy var playerViewController = AVPlayerViewController()
7
+ lazy var playerViewController = OrientationAVPlayerViewController(delegate: self)
8
8
 
9
9
  weak var player: VideoPlayer? {
10
10
  didSet {
@@ -14,11 +14,8 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
14
14
 
15
15
  #if os(tvOS)
16
16
  var wasPlaying: Bool = false
17
- #endif
18
- var isFullscreen: Bool = false
19
- var isInPictureInPicture = false
20
- #if os(tvOS)
21
17
  let startPictureInPictureAutomatically = false
18
+ var isFullscreen: Bool = false
22
19
  #else
23
20
  var startPictureInPictureAutomatically = false {
24
21
  didSet {
@@ -55,7 +52,6 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
55
52
  VideoManager.shared.register(videoView: self)
56
53
 
57
54
  clipsToBounds = true
58
- playerViewController.delegate = self
59
55
  playerViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
60
56
  playerViewController.view.backgroundColor = .clear
61
57
  // Now playing is managed by the `NowPlayingManager`
@@ -73,60 +69,34 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
73
69
  }
74
70
 
75
71
  func enterFullscreen() {
76
- if isFullscreen {
77
- return
78
- }
79
- let selectorName = "enterFullScreenAnimated:completionHandler:"
80
- let selectorToForceFullScreenMode = NSSelectorFromString(selectorName)
81
-
82
- if playerViewController.responds(to: selectorToForceFullScreenMode) {
83
- playerViewController.perform(selectorToForceFullScreenMode, with: true, with: nil)
84
- } else {
72
+ let tvOSFallback = {
85
73
  #if os(tvOS)
86
74
  // For TV, save the currently playing state,
87
75
  // remove the view controller from its superview,
88
76
  // and present the view controller normally
89
- wasPlaying = player?.isPlaying == true
77
+ self.wasPlaying = self.player?.isPlaying == true
90
78
  self.playerViewController.view.removeFromSuperview()
91
79
  self.reactViewController().present(self.playerViewController, animated: true)
92
- onFullscreenEnter()
93
- isFullscreen = true
80
+ self.onFullscreenEnter()
81
+ self.isFullscreen = true
94
82
  #endif
95
83
  }
84
+ playerViewController.enterFullscreen(selectorUnsupportedFallback: tvOSFallback)
96
85
  }
97
86
 
98
87
  func exitFullscreen() {
99
- if !isFullscreen {
100
- return
101
- }
102
- let selectorName = "exitFullScreenAnimated:completionHandler:"
103
- let selectorToExitFullScreenMode = NSSelectorFromString(selectorName)
104
-
105
- if playerViewController.responds(to: selectorToExitFullScreenMode) {
106
- playerViewController.perform(selectorToExitFullScreenMode, with: true, with: nil)
107
- }
88
+ playerViewController.exitFullscreen()
89
+ #if os(tvOS)
90
+ self.isFullscreen = false
91
+ #endif
108
92
  }
109
93
 
110
94
  func startPictureInPicture() throws {
111
- if !AVPictureInPictureController.isPictureInPictureSupported() {
112
- throw PictureInPictureUnsupportedException()
113
- }
114
-
115
- let selectorName = "startPictureInPicture"
116
- let selectorToStartPictureInPicture = NSSelectorFromString(selectorName)
117
-
118
- if playerViewController.responds(to: selectorToStartPictureInPicture) {
119
- playerViewController.perform(selectorToStartPictureInPicture)
120
- }
95
+ try playerViewController.startPictureInPicture()
121
96
  }
122
97
 
123
98
  func stopPictureInPicture() {
124
- let selectorName = "stopPictureInPicture"
125
- let selectorToStopPictureInPicture = NSSelectorFromString(selectorName)
126
-
127
- if playerViewController.responds(to: selectorToStopPictureInPicture) {
128
- playerViewController.perform(selectorToStopPictureInPicture)
129
- }
99
+ playerViewController.stopPictureInPicture()
130
100
  }
131
101
 
132
102
  // MARK: - AVPlayerViewControllerDelegate
@@ -162,7 +132,6 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
162
132
  willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator
163
133
  ) {
164
134
  onFullscreenEnter()
165
- isFullscreen = true
166
135
  }
167
136
 
168
137
  public func playerViewController(
@@ -171,27 +140,27 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
171
140
  ) {
172
141
  // Platform's behavior is to pause the player when exiting the fullscreen mode.
173
142
  // It seems better to continue playing, so we resume the player once the dismissing animation finishes.
174
- let wasPlaying = player?.ref.timeControlStatus == .playing
143
+ let wasPlaying = player?.isPlaying ?? false
175
144
 
176
145
  coordinator.animate(alongsideTransition: nil) { context in
177
- if !context.isCancelled {
178
- if wasPlaying {
146
+ if !context.isCancelled && wasPlaying {
147
+ DispatchQueue.main.async {
179
148
  self.player?.ref.play()
180
149
  }
150
+ }
151
+
152
+ if !context.isCancelled {
181
153
  self.onFullscreenExit()
182
- self.isFullscreen = false
183
154
  }
184
155
  }
185
156
  }
186
157
  #endif
187
158
 
188
159
  public func playerViewControllerDidStartPictureInPicture(_ playerViewController: AVPlayerViewController) {
189
- isInPictureInPicture = true
190
160
  onPictureInPictureStart()
191
161
  }
192
162
 
193
163
  public func playerViewControllerDidStopPictureInPicture(_ playerViewController: AVPlayerViewController) {
194
- isInPictureInPicture = false
195
164
  onPictureInPictureStop()
196
165
  }
197
166
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stepincto/expo-video",
3
3
  "title": "Expo Video",
4
- "version": "1.0.4",
4
+ "version": "1.0.6",
5
5
  "originalUpstreamVersion": "2.2.2",
6
6
  "description": "A cross-platform, performant video component for React Native and Expo with Web support",
7
7
  "main": "build/index.js",
@@ -36,12 +36,11 @@
36
36
  "devDependencies": {
37
37
  "@types/react": "^19.1.9",
38
38
  "@types/react-dom": "^19.1.7",
39
- "expo-module-scripts": "^4.1.10",
39
+ "expo-module-scripts": "^5.0.8",
40
40
  "typescript": "^5.9.2"
41
41
  },
42
42
  "peerDependencies": {
43
- "expo": "^53.0.0",
44
- "expo-modules-core": ">=2.4 <2.6",
43
+ "expo": "^54.0.0",
45
44
  "react": ">=18",
46
45
  "react-native": ">=0.75"
47
46
  },
@@ -1,5 +1,7 @@
1
1
  import NativeVideoModule from './NativeVideoModule';
2
2
 
3
+ const globalCacheOperations = new Map<string, Promise<boolean>>(); // stores the promises that are currently caching files
4
+
3
5
  /**
4
6
  * Returns whether the current device supports Picture in Picture (PiP) mode.
5
7
  *
@@ -46,8 +48,33 @@ export function getCurrentVideoCacheSize(): number {
46
48
  return NativeVideoModule.getCurrentVideoCacheSize();
47
49
  }
48
50
 
51
+ // export async function preCacheVideoAsync(url: string): Promise<boolean> {
52
+ // return NativeVideoModule.preCacheVideoAsync(url);
53
+ // }
54
+
55
+ // Modify the existing function
49
56
  export async function preCacheVideoAsync(url: string): Promise<boolean> {
50
- return NativeVideoModule.preCacheVideoAsync(url);
57
+ // If already caching, return the existing promise
58
+ if (globalCacheOperations.has(url)) {
59
+ return await globalCacheOperations.get(url)!;
60
+ }
61
+
62
+ // Start new caching operation with proper error handling
63
+ const cachingPromise = NativeVideoModule.preCacheVideoAsync(url).catch((error) => {
64
+ // Ensure cleanup happens even on native errors
65
+ globalCacheOperations.delete(url);
66
+ throw error;
67
+ });
68
+
69
+ globalCacheOperations.set(url, cachingPromise);
70
+
71
+ try {
72
+ const result = await cachingPromise;
73
+ return result;
74
+ } finally {
75
+ // Always cleanup, even if the promise was already cleaned up in catch
76
+ globalCacheOperations.delete(url);
77
+ }
51
78
  }
52
79
 
53
80
  export async function preCacheVideoPartialAsync(url: string, chunkSize?: number): Promise<boolean> {
package/src/VideoView.tsx CHANGED
@@ -61,13 +61,38 @@ export class VideoView extends PureComponent<VideoViewProps> {
61
61
  }
62
62
 
63
63
  render(): ReactNode {
64
- const { player, ...props } = this.props;
64
+ const { player, allowsFullscreen, ...props } = this.props;
65
65
  const playerId = getPlayerId(player);
66
66
 
67
+ if (allowsFullscreen !== undefined) {
68
+ console.warn(
69
+ 'The `allowsFullscreen` prop is deprecated and will be removed in a future release. Use `fullscreenOptions` prop instead.'
70
+ );
71
+ }
72
+
73
+ const fullscreenOptions = {
74
+ enable: allowsFullscreen,
75
+ ...props.fullscreenOptions,
76
+ };
77
+
67
78
  if (NativeTextureVideoView && this.props.surfaceType === 'textureView') {
68
- return <NativeTextureVideoView {...props} player={playerId} ref={this.nativeRef} />;
79
+ return (
80
+ <NativeTextureVideoView
81
+ {...props}
82
+ fullscreenOptions={fullscreenOptions}
83
+ player={playerId}
84
+ ref={this.nativeRef}
85
+ />
86
+ );
69
87
  }
70
- return <NativeVideoView {...props} player={playerId} ref={this.nativeRef} />;
88
+ return (
89
+ <NativeVideoView
90
+ {...props}
91
+ fullscreenOptions={fullscreenOptions}
92
+ player={playerId}
93
+ ref={this.nativeRef}
94
+ />
95
+ );
71
96
  }
72
97
  }
73
98
 
@@ -20,6 +20,56 @@ export type VideoContentFit = 'contain' | 'cover' | 'fill';
20
20
  */
21
21
  export type SurfaceType = 'textureView' | 'surfaceView';
22
22
 
23
+ /**
24
+ * Describes the orientation of the video in fullscreen mode. Available values are:
25
+ * - `default`: The video is displayed in any of the available device rotations.
26
+ * - `portrait`: The video is displayed in one of two available portrait orientations and rotates between them.
27
+ * - `portraitUp`: The video is displayed in the portrait orientation - the notch of the phone points upwards.
28
+ * - `portraitDown`: The video is displayed in the portrait orientation - the notch of the phone points downwards.
29
+ * - `landscape`: The video is displayed in one of two available landscape orientations and rotates between them.
30
+ * - `landscapeLeft`: The video is displayed in the left landscape orientation - the notch of the phone is in the left palm of the user.
31
+ * - `landscapeRight`: The video is displayed in the right landscape orientation - the notch of the phone is in the right palm of the user.
32
+ */
33
+ export type FullscreenOrientation =
34
+ | 'default'
35
+ | 'portrait'
36
+ | 'portraitUp'
37
+ | 'portraitDown'
38
+ | 'landscape'
39
+ | 'landscapeLeft'
40
+ | 'landscapeRight';
41
+
42
+ /**
43
+ * Describes the options for fullscreen video mode.
44
+ */
45
+ export type FullscreenOptions = {
46
+ /**
47
+ * Specifies whether the fullscreen mode should be available to the user. When `false`, the fullscreen button will be hidden in the player.
48
+ * Equivalent to the `allowsFullscreen` prop.
49
+ * @default true
50
+ */
51
+ enable?: boolean;
52
+ /**
53
+ * Specifies the orientation of the video in fullscreen mode.
54
+ * @default 'default'
55
+ * @platform android
56
+ * @platform ios
57
+ */
58
+ orientation?: FullscreenOrientation;
59
+ /**
60
+ * Specifies whether the app should exit fullscreen mode when the device is rotated to a different orientation than the one specified in the `orientation` prop.
61
+ * For example, if the `orientation` prop is set to `landscape` and the device is rotated to `portrait`, the app will exit fullscreen mode.
62
+ *
63
+ * > This prop will have no effect if the `orientation` prop is set to `default`.
64
+ * > The `VideoView` will never auto-exit fullscreen when the device auto-rotate feature has been disabled in settings.
65
+ *
66
+ * @default false
67
+ * @platform android
68
+ * @platform ios
69
+ */
70
+ autoExitOnRotate?: boolean;
71
+ };
72
+
23
73
  export interface VideoViewProps extends ViewProps {
24
74
  /**
25
75
  * A video player instance. Use [`useVideoPlayer()`](#usevideoplayersource-setup) hook to create one.
@@ -41,10 +91,17 @@ export interface VideoViewProps extends ViewProps {
41
91
 
42
92
  /**
43
93
  * Determines whether fullscreen mode is allowed or not.
94
+ *
95
+ * > Note: This option has been deprecated in favor of the `fullscreenOptions` prop and will be disabled in the future.
44
96
  * @default true
45
97
  */
46
98
  allowsFullscreen?: boolean;
47
99
 
100
+ /**
101
+ * Determines the fullscreen mode options.
102
+ */
103
+ fullscreenOptions?: FullscreenOptions;
104
+
48
105
  /**
49
106
  * Determines whether the timecodes should be displayed or not.
50
107
  * @default true
@@ -30,6 +30,8 @@ export function isPictureInPictureSupported(): boolean {
30
30
 
31
31
  export const VideoView = forwardRef((props: { player?: VideoPlayer } & VideoViewProps, ref) => {
32
32
  const videoRef = useRef<null | HTMLVideoElement>(null);
33
+ const fullscreenEnabled =
34
+ props.fullscreenOptions?.enable ?? props.allowsFullscreen ?? true;
33
35
  const mediaNodeRef = useRef<null | MediaElementAudioSourceNode>(null);
34
36
  const hasToSetupAudioContext = useRef(false);
35
37
  const fullscreenChangeListener = useRef<null | (() => void)>(null);
@@ -46,7 +48,7 @@ export const VideoView = forwardRef((props: { player?: VideoPlayer } & VideoView
46
48
 
47
49
  useImperativeHandle(ref, () => ({
48
50
  enterFullscreen: async () => {
49
- if (!props.allowsFullscreen) {
51
+ if (!fullscreenEnabled) {
50
52
  return;
51
53
  }
52
54
  await videoRef.current?.requestFullscreen();
@@ -182,7 +184,7 @@ export const VideoView = forwardRef((props: { player?: VideoPlayer } & VideoView
182
184
  return (
183
185
  <video
184
186
  controls={props.nativeControls ?? true}
185
- controlsList={props.allowsFullscreen ? undefined : 'nofullscreen'}
187
+ controlsList={fullscreenEnabled ? undefined : 'nofullscreen'}
186
188
  crossOrigin={props.crossOrigin}
187
189
  style={{
188
190
  ...mapStyles(props.style),
package/src/index.ts CHANGED
@@ -10,7 +10,13 @@ export {
10
10
  export { VideoView } from './VideoView';
11
11
  export { useVideoPlayer } from './VideoPlayer';
12
12
 
13
- export { VideoContentFit, VideoViewProps, SurfaceType } from './VideoView.types';
13
+ export {
14
+ VideoContentFit,
15
+ VideoViewProps,
16
+ SurfaceType,
17
+ FullscreenOptions,
18
+ FullscreenOrientation,
19
+ } from './VideoView.types';
14
20
  export { VideoThumbnail } from './VideoThumbnail';
15
21
 
16
22
  export { createVideoPlayer } from './VideoPlayer';