@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.
Files changed (180) hide show
  1. package/README.md +45 -0
  2. package/android/build.gradle +32 -0
  3. package/android/src/main/AndroidManifest.xml +20 -0
  4. package/android/src/main/java/expo/modules/video/AudioFocusManager.kt +241 -0
  5. package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +145 -0
  6. package/android/src/main/java/expo/modules/video/IntervalUpdateClock.kt +54 -0
  7. package/android/src/main/java/expo/modules/video/MediaMetadataRetriever.kt +89 -0
  8. package/android/src/main/java/expo/modules/video/PictureInPictureHelperFragment.kt +26 -0
  9. package/android/src/main/java/expo/modules/video/PlayerViewExtension.kt +36 -0
  10. package/android/src/main/java/expo/modules/video/VideoCache.kt +104 -0
  11. package/android/src/main/java/expo/modules/video/VideoExceptions.kt +34 -0
  12. package/android/src/main/java/expo/modules/video/VideoManager.kt +133 -0
  13. package/android/src/main/java/expo/modules/video/VideoModule.kt +414 -0
  14. package/android/src/main/java/expo/modules/video/VideoThumbnail.kt +20 -0
  15. package/android/src/main/java/expo/modules/video/VideoView.kt +367 -0
  16. package/android/src/main/java/expo/modules/video/delegates/IgnoreSameSet.kt +24 -0
  17. package/android/src/main/java/expo/modules/video/drawing/OutlineProvider.kt +217 -0
  18. package/android/src/main/java/expo/modules/video/enums/AudioMixingMode.kt +20 -0
  19. package/android/src/main/java/expo/modules/video/enums/ContentFit.kt +19 -0
  20. package/android/src/main/java/expo/modules/video/enums/ContentType.kt +22 -0
  21. package/android/src/main/java/expo/modules/video/enums/DRMType.kt +26 -0
  22. package/android/src/main/java/expo/modules/video/enums/PlayerStatus.kt +10 -0
  23. package/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt +184 -0
  24. package/android/src/main/java/expo/modules/video/playbackService/PlaybackServiceConnection.kt +39 -0
  25. package/android/src/main/java/expo/modules/video/playbackService/VideoMediaSessionCallback.kt +47 -0
  26. package/android/src/main/java/expo/modules/video/player/FirstFrameEventGenerator.kt +93 -0
  27. package/android/src/main/java/expo/modules/video/player/PlayerEvent.kt +164 -0
  28. package/android/src/main/java/expo/modules/video/player/VideoPlayer.kt +460 -0
  29. package/android/src/main/java/expo/modules/video/player/VideoPlayerAudioTracks.kt +125 -0
  30. package/android/src/main/java/expo/modules/video/player/VideoPlayerListener.kt +32 -0
  31. package/android/src/main/java/expo/modules/video/player/VideoPlayerLoadControl.kt +525 -0
  32. package/android/src/main/java/expo/modules/video/player/VideoPlayerSubtitles.kt +125 -0
  33. package/android/src/main/java/expo/modules/video/records/BufferOptions.kt +15 -0
  34. package/android/src/main/java/expo/modules/video/records/DRMOptions.kt +25 -0
  35. package/android/src/main/java/expo/modules/video/records/PlaybackError.kt +19 -0
  36. package/android/src/main/java/expo/modules/video/records/Tracks.kt +81 -0
  37. package/android/src/main/java/expo/modules/video/records/VideoEventPayloads.kt +79 -0
  38. package/android/src/main/java/expo/modules/video/records/VideoMetadata.kt +12 -0
  39. package/android/src/main/java/expo/modules/video/records/VideoSize.kt +14 -0
  40. package/android/src/main/java/expo/modules/video/records/VideoSource.kt +104 -0
  41. package/android/src/main/java/expo/modules/video/records/VideoThumbnailOptions.kt +24 -0
  42. package/android/src/main/java/expo/modules/video/utils/DataSourceUtils.kt +75 -0
  43. package/android/src/main/java/expo/modules/video/utils/EventDispatcherUtils.kt +43 -0
  44. package/android/src/main/java/expo/modules/video/utils/MutableWeakReference.kt +15 -0
  45. package/android/src/main/java/expo/modules/video/utils/PictureInPictureUtils.kt +96 -0
  46. package/android/src/main/java/expo/modules/video/utils/YogaUtils.kt +20 -0
  47. package/android/src/main/res/drawable/seek_backwards_10s.xml +25 -0
  48. package/android/src/main/res/drawable/seek_backwards_15s.xml +25 -0
  49. package/android/src/main/res/drawable/seek_backwards_5s.xml +25 -0
  50. package/android/src/main/res/drawable/seek_forwards_10s.xml +30 -0
  51. package/android/src/main/res/drawable/seek_forwards_15s.xml +31 -0
  52. package/android/src/main/res/drawable/seek_forwards_5s.xml +30 -0
  53. package/android/src/main/res/layout/fullscreen_player_activity.xml +16 -0
  54. package/android/src/main/res/layout/surface_player_view.xml +7 -0
  55. package/android/src/main/res/layout/texture_player_view.xml +7 -0
  56. package/android/src/main/res/values/styles.xml +9 -0
  57. package/app.plugin.js +1 -0
  58. package/build/NativeVideoModule.d.ts +16 -0
  59. package/build/NativeVideoModule.d.ts.map +1 -0
  60. package/build/NativeVideoModule.js +3 -0
  61. package/build/NativeVideoModule.js.map +1 -0
  62. package/build/NativeVideoModule.web.d.ts +3 -0
  63. package/build/NativeVideoModule.web.d.ts.map +1 -0
  64. package/build/NativeVideoModule.web.js +2 -0
  65. package/build/NativeVideoModule.web.js.map +1 -0
  66. package/build/NativeVideoView.d.ts +4 -0
  67. package/build/NativeVideoView.d.ts.map +1 -0
  68. package/build/NativeVideoView.js +6 -0
  69. package/build/NativeVideoView.js.map +1 -0
  70. package/build/VideoModule.d.ts +38 -0
  71. package/build/VideoModule.d.ts.map +1 -0
  72. package/build/VideoModule.js +53 -0
  73. package/build/VideoModule.js.map +1 -0
  74. package/build/VideoPlayer.d.ts +15 -0
  75. package/build/VideoPlayer.d.ts.map +1 -0
  76. package/build/VideoPlayer.js +52 -0
  77. package/build/VideoPlayer.js.map +1 -0
  78. package/build/VideoPlayer.types.d.ts +532 -0
  79. package/build/VideoPlayer.types.d.ts.map +1 -0
  80. package/build/VideoPlayer.types.js +2 -0
  81. package/build/VideoPlayer.types.js.map +1 -0
  82. package/build/VideoPlayer.web.d.ts +75 -0
  83. package/build/VideoPlayer.web.d.ts.map +1 -0
  84. package/build/VideoPlayer.web.js +376 -0
  85. package/build/VideoPlayer.web.js.map +1 -0
  86. package/build/VideoPlayerEvents.types.d.ts +262 -0
  87. package/build/VideoPlayerEvents.types.d.ts.map +1 -0
  88. package/build/VideoPlayerEvents.types.js +2 -0
  89. package/build/VideoPlayerEvents.types.js.map +1 -0
  90. package/build/VideoThumbnail.d.ts +29 -0
  91. package/build/VideoThumbnail.d.ts.map +1 -0
  92. package/build/VideoThumbnail.js +3 -0
  93. package/build/VideoThumbnail.js.map +1 -0
  94. package/build/VideoView.d.ts +44 -0
  95. package/build/VideoView.d.ts.map +1 -0
  96. package/build/VideoView.js +76 -0
  97. package/build/VideoView.js.map +1 -0
  98. package/build/VideoView.types.d.ts +147 -0
  99. package/build/VideoView.types.d.ts.map +1 -0
  100. package/build/VideoView.types.js +2 -0
  101. package/build/VideoView.types.js.map +1 -0
  102. package/build/VideoView.web.d.ts +9 -0
  103. package/build/VideoView.web.d.ts.map +1 -0
  104. package/build/VideoView.web.js +180 -0
  105. package/build/VideoView.web.js.map +1 -0
  106. package/build/index.d.ts +9 -0
  107. package/build/index.d.ts.map +1 -0
  108. package/build/index.js +7 -0
  109. package/build/index.js.map +1 -0
  110. package/build/resolveAssetSource.d.ts +3 -0
  111. package/build/resolveAssetSource.d.ts.map +1 -0
  112. package/build/resolveAssetSource.js +3 -0
  113. package/build/resolveAssetSource.js.map +1 -0
  114. package/build/resolveAssetSource.web.d.ts +4 -0
  115. package/build/resolveAssetSource.web.d.ts.map +1 -0
  116. package/build/resolveAssetSource.web.js +16 -0
  117. package/build/resolveAssetSource.web.js.map +1 -0
  118. package/expo-module.config.json +9 -0
  119. package/ios/Cache/CachableRequest.swift +44 -0
  120. package/ios/Cache/CachedResource.swift +97 -0
  121. package/ios/Cache/CachingHelpers.swift +92 -0
  122. package/ios/Cache/MediaFileHandle.swift +94 -0
  123. package/ios/Cache/MediaInfo.swift +147 -0
  124. package/ios/Cache/ResourceLoaderDelegate.swift +274 -0
  125. package/ios/Cache/SynchronizedHashTable.swift +23 -0
  126. package/ios/Cache/VideoCacheManager.swift +338 -0
  127. package/ios/ContentKeyDelegate.swift +214 -0
  128. package/ios/ContentKeyManager.swift +21 -0
  129. package/ios/Enums/AudioMixingMode.swift +37 -0
  130. package/ios/Enums/ContentType.swift +12 -0
  131. package/ios/Enums/DRMType.swift +20 -0
  132. package/ios/Enums/PlayerStatus.swift +10 -0
  133. package/ios/Enums/VideoContentFit.swift +39 -0
  134. package/ios/ExpoVideo.podspec +29 -0
  135. package/ios/NowPlayingManager.swift +296 -0
  136. package/ios/Records/BufferOptions.swift +12 -0
  137. package/ios/Records/DRMOptions.swift +24 -0
  138. package/ios/Records/PlaybackError.swift +10 -0
  139. package/ios/Records/Tracks.swift +176 -0
  140. package/ios/Records/VideoEventPayloads.swift +76 -0
  141. package/ios/Records/VideoMetadata.swift +16 -0
  142. package/ios/Records/VideoSize.swift +15 -0
  143. package/ios/Records/VideoSource.swift +25 -0
  144. package/ios/Thumbnails/VideoThumbnail.swift +27 -0
  145. package/ios/Thumbnails/VideoThumbnailGenerator.swift +68 -0
  146. package/ios/Thumbnails/VideoThumbnailOptions.swift +15 -0
  147. package/ios/VideoAsset.swift +123 -0
  148. package/ios/VideoExceptions.swift +53 -0
  149. package/ios/VideoItem.swift +11 -0
  150. package/ios/VideoManager.swift +140 -0
  151. package/ios/VideoModule.swift +383 -0
  152. package/ios/VideoPlayer/DangerousPropertiesStore.swift +19 -0
  153. package/ios/VideoPlayer.swift +435 -0
  154. package/ios/VideoPlayerAudioTracks.swift +72 -0
  155. package/ios/VideoPlayerItem.swift +97 -0
  156. package/ios/VideoPlayerObserver.swift +523 -0
  157. package/ios/VideoPlayerSubtitles.swift +71 -0
  158. package/ios/VideoSourceLoader.swift +89 -0
  159. package/ios/VideoSourceLoaderListener.swift +34 -0
  160. package/ios/VideoView.swift +224 -0
  161. package/package.json +59 -0
  162. package/plugin/build/tsconfig.tsbuildinfo +1 -0
  163. package/plugin/build/withExpoVideo.d.ts +7 -0
  164. package/plugin/build/withExpoVideo.js +38 -0
  165. package/src/NativeVideoModule.ts +20 -0
  166. package/src/NativeVideoModule.web.ts +1 -0
  167. package/src/NativeVideoView.ts +8 -0
  168. package/src/VideoModule.ts +59 -0
  169. package/src/VideoPlayer.tsx +67 -0
  170. package/src/VideoPlayer.types.ts +613 -0
  171. package/src/VideoPlayer.web.tsx +451 -0
  172. package/src/VideoPlayerEvents.types.ts +313 -0
  173. package/src/VideoThumbnail.ts +31 -0
  174. package/src/VideoView.tsx +86 -0
  175. package/src/VideoView.types.ts +165 -0
  176. package/src/VideoView.web.tsx +214 -0
  177. package/src/index.ts +46 -0
  178. package/src/resolveAssetSource.ts +2 -0
  179. package/src/resolveAssetSource.web.ts +17 -0
  180. package/src/ts-declarations/react-native-assets.d.ts +1 -0
