@stepincto/expo-video 1.0.0
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.
- package/README.md +45 -0
- package/android/build.gradle +32 -0
- package/android/src/main/AndroidManifest.xml +20 -0
- package/android/src/main/java/expo/modules/video/AudioFocusManager.kt +241 -0
- package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +145 -0
- package/android/src/main/java/expo/modules/video/IntervalUpdateClock.kt +54 -0
- package/android/src/main/java/expo/modules/video/MediaMetadataRetriever.kt +89 -0
- package/android/src/main/java/expo/modules/video/PictureInPictureHelperFragment.kt +26 -0
- package/android/src/main/java/expo/modules/video/PlayerViewExtension.kt +36 -0
- package/android/src/main/java/expo/modules/video/VideoCache.kt +104 -0
- package/android/src/main/java/expo/modules/video/VideoExceptions.kt +34 -0
- package/android/src/main/java/expo/modules/video/VideoManager.kt +133 -0
- package/android/src/main/java/expo/modules/video/VideoModule.kt +414 -0
- package/android/src/main/java/expo/modules/video/VideoThumbnail.kt +20 -0
- package/android/src/main/java/expo/modules/video/VideoView.kt +367 -0
- package/android/src/main/java/expo/modules/video/delegates/IgnoreSameSet.kt +24 -0
- package/android/src/main/java/expo/modules/video/drawing/OutlineProvider.kt +217 -0
- package/android/src/main/java/expo/modules/video/enums/AudioMixingMode.kt +20 -0
- package/android/src/main/java/expo/modules/video/enums/ContentFit.kt +19 -0
- package/android/src/main/java/expo/modules/video/enums/ContentType.kt +22 -0
- package/android/src/main/java/expo/modules/video/enums/DRMType.kt +26 -0
- package/android/src/main/java/expo/modules/video/enums/PlayerStatus.kt +10 -0
- package/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt +184 -0
- package/android/src/main/java/expo/modules/video/playbackService/PlaybackServiceConnection.kt +39 -0
- package/android/src/main/java/expo/modules/video/playbackService/VideoMediaSessionCallback.kt +47 -0
- package/android/src/main/java/expo/modules/video/player/FirstFrameEventGenerator.kt +93 -0
- package/android/src/main/java/expo/modules/video/player/PlayerEvent.kt +164 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayer.kt +460 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerAudioTracks.kt +125 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerListener.kt +32 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerLoadControl.kt +525 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerSubtitles.kt +125 -0
- package/android/src/main/java/expo/modules/video/records/BufferOptions.kt +15 -0
- package/android/src/main/java/expo/modules/video/records/DRMOptions.kt +25 -0
- package/android/src/main/java/expo/modules/video/records/PlaybackError.kt +19 -0
- package/android/src/main/java/expo/modules/video/records/Tracks.kt +81 -0
- package/android/src/main/java/expo/modules/video/records/VideoEventPayloads.kt +79 -0
- package/android/src/main/java/expo/modules/video/records/VideoMetadata.kt +12 -0
- package/android/src/main/java/expo/modules/video/records/VideoSize.kt +14 -0
- package/android/src/main/java/expo/modules/video/records/VideoSource.kt +104 -0
- package/android/src/main/java/expo/modules/video/records/VideoThumbnailOptions.kt +24 -0
- package/android/src/main/java/expo/modules/video/utils/DataSourceUtils.kt +75 -0
- package/android/src/main/java/expo/modules/video/utils/EventDispatcherUtils.kt +43 -0
- package/android/src/main/java/expo/modules/video/utils/MutableWeakReference.kt +15 -0
- package/android/src/main/java/expo/modules/video/utils/PictureInPictureUtils.kt +96 -0
- package/android/src/main/java/expo/modules/video/utils/YogaUtils.kt +20 -0
- package/android/src/main/res/drawable/seek_backwards_10s.xml +25 -0
- package/android/src/main/res/drawable/seek_backwards_15s.xml +25 -0
- package/android/src/main/res/drawable/seek_backwards_5s.xml +25 -0
- package/android/src/main/res/drawable/seek_forwards_10s.xml +30 -0
- package/android/src/main/res/drawable/seek_forwards_15s.xml +31 -0
- package/android/src/main/res/drawable/seek_forwards_5s.xml +30 -0
- package/android/src/main/res/layout/fullscreen_player_activity.xml +16 -0
- package/android/src/main/res/layout/surface_player_view.xml +7 -0
- package/android/src/main/res/layout/texture_player_view.xml +7 -0
- package/android/src/main/res/values/styles.xml +9 -0
- package/app.plugin.js +1 -0
- package/build/NativeVideoModule.d.ts +16 -0
- package/build/NativeVideoModule.d.ts.map +1 -0
- package/build/NativeVideoModule.js +3 -0
- package/build/NativeVideoModule.js.map +1 -0
- package/build/NativeVideoModule.web.d.ts +3 -0
- package/build/NativeVideoModule.web.d.ts.map +1 -0
- package/build/NativeVideoModule.web.js +2 -0
- package/build/NativeVideoModule.web.js.map +1 -0
- package/build/NativeVideoView.d.ts +4 -0
- package/build/NativeVideoView.d.ts.map +1 -0
- package/build/NativeVideoView.js +6 -0
- package/build/NativeVideoView.js.map +1 -0
- package/build/VideoModule.d.ts +38 -0
- package/build/VideoModule.d.ts.map +1 -0
- package/build/VideoModule.js +53 -0
- package/build/VideoModule.js.map +1 -0
- package/build/VideoPlayer.d.ts +15 -0
- package/build/VideoPlayer.d.ts.map +1 -0
- package/build/VideoPlayer.js +52 -0
- package/build/VideoPlayer.js.map +1 -0
- package/build/VideoPlayer.types.d.ts +532 -0
- package/build/VideoPlayer.types.d.ts.map +1 -0
- package/build/VideoPlayer.types.js +2 -0
- package/build/VideoPlayer.types.js.map +1 -0
- package/build/VideoPlayer.web.d.ts +75 -0
- package/build/VideoPlayer.web.d.ts.map +1 -0
- package/build/VideoPlayer.web.js +376 -0
- package/build/VideoPlayer.web.js.map +1 -0
- package/build/VideoPlayerEvents.types.d.ts +262 -0
- package/build/VideoPlayerEvents.types.d.ts.map +1 -0
- package/build/VideoPlayerEvents.types.js +2 -0
- package/build/VideoPlayerEvents.types.js.map +1 -0
- package/build/VideoThumbnail.d.ts +29 -0
- package/build/VideoThumbnail.d.ts.map +1 -0
- package/build/VideoThumbnail.js +3 -0
- package/build/VideoThumbnail.js.map +1 -0
- package/build/VideoView.d.ts +44 -0
- package/build/VideoView.d.ts.map +1 -0
- package/build/VideoView.js +76 -0
- package/build/VideoView.js.map +1 -0
- package/build/VideoView.types.d.ts +147 -0
- package/build/VideoView.types.d.ts.map +1 -0
- package/build/VideoView.types.js +2 -0
- package/build/VideoView.types.js.map +1 -0
- package/build/VideoView.web.d.ts +9 -0
- package/build/VideoView.web.d.ts.map +1 -0
- package/build/VideoView.web.js +180 -0
- package/build/VideoView.web.js.map +1 -0
- package/build/index.d.ts +9 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +7 -0
- package/build/index.js.map +1 -0
- package/build/resolveAssetSource.d.ts +3 -0
- package/build/resolveAssetSource.d.ts.map +1 -0
- package/build/resolveAssetSource.js +3 -0
- package/build/resolveAssetSource.js.map +1 -0
- package/build/resolveAssetSource.web.d.ts +4 -0
- package/build/resolveAssetSource.web.d.ts.map +1 -0
- package/build/resolveAssetSource.web.js +16 -0
- package/build/resolveAssetSource.web.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/Cache/CachableRequest.swift +44 -0
- package/ios/Cache/CachedResource.swift +97 -0
- package/ios/Cache/CachingHelpers.swift +92 -0
- package/ios/Cache/MediaFileHandle.swift +94 -0
- package/ios/Cache/MediaInfo.swift +147 -0
- package/ios/Cache/ResourceLoaderDelegate.swift +274 -0
- package/ios/Cache/SynchronizedHashTable.swift +23 -0
- package/ios/Cache/VideoCacheManager.swift +338 -0
- package/ios/ContentKeyDelegate.swift +214 -0
- package/ios/ContentKeyManager.swift +21 -0
- package/ios/Enums/AudioMixingMode.swift +37 -0
- package/ios/Enums/ContentType.swift +12 -0
- package/ios/Enums/DRMType.swift +20 -0
- package/ios/Enums/PlayerStatus.swift +10 -0
- package/ios/Enums/VideoContentFit.swift +39 -0
- package/ios/ExpoVideo.podspec +29 -0
- package/ios/NowPlayingManager.swift +296 -0
- package/ios/Records/BufferOptions.swift +12 -0
- package/ios/Records/DRMOptions.swift +24 -0
- package/ios/Records/PlaybackError.swift +10 -0
- package/ios/Records/Tracks.swift +176 -0
- package/ios/Records/VideoEventPayloads.swift +76 -0
- package/ios/Records/VideoMetadata.swift +16 -0
- package/ios/Records/VideoSize.swift +15 -0
- package/ios/Records/VideoSource.swift +25 -0
- package/ios/Thumbnails/VideoThumbnail.swift +27 -0
- package/ios/Thumbnails/VideoThumbnailGenerator.swift +68 -0
- package/ios/Thumbnails/VideoThumbnailOptions.swift +15 -0
- package/ios/VideoAsset.swift +123 -0
- package/ios/VideoExceptions.swift +53 -0
- package/ios/VideoItem.swift +11 -0
- package/ios/VideoManager.swift +140 -0
- package/ios/VideoModule.swift +383 -0
- package/ios/VideoPlayer/DangerousPropertiesStore.swift +19 -0
- package/ios/VideoPlayer.swift +435 -0
- package/ios/VideoPlayerAudioTracks.swift +72 -0
- package/ios/VideoPlayerItem.swift +97 -0
- package/ios/VideoPlayerObserver.swift +523 -0
- package/ios/VideoPlayerSubtitles.swift +71 -0
- package/ios/VideoSourceLoader.swift +89 -0
- package/ios/VideoSourceLoaderListener.swift +34 -0
- package/ios/VideoView.swift +224 -0
- package/package.json +59 -0
- package/plugin/build/tsconfig.tsbuildinfo +1 -0
- package/plugin/build/withExpoVideo.d.ts +7 -0
- package/plugin/build/withExpoVideo.js +38 -0
- package/src/NativeVideoModule.ts +20 -0
- package/src/NativeVideoModule.web.ts +1 -0
- package/src/NativeVideoView.ts +8 -0
- package/src/VideoModule.ts +59 -0
- package/src/VideoPlayer.tsx +67 -0
- package/src/VideoPlayer.types.ts +613 -0
- package/src/VideoPlayer.web.tsx +451 -0
- package/src/VideoPlayerEvents.types.ts +313 -0
- package/src/VideoThumbnail.ts +31 -0
- package/src/VideoView.tsx +86 -0
- package/src/VideoView.types.ts +165 -0
- package/src/VideoView.web.tsx +214 -0
- package/src/index.ts +46 -0
- package/src/resolveAssetSource.ts +2 -0
- package/src/resolveAssetSource.web.ts +17 -0
- package/src/ts-declarations/react-native-assets.d.ts +1 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
protocol VideoSourceLoaderListener: AnyObject {
|
|
4
|
+
func onLoadingStarted(loader: VideoSourceLoader, videoSource: VideoSource?)
|
|
5
|
+
func onLoadingFinished(loader: VideoSourceLoader, videoSource: VideoSource?, result: VideoPlayerItem?)
|
|
6
|
+
func onLoadingCancelled(loader: VideoSourceLoader, videoSource: VideoSource?)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
extension VideoSourceLoaderListener {
|
|
10
|
+
func onLoadingStarted(loader: VideoSourceLoader, videoSource: VideoSource?) {}
|
|
11
|
+
func onLoadingFinished(loader: VideoSourceLoader, videoSource: VideoSource?, result: VideoPlayerItem?) {}
|
|
12
|
+
func onLoadingCancelled(loader: VideoSourceLoader, videoSource: VideoSource?) {}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
final class WeakVideoSourceLoaderListener: Hashable {
|
|
16
|
+
private(set) weak var value: VideoSourceLoaderListener?
|
|
17
|
+
|
|
18
|
+
init(value: VideoSourceLoaderListener? = nil) {
|
|
19
|
+
self.value = value
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static func == (lhs: WeakVideoSourceLoaderListener, rhs: WeakVideoSourceLoaderListener) -> Bool {
|
|
23
|
+
guard let lhsValue = lhs.value, let rhsValue = rhs.value else {
|
|
24
|
+
return lhs.value == nil && rhs.value == nil
|
|
25
|
+
}
|
|
26
|
+
return ObjectIdentifier(lhsValue) == ObjectIdentifier(rhsValue)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func hash(into hasher: inout Hasher) {
|
|
30
|
+
if let value {
|
|
31
|
+
hasher.combine(ObjectIdentifier(value))
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// Copyright 2023-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import AVKit
|
|
4
|
+
import ExpoModulesCore
|
|
5
|
+
|
|
6
|
+
public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
|
7
|
+
lazy var playerViewController = AVPlayerViewController()
|
|
8
|
+
|
|
9
|
+
weak var player: VideoPlayer? {
|
|
10
|
+
didSet {
|
|
11
|
+
playerViewController.player = player?.ref
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
#if os(tvOS)
|
|
16
|
+
var wasPlaying: Bool = false
|
|
17
|
+
#endif
|
|
18
|
+
var isFullscreen: Bool = false
|
|
19
|
+
var isInPictureInPicture = false
|
|
20
|
+
#if os(tvOS)
|
|
21
|
+
let startPictureInPictureAutomatically = false
|
|
22
|
+
#else
|
|
23
|
+
var startPictureInPictureAutomatically = false {
|
|
24
|
+
didSet {
|
|
25
|
+
playerViewController.canStartPictureInPictureAutomaticallyFromInline = startPictureInPictureAutomatically
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
#endif
|
|
29
|
+
|
|
30
|
+
var allowPictureInPicture: Bool = false {
|
|
31
|
+
didSet {
|
|
32
|
+
// PiP requires `.playback` audio session category in `.moviePlayback` mode
|
|
33
|
+
VideoManager.shared.setAppropriateAudioSessionOrWarn()
|
|
34
|
+
playerViewController.allowsPictureInPicturePlayback = allowPictureInPicture
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let onPictureInPictureStart = EventDispatcher()
|
|
39
|
+
let onPictureInPictureStop = EventDispatcher()
|
|
40
|
+
let onFullscreenEnter = EventDispatcher()
|
|
41
|
+
let onFullscreenExit = EventDispatcher()
|
|
42
|
+
let onFirstFrameRender = EventDispatcher()
|
|
43
|
+
|
|
44
|
+
var firstFrameObserver: NSKeyValueObservation?
|
|
45
|
+
|
|
46
|
+
public override var bounds: CGRect {
|
|
47
|
+
didSet {
|
|
48
|
+
playerViewController.view.frame = self.bounds
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public required init(appContext: AppContext? = nil) {
|
|
53
|
+
super.init(appContext: appContext)
|
|
54
|
+
|
|
55
|
+
VideoManager.shared.register(videoView: self)
|
|
56
|
+
|
|
57
|
+
clipsToBounds = true
|
|
58
|
+
playerViewController.delegate = self
|
|
59
|
+
playerViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
60
|
+
playerViewController.view.backgroundColor = .clear
|
|
61
|
+
// Now playing is managed by the `NowPlayingManager`
|
|
62
|
+
#if !os(tvOS)
|
|
63
|
+
playerViewController.updatesNowPlayingInfoCenter = false
|
|
64
|
+
#endif
|
|
65
|
+
|
|
66
|
+
addFirstFrameObserver()
|
|
67
|
+
addSubview(playerViewController.view)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
deinit {
|
|
71
|
+
VideoManager.shared.unregister(videoView: self)
|
|
72
|
+
removeFirstFrameObserver()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
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 {
|
|
85
|
+
#if os(tvOS)
|
|
86
|
+
// For TV, save the currently playing state,
|
|
87
|
+
// remove the view controller from its superview,
|
|
88
|
+
// and present the view controller normally
|
|
89
|
+
wasPlaying = player?.isPlaying == true
|
|
90
|
+
self.playerViewController.view.removeFromSuperview()
|
|
91
|
+
self.reactViewController().present(self.playerViewController, animated: true)
|
|
92
|
+
onFullscreenEnter()
|
|
93
|
+
isFullscreen = true
|
|
94
|
+
#endif
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
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
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
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
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
func stopPictureInPicture() {
|
|
124
|
+
let selectorName = "stopPictureInPicture"
|
|
125
|
+
let selectorToStopPictureInPicture = NSSelectorFromString(selectorName)
|
|
126
|
+
|
|
127
|
+
if playerViewController.responds(to: selectorToStopPictureInPicture) {
|
|
128
|
+
playerViewController.perform(selectorToStopPictureInPicture)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// MARK: - AVPlayerViewControllerDelegate
|
|
133
|
+
|
|
134
|
+
#if os(tvOS)
|
|
135
|
+
// TV actually presents the playerViewController, so it implements the view controller
|
|
136
|
+
// dismissal delegate methods
|
|
137
|
+
public func playerViewControllerWillBeginDismissalTransition(_ playerViewController: AVPlayerViewController) {
|
|
138
|
+
// Start an appearance transition
|
|
139
|
+
self.playerViewController.beginAppearanceTransition(true, animated: true)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public func playerViewControllerDidEndDismissalTransition(_ playerViewController: AVPlayerViewController) {
|
|
143
|
+
self.onFullscreenExit()
|
|
144
|
+
self.isFullscreen = false
|
|
145
|
+
// Reset the bounds of the view controller and add it back to our view
|
|
146
|
+
self.playerViewController.view.frame = self.bounds
|
|
147
|
+
addSubview(self.playerViewController.view)
|
|
148
|
+
// End the appearance transition
|
|
149
|
+
self.playerViewController.endAppearanceTransition()
|
|
150
|
+
// Ensure playing state is preserved
|
|
151
|
+
if wasPlaying {
|
|
152
|
+
self.player?.ref.play()
|
|
153
|
+
} else {
|
|
154
|
+
self.player?.ref.pause()
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
#endif
|
|
158
|
+
|
|
159
|
+
#if !os(tvOS)
|
|
160
|
+
public func playerViewController(
|
|
161
|
+
_ playerViewController: AVPlayerViewController,
|
|
162
|
+
willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator
|
|
163
|
+
) {
|
|
164
|
+
onFullscreenEnter()
|
|
165
|
+
isFullscreen = true
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
public func playerViewController(
|
|
169
|
+
_ playerViewController: AVPlayerViewController,
|
|
170
|
+
willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator
|
|
171
|
+
) {
|
|
172
|
+
// Platform's behavior is to pause the player when exiting the fullscreen mode.
|
|
173
|
+
// It seems better to continue playing, so we resume the player once the dismissing animation finishes.
|
|
174
|
+
let wasPlaying = player?.ref.timeControlStatus == .playing
|
|
175
|
+
|
|
176
|
+
coordinator.animate(alongsideTransition: nil) { context in
|
|
177
|
+
if !context.isCancelled {
|
|
178
|
+
if wasPlaying {
|
|
179
|
+
self.player?.ref.play()
|
|
180
|
+
}
|
|
181
|
+
self.onFullscreenExit()
|
|
182
|
+
self.isFullscreen = false
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
#endif
|
|
187
|
+
|
|
188
|
+
public func playerViewControllerDidStartPictureInPicture(_ playerViewController: AVPlayerViewController) {
|
|
189
|
+
isInPictureInPicture = true
|
|
190
|
+
onPictureInPictureStart()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
public func playerViewControllerDidStopPictureInPicture(_ playerViewController: AVPlayerViewController) {
|
|
194
|
+
isInPictureInPicture = false
|
|
195
|
+
onPictureInPictureStop()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
public override func didMoveToWindow() {
|
|
199
|
+
// TV is doing a normal view controller present, so we should not execute
|
|
200
|
+
// this code
|
|
201
|
+
#if !os(tvOS)
|
|
202
|
+
playerViewController.beginAppearanceTransition(self.window != nil, animated: true)
|
|
203
|
+
#endif
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
public override func safeAreaInsetsDidChange() {
|
|
207
|
+
super.safeAreaInsetsDidChange()
|
|
208
|
+
// This is the only way that I (@behenate) found to force re-calculation of the safe-area insets for native controls
|
|
209
|
+
playerViewController.view.removeFromSuperview()
|
|
210
|
+
addSubview(playerViewController.view)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private func addFirstFrameObserver() {
|
|
214
|
+
firstFrameObserver = playerViewController.observe(\.isReadyForDisplay, changeHandler: { [weak self] playerViewController, _ in
|
|
215
|
+
if playerViewController.isReadyForDisplay {
|
|
216
|
+
self?.onFirstFrameRender()
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
private func removeFirstFrameObserver() {
|
|
221
|
+
firstFrameObserver?.invalidate()
|
|
222
|
+
firstFrameObserver = nil
|
|
223
|
+
}
|
|
224
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stepincto/expo-video",
|
|
3
|
+
"title": "Expo Video",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"originalUpstreamVersion": "2.2.2",
|
|
6
|
+
"description": "A cross-platform, performant video component for React Native and Expo with Web support",
|
|
7
|
+
"main": "build/index.js",
|
|
8
|
+
"types": "build/index.d.ts",
|
|
9
|
+
"sideEffects": false,
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "expo-module build",
|
|
12
|
+
"clean": "expo-module clean",
|
|
13
|
+
"lint": "expo-module lint",
|
|
14
|
+
"test": "expo-module test",
|
|
15
|
+
"prepare": "expo-module prepare",
|
|
16
|
+
"prepublishOnly": "expo-module prepublishOnly",
|
|
17
|
+
"expo-module": "expo-module",
|
|
18
|
+
"build:lib": "tsc -p tsconfig.json",
|
|
19
|
+
"build:plugin": "tsc -p plugin/tsconfig.json",
|
|
20
|
+
"build:once": "npm run build:lib && npm run build:plugin",
|
|
21
|
+
"pack:local": "npm pack --ignore-scripts"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://docs.expo.dev/versions/latest/sdk/video/",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/expo/expo.git"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"react-native",
|
|
30
|
+
"expo",
|
|
31
|
+
"video",
|
|
32
|
+
"player"
|
|
33
|
+
],
|
|
34
|
+
"author": "650 Industries, Inc.",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/react": "^19.1.9",
|
|
38
|
+
"@types/react-dom": "^19.1.7",
|
|
39
|
+
"expo-module-scripts": "^4.1.10",
|
|
40
|
+
"typescript": "^5.9.2"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"expo": "^53.0.0",
|
|
44
|
+
"expo-modules-core": ">=2.4 <2.6",
|
|
45
|
+
"react": ">=18",
|
|
46
|
+
"react-native": ">=0.75"
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"build",
|
|
50
|
+
"ios",
|
|
51
|
+
"android",
|
|
52
|
+
"plugin/build",
|
|
53
|
+
"src",
|
|
54
|
+
"app.plugin.js",
|
|
55
|
+
"expo-module.config.json",
|
|
56
|
+
"README.md",
|
|
57
|
+
"LICENSE"
|
|
58
|
+
]
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["../src/withexpovideo.ts"],"version":"5.9.2"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ConfigPlugin } from 'expo/config-plugins';
|
|
2
|
+
type WithExpoVideoOptions = {
|
|
3
|
+
supportsBackgroundPlayback?: boolean;
|
|
4
|
+
supportsPictureInPicture?: boolean;
|
|
5
|
+
};
|
|
6
|
+
declare const withExpoVideo: ConfigPlugin<WithExpoVideoOptions>;
|
|
7
|
+
export default withExpoVideo;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const config_plugins_1 = require("expo/config-plugins");
|
|
4
|
+
const withExpoVideo = (config, { supportsBackgroundPlayback, supportsPictureInPicture } = {}) => {
|
|
5
|
+
(0, config_plugins_1.withInfoPlist)(config, (config) => {
|
|
6
|
+
var _a;
|
|
7
|
+
const currentBackgroundModes = (_a = config.modResults.UIBackgroundModes) !== null && _a !== void 0 ? _a : [];
|
|
8
|
+
const shouldEnableBackgroundAudio = supportsBackgroundPlayback || supportsPictureInPicture;
|
|
9
|
+
// No-op if the values are not defined
|
|
10
|
+
if (typeof supportsBackgroundPlayback === 'undefined' &&
|
|
11
|
+
typeof supportsPictureInPicture === 'undefined') {
|
|
12
|
+
return config;
|
|
13
|
+
}
|
|
14
|
+
if (shouldEnableBackgroundAudio && !currentBackgroundModes.includes('audio')) {
|
|
15
|
+
config.modResults.UIBackgroundModes = [...currentBackgroundModes, 'audio'];
|
|
16
|
+
}
|
|
17
|
+
else if (!shouldEnableBackgroundAudio) {
|
|
18
|
+
config.modResults.UIBackgroundModes = currentBackgroundModes.filter((mode) => mode !== 'audio');
|
|
19
|
+
}
|
|
20
|
+
return config;
|
|
21
|
+
});
|
|
22
|
+
(0, config_plugins_1.withAndroidManifest)(config, (config) => {
|
|
23
|
+
const activity = config_plugins_1.AndroidConfig.Manifest.getMainActivityOrThrow(config.modResults);
|
|
24
|
+
// No-op if the values are not defined
|
|
25
|
+
if (typeof supportsPictureInPicture === 'undefined') {
|
|
26
|
+
return config;
|
|
27
|
+
}
|
|
28
|
+
if (supportsPictureInPicture) {
|
|
29
|
+
activity.$['android:supportsPictureInPicture'] = 'true';
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
delete activity.$['android:supportsPictureInPicture'];
|
|
33
|
+
}
|
|
34
|
+
return config;
|
|
35
|
+
});
|
|
36
|
+
return config;
|
|
37
|
+
};
|
|
38
|
+
exports.default = withExpoVideo;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { requireNativeModule } from 'expo-modules-core';
|
|
2
|
+
|
|
3
|
+
import type { VideoPlayer } from './VideoPlayer.types';
|
|
4
|
+
import type { VideoThumbnail } from './VideoThumbnail';
|
|
5
|
+
|
|
6
|
+
type ExpoVideoModule = {
|
|
7
|
+
VideoPlayer: typeof VideoPlayer;
|
|
8
|
+
VideoThumbnail: typeof VideoThumbnail;
|
|
9
|
+
|
|
10
|
+
isPictureInPictureSupported(): boolean;
|
|
11
|
+
setVideoCacheSizeAsync(sizeBytes: number): Promise<void>;
|
|
12
|
+
clearVideoCacheAsync(): Promise<void>;
|
|
13
|
+
getCurrentVideoCacheSize(): number;
|
|
14
|
+
|
|
15
|
+
preCacheVideoPartialAsync(url: string, chunkSize?: number): Promise<boolean>;
|
|
16
|
+
preCacheVideoAsync(url: string): Promise<boolean>;
|
|
17
|
+
isVideoCachedAsync(url: string): Promise<boolean>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default requireNativeModule<ExpoVideoModule>('ExpoVideo');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default () => {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { requireNativeViewManager } from 'expo-modules-core';
|
|
2
|
+
import { Platform } from 'react-native';
|
|
3
|
+
|
|
4
|
+
const defaultViewName = Platform.OS === 'android' ? 'SurfaceVideoView' : 'VideoView';
|
|
5
|
+
|
|
6
|
+
export default requireNativeViewManager('ExpoVideo', defaultViewName);
|
|
7
|
+
export const NativeTextureVideoView =
|
|
8
|
+
Platform.OS === 'android' ? requireNativeViewManager('ExpoVideo', 'TextureVideoView') : null;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import NativeVideoModule from './NativeVideoModule';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns whether the current device supports Picture in Picture (PiP) mode.
|
|
5
|
+
*
|
|
6
|
+
* @returns A `boolean` which is `true` if the device supports PiP mode, and `false` otherwise.
|
|
7
|
+
* @platform android
|
|
8
|
+
* @platform ios
|
|
9
|
+
*/
|
|
10
|
+
export function isPictureInPictureSupported(): boolean {
|
|
11
|
+
return NativeVideoModule.isPictureInPictureSupported();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Clears all video cache.
|
|
16
|
+
* > This function can be called only if there are no existing `VideoPlayer` instances.
|
|
17
|
+
*
|
|
18
|
+
* @returns A promise that fulfills after the cache has been cleaned.
|
|
19
|
+
* @platform android
|
|
20
|
+
* @platform ios
|
|
21
|
+
*/
|
|
22
|
+
export function clearVideoCacheAsync(): Promise<void> {
|
|
23
|
+
return NativeVideoModule.clearVideoCacheAsync();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Sets desired video cache size in bytes. The default video cache size is 1GB. Value set by this function is persistent.
|
|
28
|
+
* The cache size is not guaranteed to be exact and the actual cache size may be slightly larger. The cache is evicted on a least-recently-used basis.
|
|
29
|
+
* > This function can be called only if there are no existing `VideoPlayer` instances.
|
|
30
|
+
*
|
|
31
|
+
* @returns A promise that fulfills after the cache size has been set.
|
|
32
|
+
* @platform android
|
|
33
|
+
* @platform ios
|
|
34
|
+
*/
|
|
35
|
+
export function setVideoCacheSizeAsync(sizeBytes: number): Promise<void> {
|
|
36
|
+
return NativeVideoModule.setVideoCacheSizeAsync(sizeBytes);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns the space currently occupied by the video cache in bytes.
|
|
41
|
+
*
|
|
42
|
+
* @platform android
|
|
43
|
+
* @platform ios
|
|
44
|
+
*/
|
|
45
|
+
export function getCurrentVideoCacheSize(): number {
|
|
46
|
+
return NativeVideoModule.getCurrentVideoCacheSize();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function preCacheVideoAsync(url: string): Promise<boolean> {
|
|
50
|
+
return NativeVideoModule.preCacheVideoAsync(url);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function preCacheVideoPartialAsync(url: string, chunkSize?: number): Promise<boolean> {
|
|
54
|
+
return NativeVideoModule.preCacheVideoPartialAsync(url, chunkSize);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function isVideoCachedAsync(url: string): Promise<boolean> {
|
|
58
|
+
return NativeVideoModule.isVideoCachedAsync(url);
|
|
59
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useReleasingSharedObject } from 'expo-modules-core';
|
|
2
|
+
|
|
3
|
+
import NativeVideoModule from './NativeVideoModule';
|
|
4
|
+
import { VideoSource, VideoPlayer } from './VideoPlayer.types';
|
|
5
|
+
import resolveAssetSource from './resolveAssetSource';
|
|
6
|
+
|
|
7
|
+
// TODO: Temporary solution until we develop a way of overriding prototypes that won't break the lazy loading of the module.
|
|
8
|
+
const replace = NativeVideoModule.VideoPlayer.prototype.replace;
|
|
9
|
+
NativeVideoModule.VideoPlayer.prototype.replace = function (
|
|
10
|
+
source: VideoSource,
|
|
11
|
+
disableWarning: boolean = false
|
|
12
|
+
) {
|
|
13
|
+
if (!disableWarning) {
|
|
14
|
+
console.warn(
|
|
15
|
+
'On iOS `VideoPlayer.replace` loads the asset data synchronously on the main thread, which can lead to UI freezes and will be deprecated in a future release. Switch to `replaceAsync` for better user experience.'
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return replace.call(this, parseSource(source));
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const replaceAsync = NativeVideoModule.VideoPlayer.prototype.replaceAsync;
|
|
23
|
+
NativeVideoModule.VideoPlayer.prototype.replaceAsync = function (source: VideoSource) {
|
|
24
|
+
return replaceAsync.call(this, parseSource(source));
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Creates a direct instance of `VideoPlayer` that doesn't release automatically.
|
|
28
|
+
*
|
|
29
|
+
* > **info** For most use cases you should use the [`useVideoPlayer`](#usevideoplayer) hook instead. See the [Using the VideoPlayer Directly](#using-the-videoplayer-directly) section for more details.
|
|
30
|
+
* @param source
|
|
31
|
+
*/
|
|
32
|
+
export function createVideoPlayer(source: VideoSource): VideoPlayer {
|
|
33
|
+
return new NativeVideoModule.VideoPlayer(parseSource(source));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Creates a `VideoPlayer`, which will be automatically cleaned up when the component is unmounted.
|
|
38
|
+
* @param source - A video source that is used to initialize the player.
|
|
39
|
+
* @param setup - A function that allows setting up the player. It will run after the player is created.
|
|
40
|
+
*/
|
|
41
|
+
export function useVideoPlayer(
|
|
42
|
+
source: VideoSource,
|
|
43
|
+
setup?: (player: VideoPlayer) => void
|
|
44
|
+
): VideoPlayer {
|
|
45
|
+
const parsedSource = parseSource(source);
|
|
46
|
+
|
|
47
|
+
return useReleasingSharedObject(() => {
|
|
48
|
+
const player = new NativeVideoModule.VideoPlayer(parsedSource);
|
|
49
|
+
setup?.(player);
|
|
50
|
+
return player;
|
|
51
|
+
}, [JSON.stringify(parsedSource)]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseSource(source: VideoSource): VideoSource {
|
|
55
|
+
if (typeof source === 'number') {
|
|
56
|
+
// TODO(@kitten): This seems to not handle the `null` case. Is this correct?
|
|
57
|
+
return { uri: resolveAssetSource(source)!.uri };
|
|
58
|
+
} else if (typeof source === 'string') {
|
|
59
|
+
return { uri: source };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (typeof source?.assetId === 'number' && !source.uri) {
|
|
63
|
+
// TODO(@kitten): This seems to not handle the `null` case. Is this correct?
|
|
64
|
+
return { ...source, uri: resolveAssetSource(source.assetId)!.uri };
|
|
65
|
+
}
|
|
66
|
+
return source;
|
|
67
|
+
}
|