@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,104 @@
1
+ package expo.modules.video
2
+
3
+ import android.content.Context
4
+ import android.os.Looper
5
+ import android.util.Log
6
+ import androidx.media3.common.util.UnstableApi
7
+ import androidx.media3.database.DatabaseProvider
8
+ import androidx.media3.database.StandaloneDatabaseProvider
9
+ import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
10
+ import androidx.media3.datasource.cache.SimpleCache
11
+ import expo.modules.kotlin.exception.Exceptions
12
+ import java.io.File
13
+ import java.lang.ref.WeakReference
14
+ import java.util.UUID
15
+
16
+ private const val SHARED_PREFERENCES_NAME = "ExpoVideoCache"
17
+ private const val CACHE_SIZE_KEY = "cacheSize"
18
+ private const val VIDEO_CACHE_PARENT_DIR = "ExpoVideoCache"
19
+ private const val VIDEO_CACHE_DIR_KEY = "cacheDir"
20
+ private const val DEFAULT_CACHE_SIZE = 1024 * 1024 * 1024L // 1GB
21
+
22
+ @UnstableApi
23
+ class VideoCache(context: Context) {
24
+ // We don't want a strong reference to the context, as this class is used inside of a singleton (VideoManager)
25
+ private val weakContext = WeakReference(context)
26
+ private val context: Context
27
+ get() {
28
+ return weakContext.get() ?: throw Exceptions.ReactContextLost()
29
+ }
30
+ private val databaseProvider: DatabaseProvider = StandaloneDatabaseProvider(context)
31
+ private val sharedPreferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
32
+ private var cacheEvictor = LeastRecentlyUsedCacheEvictor(getMaxCacheSize())
33
+ var instance = SimpleCache(getCacheDir(), cacheEvictor, databaseProvider)
34
+
35
+ // Function that gets the cache size from shared preferences
36
+ private fun getMaxCacheSize(): Long {
37
+ return sharedPreferences.getLong(CACHE_SIZE_KEY, DEFAULT_CACHE_SIZE)
38
+ }
39
+
40
+ fun setMaxCacheSize(size: Long) {
41
+ assertModificationReleaseConditions()
42
+ instance.release()
43
+ sharedPreferences.edit().putLong(CACHE_SIZE_KEY, size).apply()
44
+ cacheEvictor = LeastRecentlyUsedCacheEvictor(size)
45
+ instance = SimpleCache(getCacheDir(), cacheEvictor, databaseProvider)
46
+ }
47
+
48
+ fun getCurrentCacheSize(): Long {
49
+ return getFileSize(getCacheDir())
50
+ }
51
+
52
+ // We have to keep the current directory name in the shared preferences, when the cache is released, a new name will be assigned to avoid conflicts.
53
+ private fun getCacheDir(): File {
54
+ // Weird structure, because kotlin marks the result of `getString` as nullable
55
+ val videoCacheDirName = sharedPreferences.getString(VIDEO_CACHE_DIR_KEY, null) ?: run {
56
+ val newCacheDirName = generateCacheDirName()
57
+ sharedPreferences.edit().putString(VIDEO_CACHE_DIR_KEY, newCacheDirName).apply()
58
+ newCacheDirName
59
+ }
60
+ val cacheParentDir = File(context.cacheDir, VIDEO_CACHE_PARENT_DIR)
61
+ val cacheDir = File(cacheParentDir, videoCacheDirName)
62
+
63
+ if (!cacheDir.exists()) {
64
+ cacheDir.mkdirs()
65
+ }
66
+ return cacheDir
67
+ }
68
+
69
+ private fun generateCacheDirName(): String {
70
+ return UUID.randomUUID().toString()
71
+ }
72
+
73
+ fun clear() {
74
+ assertModificationReleaseConditions()
75
+
76
+ // Creates a new cache directory to avoid conflicts while removing the old cache
77
+ val oldCacheDirectory = getCacheDir()
78
+ val oldCache = instance
79
+ val newCacheName = generateCacheDirName()
80
+
81
+ sharedPreferences.edit().putString(VIDEO_CACHE_DIR_KEY, newCacheName).apply()
82
+ instance = SimpleCache(getCacheDir(), cacheEvictor, databaseProvider)
83
+ oldCache.release()
84
+ oldCacheDirectory.deleteRecursively()
85
+ }
86
+
87
+ private fun getFileSize(file: File): Long {
88
+ return file
89
+ .walkTopDown()
90
+ .filter { it.isFile }
91
+ .map { it.length() }
92
+ .sum()
93
+ }
94
+
95
+ private fun assertModificationReleaseConditions() {
96
+ if (VideoManager.hasRegisteredPlayers()) {
97
+ throw VideoCacheException("Cannot clear cache while there are active players")
98
+ }
99
+
100
+ if (Looper.myLooper() == Looper.getMainLooper()) {
101
+ Log.w("ExpoVideo", "Clearing cache on the main thread, this might cause performance issues")
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,34 @@
1
+ package expo.modules.video
2
+
3
+ import expo.modules.kotlin.exception.CodedException
4
+ import expo.modules.video.enums.DRMType
5
+
6
+ internal class FullScreenVideoViewNotFoundException :
7
+ CodedException("VideoView id wasn't passed to the activity")
8
+
9
+ internal class VideoViewNotFoundException(id: String) :
10
+ CodedException("VideoView with id: $id not found")
11
+
12
+ internal class MethodUnsupportedException(methodName: String) :
13
+ CodedException("Method `$methodName` is not supported on Android")
14
+
15
+ internal class PictureInPictureEnterException(message: String?) :
16
+ CodedException("Failed to enter Picture in Picture mode${message?.let { ". $message" } ?: ""}")
17
+
18
+ internal class PictureInPictureConfigurationException :
19
+ CodedException("Current activity does not support picture-in-picture. Make sure you have configured the `expo-video` config plugin correctly.")
20
+
21
+ internal class PictureInPictureUnsupportedException :
22
+ CodedException("Picture in Picture mode is not supported on this device")
23
+
24
+ internal class UnsupportedDRMTypeException(type: DRMType) :
25
+ CodedException("DRM type `$type` is not supported on Android")
26
+
27
+ internal class PlaybackException(reason: String?, cause: Throwable? = null) :
28
+ CodedException("A playback exception has occurred: ${reason ?: "reason unknown"}", cause)
29
+
30
+ internal class FailedToGetAudioFocusManagerException :
31
+ CodedException("Failed to get AudioFocusManager service")
32
+
33
+ internal class VideoCacheException(message: String?, cause: Throwable? = null) :
34
+ CodedException(message ?: "Unexpected expo-video cache error", cause)
@@ -0,0 +1,133 @@
1
+ package expo.modules.video
2
+
3
+ import androidx.annotation.OptIn
4
+ import androidx.media3.common.util.UnstableApi
5
+ import expo.modules.kotlin.AppContext
6
+ import expo.modules.kotlin.exception.Exceptions
7
+ import expo.modules.video.player.VideoPlayer
8
+ import java.lang.ref.WeakReference
9
+
10
+ // Helper class used to keep track of all existing VideoViews and VideoPlayers
11
+ @OptIn(UnstableApi::class)
12
+ object VideoManager {
13
+ const val INTENT_PLAYER_KEY = "player_uuid"
14
+
15
+ // Used for sharing videoViews between VideoView and FullscreenPlayerActivity
16
+ private var videoViews = mutableMapOf<String, VideoView>()
17
+ private var fullscreenPlayerActivities = mutableMapOf<String, WeakReference<FullscreenPlayerActivity>>()
18
+
19
+ // Keeps track of all existing VideoPlayers, and whether they are attached to a VideoView
20
+ private var videoPlayersToVideoViews = mutableMapOf<VideoPlayer, MutableList<VideoView>>()
21
+
22
+ private lateinit var audioFocusManager: AudioFocusManager
23
+ lateinit var cache: VideoCache
24
+
25
+ fun onModuleCreated(appContext: AppContext) {
26
+ val context = appContext.reactContext ?: throw Exceptions.ReactContextLost()
27
+
28
+ if (!this::audioFocusManager.isInitialized) {
29
+ audioFocusManager = AudioFocusManager(appContext)
30
+ }
31
+ if (!this::cache.isInitialized) {
32
+ cache = VideoCache(context)
33
+ }
34
+ }
35
+
36
+ fun registerVideoView(videoView: VideoView) {
37
+ videoViews[videoView.videoViewId] = videoView
38
+ }
39
+
40
+ fun getVideoView(id: String): VideoView {
41
+ return videoViews[id] ?: throw VideoViewNotFoundException(id)
42
+ }
43
+
44
+ fun unregisterVideoView(videoView: VideoView) {
45
+ videoViews.remove(videoView.videoViewId)
46
+ }
47
+
48
+ fun registerVideoPlayer(videoPlayer: VideoPlayer) {
49
+ videoPlayersToVideoViews[videoPlayer] = videoPlayersToVideoViews[videoPlayer] ?: mutableListOf()
50
+ audioFocusManager.registerPlayer(videoPlayer)
51
+ }
52
+
53
+ fun unregisterVideoPlayer(videoPlayer: VideoPlayer) {
54
+ videoPlayersToVideoViews.remove(videoPlayer)
55
+ audioFocusManager.unregisterPlayer(videoPlayer)
56
+ }
57
+
58
+ fun registerFullscreenPlayerActivity(id: String, fullscreenActivity: FullscreenPlayerActivity) {
59
+ fullscreenPlayerActivities[id] = WeakReference(fullscreenActivity)
60
+ }
61
+
62
+ fun unregisterFullscreenPlayerActivity(id: String) {
63
+ fullscreenPlayerActivities.remove(id)
64
+ }
65
+
66
+ fun onVideoPlayerAttachedToView(videoPlayer: VideoPlayer, videoView: VideoView) {
67
+ if (videoPlayersToVideoViews[videoPlayer]?.contains(videoView) == true) {
68
+ return
69
+ }
70
+ videoPlayersToVideoViews[videoPlayer]?.add(videoView) ?: run {
71
+ videoPlayersToVideoViews[videoPlayer] = arrayListOf(videoView)
72
+ }
73
+
74
+ if (videoPlayersToVideoViews[videoPlayer]?.size == 1) {
75
+ videoPlayer.serviceConnection.playbackServiceBinder?.service?.registerPlayer(videoPlayer)
76
+ }
77
+ }
78
+
79
+ fun onVideoPlayerDetachedFromView(videoPlayer: VideoPlayer, videoView: VideoView) {
80
+ videoPlayersToVideoViews[videoPlayer]?.remove(videoView)
81
+
82
+ // Unregister disconnected VideoPlayers from the playback service
83
+ if (videoPlayersToVideoViews[videoPlayer] == null || videoPlayersToVideoViews[videoPlayer]?.size == 0) {
84
+ videoPlayer.serviceConnection.playbackServiceBinder?.service?.unregisterPlayer(videoPlayer.player)
85
+ }
86
+ }
87
+
88
+ fun isVideoPlayerAttachedToView(videoPlayer: VideoPlayer): Boolean {
89
+ return videoPlayersToVideoViews[videoPlayer]?.isNotEmpty() ?: false
90
+ }
91
+
92
+ fun hasRegisteredPlayers(): Boolean {
93
+ return videoPlayersToVideoViews.isNotEmpty()
94
+ }
95
+
96
+ fun onAppForegrounded() {
97
+ for (videoView in videoViews.values) {
98
+ videoView.playerView.useController = videoView.useNativeControls
99
+ }
100
+
101
+ // Pressing the app icon will bring up the mainActivity instead of the fullscreen activity (at least for BareExpo)
102
+ // In this case we have to manually finish the fullscreen activity
103
+ for (fullscreenActivity in fullscreenPlayerActivities.values) {
104
+ fullscreenActivity.get()?.finish()
105
+ }
106
+ }
107
+
108
+ fun onAppBackgrounded() {
109
+ for (videoView in videoViews.values) {
110
+ if (shouldPauseVideo(videoView)) {
111
+ handleVideoPause(videoView)
112
+ } else {
113
+ videoView.wasAutoPaused = false
114
+ }
115
+ }
116
+ }
117
+
118
+ private fun shouldPauseVideo(videoView: VideoView): Boolean {
119
+ return videoView.videoPlayer?.staysActiveInBackground == false &&
120
+ !videoView.willEnterPiP &&
121
+ !videoView.isInFullscreen
122
+ }
123
+
124
+ private fun handleVideoPause(videoView: VideoView) {
125
+ videoView.playerView.useController = false
126
+ videoView.videoPlayer?.player?.let { player ->
127
+ if (player.isPlaying) {
128
+ player.pause()
129
+ videoView.wasAutoPaused = true
130
+ }
131
+ }
132
+ }
133
+ }
@@ -0,0 +1,414 @@
1
+ @file:OptIn(EitherType::class)
2
+
3
+ package expo.modules.video
4
+
5
+ import android.net.Uri
6
+ import androidx.media3.common.PlaybackParameters
7
+ import androidx.media3.common.Player.REPEAT_MODE_OFF
8
+ import androidx.media3.common.Player.REPEAT_MODE_ONE
9
+ import androidx.media3.common.util.UnstableApi
10
+ import com.facebook.react.common.annotations.UnstableReactNativeAPI
11
+ import expo.modules.kotlin.Promise
12
+ import expo.modules.kotlin.apifeatures.EitherType
13
+ import expo.modules.kotlin.functions.Coroutine
14
+ import expo.modules.kotlin.functions.Queues
15
+ import expo.modules.kotlin.modules.Module
16
+ import expo.modules.kotlin.modules.ModuleDefinition
17
+ import expo.modules.kotlin.types.Either
18
+ import expo.modules.kotlin.views.ViewDefinitionBuilder
19
+ import expo.modules.video.enums.AudioMixingMode
20
+ import expo.modules.video.enums.ContentFit
21
+ import expo.modules.video.player.VideoPlayer
22
+ import expo.modules.video.records.BufferOptions
23
+ import expo.modules.video.records.SubtitleTrack
24
+ import expo.modules.video.records.AudioTrack
25
+ import expo.modules.video.records.VideoSource
26
+ import expo.modules.video.records.VideoThumbnailOptions
27
+ import expo.modules.video.utils.runWithPiPMisconfigurationSoftHandling
28
+ import kotlinx.coroutines.async
29
+ import kotlinx.coroutines.awaitAll
30
+ import kotlinx.coroutines.launch
31
+ import kotlinx.coroutines.runBlocking
32
+ import kotlin.time.Duration
33
+
34
+ // https://developer.android.com/guide/topics/media/media3/getting-started/migration-guide#improvements_in_media3
35
+ @UnstableReactNativeAPI
36
+ @androidx.annotation.OptIn(UnstableApi::class)
37
+ class VideoModule : Module() {
38
+ override fun definition() = ModuleDefinition {
39
+ Name("ExpoVideo")
40
+
41
+ OnCreate {
42
+ VideoManager.onModuleCreated(appContext)
43
+ }
44
+
45
+ Function("isPictureInPictureSupported") {
46
+ VideoView.isPictureInPictureSupported(appContext.throwingActivity)
47
+ }
48
+
49
+ Function("getCurrentVideoCacheSize") {
50
+ VideoManager.cache.getCurrentCacheSize()
51
+ }
52
+
53
+ AsyncFunction("setVideoCacheSizeAsync") { size: Long ->
54
+ VideoManager.cache.setMaxCacheSize(size)
55
+ }
56
+
57
+ AsyncFunction("clearVideoCacheAsync") {
58
+ VideoManager.cache.clear()
59
+ }
60
+
61
+ View(SurfaceVideoView::class) {
62
+ VideoViewComponent<SurfaceVideoView>()
63
+ }
64
+ View(TextureVideoView::class) {
65
+ VideoViewComponent<TextureVideoView>()
66
+ }
67
+
68
+ Class(VideoPlayer::class) {
69
+ Constructor { source: VideoSource? ->
70
+ val player = VideoPlayer(appContext.throwingActivity.applicationContext, appContext, source)
71
+ appContext.mainQueue.launch {
72
+ player.prepare()
73
+ }
74
+ return@Constructor player
75
+ }
76
+
77
+ Property("playing")
78
+ .get { ref: VideoPlayer ->
79
+ ref.playing
80
+ }
81
+
82
+ Property("muted")
83
+ .get { ref: VideoPlayer ->
84
+ ref.muted
85
+ }
86
+ .set { ref: VideoPlayer, muted: Boolean ->
87
+ appContext.mainQueue.launch {
88
+ ref.muted = muted
89
+ }
90
+ }
91
+
92
+ Property("volume")
93
+ .get { ref: VideoPlayer ->
94
+ ref.volume
95
+ }
96
+ .set { ref: VideoPlayer, volume: Float ->
97
+ appContext.mainQueue.launch {
98
+ ref.userVolume = volume
99
+ ref.volume = volume
100
+ }
101
+ }
102
+
103
+ Property("currentTime")
104
+ .get { ref: VideoPlayer ->
105
+ // TODO: we shouldn't block the thread, but there are no events for the player position change,
106
+ // so we can't update the currentTime in a non-blocking way like the other properties.
107
+ // Until we think of something better we can temporarily do it this way
108
+ runBlocking(appContext.mainQueue.coroutineContext) {
109
+ ref.player.currentPosition / 1000f
110
+ }
111
+ }
112
+ .set { ref: VideoPlayer, currentTime: Double ->
113
+ appContext.mainQueue.launch {
114
+ ref.player.seekTo((currentTime * 1000).toLong())
115
+ }
116
+ }
117
+
118
+ Property("currentLiveTimestamp")
119
+ .get { ref: VideoPlayer ->
120
+ runBlocking(appContext.mainQueue.coroutineContext) {
121
+ ref.currentLiveTimestamp
122
+ }
123
+ }
124
+
125
+ Property("availableVideoTracks")
126
+ .get { ref: VideoPlayer ->
127
+ ref.availableVideoTracks
128
+ }
129
+
130
+ Property("videoTrack")
131
+ .get { ref: VideoPlayer ->
132
+ ref.currentVideoTrack
133
+ }
134
+
135
+ Property("availableSubtitleTracks")
136
+ .get { ref: VideoPlayer ->
137
+ ref.subtitles.availableSubtitleTracks
138
+ }
139
+
140
+ Property("subtitleTrack")
141
+ .get { ref: VideoPlayer ->
142
+ ref.subtitles.currentSubtitleTrack
143
+ }
144
+ .set { ref: VideoPlayer, subtitleTrack: SubtitleTrack? ->
145
+ appContext.mainQueue.launch {
146
+ ref.subtitles.currentSubtitleTrack = subtitleTrack
147
+ }
148
+ }
149
+
150
+ Property("availableAudioTracks")
151
+ .get { ref: VideoPlayer ->
152
+ ref.audioTracks.availableAudioTracks
153
+ }
154
+
155
+ Property("audioTrack")
156
+ .get { ref: VideoPlayer ->
157
+ ref.audioTracks.currentAudioTrack
158
+ }
159
+ .set { ref: VideoPlayer, audioTrack: AudioTrack? ->
160
+ appContext.mainQueue.launch {
161
+ ref.audioTracks.currentAudioTrack = audioTrack
162
+ }
163
+ }
164
+
165
+ Property("currentOffsetFromLive")
166
+ .get { ref: VideoPlayer ->
167
+ runBlocking(appContext.mainQueue.coroutineContext) {
168
+ ref.currentOffsetFromLive
169
+ }
170
+ }
171
+
172
+ Property("duration")
173
+ .get { ref: VideoPlayer ->
174
+ ref.duration
175
+ }
176
+
177
+ Property("playbackRate")
178
+ .get { ref: VideoPlayer ->
179
+ ref.playbackParameters.speed
180
+ }
181
+ .set { ref: VideoPlayer, playbackRate: Float ->
182
+ appContext.mainQueue.launch {
183
+ val pitch = if (ref.preservesPitch) 1f else playbackRate
184
+ ref.playbackParameters = PlaybackParameters(playbackRate, pitch)
185
+ }
186
+ }
187
+
188
+ Property("isLive")
189
+ .get { ref: VideoPlayer ->
190
+ ref.isLive
191
+ }
192
+
193
+ Property("preservesPitch")
194
+ .get { ref: VideoPlayer ->
195
+ ref.preservesPitch
196
+ }
197
+ .set { ref: VideoPlayer, preservesPitch: Boolean ->
198
+ appContext.mainQueue.launch {
199
+ ref.preservesPitch = preservesPitch
200
+ }
201
+ }
202
+
203
+ Property("showNowPlayingNotification")
204
+ .get { ref: VideoPlayer ->
205
+ ref.showNowPlayingNotification
206
+ }
207
+ .set { ref: VideoPlayer, showNotification: Boolean ->
208
+ appContext.mainQueue.launch {
209
+ ref.showNowPlayingNotification = showNotification
210
+ }
211
+ }
212
+
213
+ Property("status")
214
+ .get { ref: VideoPlayer ->
215
+ ref.status
216
+ }
217
+
218
+ Property("staysActiveInBackground")
219
+ .get { ref: VideoPlayer ->
220
+ ref.staysActiveInBackground
221
+ }
222
+ .set { ref: VideoPlayer, staysActive: Boolean ->
223
+ ref.staysActiveInBackground = staysActive
224
+ }
225
+
226
+ Property("loop")
227
+ .get { ref: VideoPlayer ->
228
+ ref.player.repeatMode == REPEAT_MODE_ONE
229
+ }
230
+ .set { ref: VideoPlayer, loop: Boolean ->
231
+ appContext.mainQueue.launch {
232
+ ref.player.repeatMode = if (loop) {
233
+ REPEAT_MODE_ONE
234
+ } else {
235
+ REPEAT_MODE_OFF
236
+ }
237
+ }
238
+ }
239
+
240
+ Property("bufferedPosition")
241
+ .get { ref: VideoPlayer ->
242
+ // Same as currentTime
243
+ runBlocking(appContext.mainQueue.coroutineContext) {
244
+ ref.bufferedPosition
245
+ }
246
+ }
247
+
248
+ Property("bufferOptions")
249
+ .get { ref: VideoPlayer ->
250
+ ref.bufferOptions
251
+ }
252
+ .set { ref: VideoPlayer, bufferOptions: BufferOptions ->
253
+ ref.bufferOptions = bufferOptions
254
+ }
255
+
256
+ Function("play") { ref: VideoPlayer ->
257
+ appContext.mainQueue.launch {
258
+ ref.player.play()
259
+ }
260
+ }
261
+
262
+ Function("pause") { ref: VideoPlayer ->
263
+ appContext.mainQueue.launch {
264
+ ref.player.pause()
265
+ }
266
+ }
267
+
268
+ Property("timeUpdateEventInterval")
269
+ .get { ref: VideoPlayer ->
270
+ ref.intervalUpdateClock.interval / 1000.0
271
+ }
272
+ .set { ref: VideoPlayer, intervalSeconds: Float ->
273
+ ref.intervalUpdateClock.interval = (intervalSeconds * 1000).toLong()
274
+ }
275
+
276
+ Property("audioMixingMode")
277
+ .get { ref: VideoPlayer ->
278
+ ref.audioMixingMode
279
+ }
280
+ .set { ref: VideoPlayer, audioMixingMode: AudioMixingMode ->
281
+ appContext.mainQueue.launch {
282
+ ref.audioMixingMode = audioMixingMode
283
+ }
284
+ }
285
+
286
+ Function("replace") { ref: VideoPlayer, source: Either<Uri, VideoSource>? ->
287
+ replaceImpl(ref, source)
288
+ }
289
+
290
+ // ExoPlayer automatically offloads loading of the asset onto a different thread so we can keep the same
291
+ // implementation until `replace` is deprecated and removed.
292
+ // TODO: @behenate see if we can further reduce load on the main thread
293
+ AsyncFunction("replaceAsync") { ref: VideoPlayer, source: Either<Uri, VideoSource>?, promise: Promise ->
294
+ replaceImpl(ref, source, promise)
295
+ }
296
+
297
+ Function("seekBy") { ref: VideoPlayer, seekTime: Double ->
298
+ appContext.mainQueue.launch {
299
+ val seekPos = ref.player.currentPosition + (seekTime * 1000).toLong()
300
+ ref.player.seekTo(seekPos)
301
+ }
302
+ }
303
+
304
+ Function("replay") { ref: VideoPlayer ->
305
+ appContext.mainQueue.launch {
306
+ ref.player.seekTo(0)
307
+ ref.player.play()
308
+ }
309
+ }
310
+
311
+ AsyncFunction("generateThumbnailsAsync") Coroutine { ref: VideoPlayer, times: List<Duration>, options: VideoThumbnailOptions? ->
312
+ return@Coroutine ref.toMetadataRetriever().safeUse {
313
+ val bitmaps = times.map { time ->
314
+ appContext.backgroundCoroutineScope.async {
315
+ generateThumbnailAtTime(time, options)
316
+ }
317
+ }
318
+
319
+ bitmaps.awaitAll()
320
+ }
321
+ }
322
+
323
+ Class<VideoThumbnail> {
324
+ Property("width") { ref -> ref.width }
325
+ Property("height") { ref -> ref.height }
326
+ Property("requestedTime") { ref -> ref.requestedTime }
327
+ Property("actualTime") { ref -> ref.actualTime }
328
+ }
329
+ }
330
+
331
+ OnActivityEntersForeground {
332
+ VideoManager.onAppForegrounded()
333
+ }
334
+
335
+ OnActivityEntersBackground {
336
+ VideoManager.onAppBackgrounded()
337
+ }
338
+ }
339
+ private fun replaceImpl(
340
+ ref: VideoPlayer,
341
+ source: Either<Uri, VideoSource>?,
342
+ promise: Promise? = null
343
+ ) {
344
+ val videoSource = source?.let {
345
+ if (it.`is`(VideoSource::class)) {
346
+ it.get(VideoSource::class)
347
+ } else {
348
+ VideoSource(it.get(Uri::class))
349
+ }
350
+ }
351
+
352
+ appContext.mainQueue.launch {
353
+ ref.uncommittedSource = videoSource
354
+ ref.prepare()
355
+ promise?.resolve()
356
+ }
357
+ }
358
+ }
359
+
360
+ @androidx.annotation.OptIn(UnstableApi::class)
361
+ private inline fun <reified T : VideoView> ViewDefinitionBuilder<T>.VideoViewComponent() {
362
+ Events(
363
+ "onPictureInPictureStart",
364
+ "onPictureInPictureStop",
365
+ "onFullscreenEnter",
366
+ "onFullscreenExit",
367
+ "onFirstFrameRender"
368
+ )
369
+ Prop("player") { view: T, player: VideoPlayer ->
370
+ view.videoPlayer = player
371
+ }
372
+ Prop("nativeControls") { view: T, useNativeControls: Boolean ->
373
+ view.useNativeControls = useNativeControls
374
+ }
375
+ Prop("contentFit") { view: T, contentFit: ContentFit ->
376
+ view.contentFit = contentFit
377
+ }
378
+ Prop("startsPictureInPictureAutomatically") { view: T, autoEnterPiP: Boolean ->
379
+ view.autoEnterPiP = autoEnterPiP
380
+ }
381
+ Prop("allowsFullscreen") { view: T, allowsFullscreen: Boolean? ->
382
+ view.allowsFullscreen = allowsFullscreen ?: true
383
+ }
384
+ Prop("requiresLinearPlayback") { view: T, requiresLinearPlayback: Boolean? ->
385
+ val linearPlayback = requiresLinearPlayback ?: false
386
+ view.playerView.applyRequiresLinearPlayback(linearPlayback)
387
+ view.videoPlayer?.requiresLinearPlayback = linearPlayback
388
+ }
389
+ Prop("useExoShutter") { view: T, useExoShutter: Boolean? ->
390
+ view.useExoShutter = useExoShutter
391
+ }
392
+ AsyncFunction("enterFullscreen") { view: T ->
393
+ view.enterFullscreen()
394
+ }.runOnQueue(Queues.MAIN)
395
+ AsyncFunction("exitFullscreen") {
396
+ throw MethodUnsupportedException("exitFullscreen")
397
+ }
398
+ AsyncFunction("startPictureInPicture") { view: T ->
399
+ runWithPiPMisconfigurationSoftHandling(true) {
400
+ view.enterPictureInPicture()
401
+ }
402
+ }
403
+ AsyncFunction("stopPictureInPicture") {
404
+ throw MethodUnsupportedException("stopPictureInPicture")
405
+ }
406
+ OnViewDestroys {
407
+ VideoManager.unregisterVideoView(it)
408
+ }
409
+ OnViewDidUpdateProps { view ->
410
+ if (view.playerView.useController != view.useNativeControls) {
411
+ view.playerView.useController = view.useNativeControls
412
+ }
413
+ }
414
+ }
@@ -0,0 +1,20 @@
1
+ package expo.modules.video
2
+
3
+ import android.graphics.Bitmap
4
+ import expo.modules.kotlin.sharedobjects.SharedRef
5
+ import kotlin.time.Duration
6
+
7
+ class VideoThumbnail(
8
+ ref: Bitmap,
9
+ val requestedTime: Duration,
10
+ val actualTime: Duration
11
+ ) : SharedRef<Bitmap>(ref) {
12
+ val width = ref.width
13
+ val height = ref.height
14
+
15
+ override val nativeRefType: String = "image"
16
+
17
+ override fun getAdditionalMemoryPressure(): Int {
18
+ return ref.byteCount
19
+ }
20
+ }