@@ -0,0 +1,383 @@
1
+ // Copyright 2023-present 650 Industries. All rights reserved.
2
+
3
+ import ExpoModulesCore
4
+ import AVKit
5
+
6
+ public final class VideoModule: Module {
7
+ public func definition() -> ModuleDefinition {
8
+ Name("ExpoVideo")
9
+
10
+ Function("isPictureInPictureSupported") { () -> Bool in
11
+ return AVPictureInPictureController.isPictureInPictureSupported()
12
+ }
13
+
14
+ Function("getCurrentVideoCacheSize") {
15
+ VideoCacheManager.shared.getCacheDirectorySize()
16
+ }
17
+
18
+ AsyncFunction("setVideoCacheSizeAsync") { size in
19
+ try VideoCacheManager.shared.setMaxCacheSize(newSize: size)
20
+ }
21
+
22
+ AsyncFunction("clearVideoCacheAsync") {
23
+ return try await VideoCacheManager.shared.clearAllCache()
24
+ }
25
+
26
+ AsyncFunction("preCacheVideoAsync") { (urlString: String) -> Bool in
27
+ return try await VideoCacheManager.preCacheVideoAsync(from: urlString)
28
+ }
29
+ AsyncFunction("preCacheVideoPartialAsync") { (urlString: String, chunkSize: Int?) -> Bool in
30
+ return try await VideoCacheManager.preCacheVideoPartialAsync(from: urlString, chunkSize: chunkSize ?? 1_048_576)
31
+ }
32
+ AsyncFunction("isVideoCachedAsync") { (urlString: String) -> Bool in
33
+ guard let url = URL(string: urlString) else {
34
+ return false
35
+ }
36
+ let fileExtension = url.pathExtension
37
+ guard let saveFilePath = VideoAsset.pathForUrl(url: url, fileExtension: fileExtension) else {
38
+ return false
39
+ }
40
+ let mediaInfoPath = saveFilePath + VideoCacheManager.mediaInfoSuffix
41
+ guard let mediaInfo = MediaInfo(at: mediaInfoPath) else {
42
+ return false
43
+ }
44
+ let expectedLength = Int(mediaInfo.expectedContentLength)
45
+ let ranges = mediaInfo.loadedDataRanges
46
+ if ranges.count == 1, ranges[0].0 == 0, ranges[0].1 == expectedLength - 1 {
47
+ return true
48
+ }
49
+ return false
50
+ }
51
+
52
+ View(VideoView.self) {
53
+ Events(
54
+ "onPictureInPictureStart",
55
+ "onPictureInPictureStop",
56
+ "onFullscreenEnter",
57
+ "onFullscreenExit",
58
+ "onFirstFrameRender"
59
+ )
60
+
61
+ Prop("player") { (view, player: VideoPlayer?) in
62
+ view.player = player
63
+ }
64
+
65
+ Prop("nativeControls") { (view, nativeControls: Bool?) in
66
+ view.playerViewController.showsPlaybackControls = nativeControls ?? true
67
+ #if os(tvOS)
68
+ view.playerViewController.isSkipForwardEnabled = nativeControls ?? true
69
+ view.playerViewController.isSkipBackwardEnabled = nativeControls ?? true
70
+ #endif
71
+ }
72
+
73
+ Prop("contentFit") { (view, contentFit: VideoContentFit?) in
74
+ view.playerViewController.videoGravity = contentFit?.toVideoGravity() ?? .resizeAspect
75
+ }
76
+
77
+ Prop("contentPosition") { (view, contentPosition: CGVector?) in
78
+ let layer = view.playerViewController.view.layer
79
+
80
+ layer.frame = CGRect(
81
+ x: contentPosition?.dx ?? 0,
82
+ y: contentPosition?.dy ?? 0,
83
+ width: layer.frame.width,
84
+ height: layer.frame.height
85
+ )
86
+ }
87
+
88
+ Prop("allowsFullscreen") { (view, allowsFullscreen: Bool?) in
89
+ #if !os(tvOS)
90
+ view.playerViewController.setValue(allowsFullscreen ?? true, forKey: "allowsEnteringFullScreen")
91
+ #endif
92
+ }
93
+
94
+ Prop("showsTimecodes") { (view, showsTimecodes: Bool?) in
95
+ #if !os(tvOS)
96
+ view.playerViewController.showsTimecodes = showsTimecodes ?? true
97
+ #endif
98
+ }
99
+
100
+ Prop("requiresLinearPlayback") { (view, requiresLinearPlayback: Bool?) in
101
+ view.playerViewController.requiresLinearPlayback = requiresLinearPlayback ?? false
102
+ }
103
+
104
+ Prop("allowsPictureInPicture") { (view, allowsPictureInPicture: Bool?) in
105
+ view.allowPictureInPicture = allowsPictureInPicture ?? false
106
+ }
107
+
108
+ Prop("startsPictureInPictureAutomatically") { (view, startsPictureInPictureAutomatically: Bool?) in
109
+ #if !os(tvOS)
110
+ view.startPictureInPictureAutomatically = startsPictureInPictureAutomatically ?? false
111
+ #endif
112
+ }
113
+
114
+ Prop("allowsVideoFrameAnalysis") { (view, allowsVideoFrameAnalysis: Bool?) in
115
+ #if !os(tvOS)
116
+ if #available(iOS 16.0, macCatalyst 18.0, *) {
117
+ let newValue = allowsVideoFrameAnalysis ?? true
118
+
119
+ view.playerViewController.allowsVideoFrameAnalysis = newValue
120
+
121
+ // Setting the `allowsVideoFrameAnalysis` to false after the scanning was already perofrmed doesn't update the UI.
122
+ // We can force the desired behaviour by quickly toggling the property. Setting it to true clears existing requests,
123
+ // which updates the UI, hiding the button, then setting it to false before it detects any text keeps it in the desired state.
124
+ // Tested in iOS 17.5
125
+ if !newValue {
126
+ view.playerViewController.allowsVideoFrameAnalysis = true
127
+ view.playerViewController.allowsVideoFrameAnalysis = false
128
+ }
129
+ }
130
+ #endif
131
+ }
132
+
133
+ AsyncFunction("enterFullscreen") { view in
134
+ view.enterFullscreen()
135
+ }
136
+
137
+ AsyncFunction("exitFullscreen") { view in
138
+ view.exitFullscreen()
139
+ }
140
+
141
+ AsyncFunction("startPictureInPicture") { view in
142
+ try view.startPictureInPicture()
143
+ }
144
+
145
+ AsyncFunction("stopPictureInPicture") { view in
146
+ view.stopPictureInPicture()
147
+ }
148
+ }
149
+
150
+ Class(VideoPlayer.self) {
151
+ Constructor { (source: VideoSource?, useSynchronousReplace: Bool?) -> VideoPlayer in
152
+ let useSynchronousReplace = useSynchronousReplace ?? false
153
+ let player = AVPlayer()
154
+ let videoPlayer = try VideoPlayer(player, initialSource: source, useSynchronousReplace: useSynchronousReplace)
155
+ player.pause()
156
+ return videoPlayer
157
+ }
158
+
159
+ Property("playing") { player -> Bool in
160
+ return player.isPlaying
161
+ }
162
+
163
+ Property("muted") { player -> Bool in
164
+ return player.isMuted
165
+ }
166
+ .set { (player, muted: Bool) in
167
+ player.isMuted = muted
168
+ }
169
+
170
+ Property("allowsExternalPlayback") { player -> Bool in
171
+ return player.ref.allowsExternalPlayback
172
+ }
173
+ .set { (player, allowsExternalPlayback: Bool) in
174
+ player.ref.allowsExternalPlayback = allowsExternalPlayback
175
+ }
176
+
177
+ Property("staysActiveInBackground") { player -> Bool in
178
+ return player.staysActiveInBackground
179
+ }
180
+ .set { (player, staysActive: Bool) in
181
+ player.staysActiveInBackground = staysActive
182
+ }
183
+
184
+ Property("loop") { player -> Bool in
185
+ return player.loop
186
+ }
187
+ .set { (player, loop: Bool) in
188
+ player.loop = loop
189
+ }
190
+
191
+ Property("currentTime") { player -> Double in
192
+ return player.currentTime
193
+ }
194
+ .set { (player, time: Double) in
195
+ // Only clamp the lower limit, AVPlayer automatically clamps the upper limit.
196
+ player.currentTime = time
197
+ }
198
+
199
+ Property("currentLiveTimestamp") { player -> Double? in
200
+ return player.currentLiveTimestamp
201
+ }
202
+
203
+ Property("currentOffsetFromLive") { player -> Double? in
204
+ return player.currentOffsetFromLive
205
+ }
206
+
207
+ Property("targetOffsetFromLive") { player -> Double in
208
+ return player.ref.currentItem?.configuredTimeOffsetFromLive.seconds ?? 0
209
+ }
210
+ .set { (player, timeOffset: Double) in
211
+ let timeOffset = CMTime(seconds: timeOffset, preferredTimescale: .max)
212
+ player.ref.currentItem?.configuredTimeOffsetFromLive = timeOffset
213
+ }
214
+
215
+ Property("duration") { player -> Double in
216
+ let duration = player.ref.currentItem?.duration.seconds ?? 0
217
+ return duration.isNaN ? 0 : duration
218
+ }
219
+
220
+ Property("playbackRate") { player -> Float in
221
+ return player.playbackRate
222
+ }
223
+ .set { (player, playbackRate: Float) in
224
+ player.playbackRate = playbackRate
225
+ }
226
+
227
+ Property("isLive") { player -> Bool in
228
+ return player.ref.currentItem?.duration.isIndefinite ?? false
229
+ }
230
+
231
+ Property("preservesPitch") { player -> Bool in
232
+ return player.preservesPitch
233
+ }
234
+ .set { (player, preservesPitch: Bool) in
235
+ player.preservesPitch = preservesPitch
236
+ }
237
+
238
+ Property("timeUpdateEventInterval") { player -> Double in
239
+ return player.timeUpdateEventInterval
240
+ }
241
+ .set { (player, timeUpdateEventInterval: Double) in
242
+ player.timeUpdateEventInterval = timeUpdateEventInterval
243
+ }
244
+
245
+ Property("showNowPlayingNotification") { player -> Bool in
246
+ return player.showNowPlayingNotification
247
+ }
248
+ .set { (player, showNowPlayingNotification: Bool) in
249
+ player.showNowPlayingNotification = showNowPlayingNotification
250
+ }
251
+
252
+ Property("status") { player in
253
+ return player.status.rawValue
254
+ }
255
+
256
+ Property("volume") { player -> Float in
257
+ return player.volume
258
+ }
259
+ .set { (player, volume: Float) in
260
+ player.volume = volume
261
+ }
262
+
263
+ Property("bufferedPosition") { player -> Double in
264
+ return player.bufferedPosition
265
+ }
266
+
267
+ Property("bufferOptions") { player -> [String: Any] in
268
+ return player.bufferOptions.toDictionary()
269
+ }
270
+ .set { (player, bufferOptions: BufferOptions) in
271
+ player.bufferOptions = bufferOptions
272
+ }
273
+
274
+ Property("audioMixingMode") { player -> AudioMixingMode in
275
+ return player.audioMixingMode
276
+ }
277
+ .set { player, audioMixingMode in
278
+ player.audioMixingMode = audioMixingMode
279
+ }
280
+
281
+ Property("availableVideoTracks") { player -> [VideoTrack] in
282
+ return player.availableVideoTracks
283
+ }
284
+
285
+ Property("videoTrack") { player -> VideoTrack? in
286
+ return player.currentVideoTrack
287
+ }
288
+
289
+ Property("availableSubtitleTracks") { player -> [SubtitleTrack] in
290
+ return player.subtitles.availableSubtitleTracks
291
+ }
292
+
293
+ Property("subtitleTrack") { player -> SubtitleTrack? in
294
+ return player.subtitles.currentSubtitleTrack
295
+ }
296
+ .set { player, subtitleTrack in
297
+ player.subtitles.selectSubtitleTrack(subtitleTrack: subtitleTrack)
298
+ }
299
+
300
+ Property("availableAudioTracks") { player -> [AudioTrack] in
301
+ return player.audioTracks.availableAudioTracks
302
+ }
303
+
304
+ Property("audioTrack") { player -> AudioTrack? in
305
+ return player.audioTracks.currentAudioTrack
306
+ }
307
+ .set { player, audioTrack in
308
+ player.audioTracks.selectAudioTrack(audioTrack: audioTrack)
309
+ }
310
+
311
+ Function("play") { player in
312
+ player.ref.play()
313
+ }
314
+
315
+ Function("pause") { player in
316
+ player.ref.pause()
317
+ }
318
+
319
+ Function("replace") { (player, source: Either<String, VideoSource>?) in
320
+ let videoSource = parseSource(source: source)
321
+ try player.replaceCurrentItem(with: videoSource)
322
+ }
323
+
324
+ AsyncFunction("replaceAsync") { (player, source: Either<String, VideoSource>?) in
325
+ let videoSource = parseSource(source: source)
326
+ try await player.replaceCurrentItem(with: videoSource)
327
+ }
328
+
329
+ Function("seekBy") { (player, seconds: Double) in
330
+ let newTime = player.ref.currentTime() + CMTime(seconds: seconds, preferredTimescale: .max)
331
+
332
+ player.ref.seek(to: newTime)
333
+ }
334
+
335
+ Function("replay") { player in
336
+ player.ref.seek(to: CMTime.zero)
337
+ }
338
+
339
+ AsyncFunction("generateThumbnailsAsync") { (player: VideoPlayer, times: [CMTime]?, options: VideoThumbnailOptions?) -> [VideoThumbnail] in
340
+ guard let times, !times.isEmpty else {
341
+ return []
342
+ }
343
+ guard let asset = player.ref.currentItem?.asset else {
344
+ // TODO: We should throw here as nothing is playing
345
+ return []
346
+ }
347
+ return try await generateThumbnails(
348
+ asset: asset,
349
+ times: times,
350
+ options: options ?? .default
351
+ )
352
+ }
353
+ }
354
+
355
+ Class(VideoThumbnail.self) {
356
+ Property("width", \.ref.size.width)
357
+ Property("height", \.ref.size.height)
358
+ Property("requestedTime", \.requestedTime.seconds)
359
+ Property("actualTime", \.actualTime.seconds)
360
+ }
361
+
362
+ OnAppEntersBackground {
363
+ VideoManager.shared.onAppBackgrounded()
364
+ }
365
+
366
+ OnAppEntersForeground {
367
+ VideoManager.shared.onAppForegrounded()
368
+ }
369
+ }
370
+
371
+ private func parseSource(source: Either<String, VideoSource>?) -> VideoSource? {
372
+ guard let source else {
373
+ return nil
374
+ }
375
+ if source.is(String.self), let url: String = source.get() {
376
+ return VideoSource(uri: URL(string: url))
377
+ }
378
+ if source.is(VideoSource.self) {
379
+ return source.get()
380
+ }
381
+ return nil
382
+ }
383
+ }
@@ -0,0 +1,19 @@
1
+ // Constains player properties that have been set while the current item was being loaded and which can be
2
+ // correctly applied only after the player has set it's item. For now it's only currentTime, but we may add more in the future
3
+ internal class DangerousPropertiesStore {
4
+ var ownerIsReplacing: Bool = false
5
+ var currentTime: Double?
6
+
7
+ func applyProperties(to player: VideoPlayer, reset shouldReset: Bool = true) {
8
+ if let currentTime {
9
+ player.currentTime = currentTime
10
+ }
11
+ if shouldReset {
12
+ reset()
13
+ }
14
+ }
15
+
16
+ func reset() {
17
+ currentTime = nil
18
+ }
19
+ }