@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,367 @@
1
+ package expo.modules.video
2
+
3
+ import android.app.Activity
4
+ import android.app.PictureInPictureParams
5
+ import android.content.Context
6
+ import android.content.Intent
7
+ import android.graphics.Color
8
+ import android.os.Build
9
+ import android.util.Rational
10
+ import android.view.LayoutInflater
11
+ import android.view.MotionEvent
12
+ import android.view.View
13
+ import android.view.ViewGroup
14
+ import android.widget.FrameLayout
15
+ import android.widget.ImageButton
16
+ import androidx.fragment.app.FragmentActivity
17
+ import androidx.media3.common.Tracks
18
+ import androidx.media3.common.VideoSize
19
+ import androidx.media3.ui.PlayerView
20
+ import com.facebook.react.bridge.ReactContext
21
+ import com.facebook.react.uimanager.UIManagerHelper
22
+ import com.facebook.react.uimanager.events.EventDispatcher
23
+ import com.facebook.react.uimanager.events.TouchEventCoalescingKeyHelper
24
+ import expo.modules.kotlin.AppContext
25
+ import expo.modules.kotlin.viewevent.EventDispatcher
26
+ import expo.modules.kotlin.views.ExpoView
27
+ import expo.modules.video.delegates.IgnoreSameSet
28
+ import expo.modules.video.enums.ContentFit
29
+ import expo.modules.video.player.VideoPlayer
30
+ import expo.modules.video.player.VideoPlayerListener
31
+ import expo.modules.video.records.AudioTrack
32
+ import expo.modules.video.records.SubtitleTrack
33
+ import expo.modules.video.records.VideoSource
34
+ import expo.modules.video.records.VideoTrack
35
+ import expo.modules.video.utils.applyPiPParams
36
+ import expo.modules.video.utils.applyRectHint
37
+ import expo.modules.video.utils.calculatePiPAspectRatio
38
+ import expo.modules.video.utils.calculateRectHint
39
+ import expo.modules.video.utils.dispatchMotionEvent
40
+ import java.util.UUID
41
+
42
+ class SurfaceVideoView(context: Context, appContext: AppContext) : VideoView(context, appContext)
43
+ class TextureVideoView(context: Context, appContext: AppContext) : VideoView(context, appContext, true)
44
+
45
+ @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
46
+ open class VideoView(context: Context, appContext: AppContext, useTextureView: Boolean = false) : ExpoView(context, appContext), VideoPlayerListener {
47
+ val videoViewId: String = UUID.randomUUID().toString()
48
+ val playerView: PlayerView = LayoutInflater.from(context.applicationContext).inflate(getPlayerViewLayoutId(useTextureView), null) as PlayerView
49
+ val onPictureInPictureStart by EventDispatcher<Unit>()
50
+ val onPictureInPictureStop by EventDispatcher<Unit>()
51
+ val onFullscreenEnter by EventDispatcher<Unit>()
52
+ val onFullscreenExit by EventDispatcher<Unit>()
53
+ val onFirstFrameRender by EventDispatcher<Unit>()
54
+
55
+ var willEnterPiP: Boolean = false
56
+
57
+ // In some situations we can't detect if the view will enter PiP, in that case the playback will be paused
58
+ // We can get an event after PiP has started, that's when we should resume playback
59
+ var wasAutoPaused: Boolean = false
60
+ var isInFullscreen: Boolean = false
61
+ private set
62
+ var showsSubtitlesButton = false
63
+ private set
64
+ var showsAudioTracksButton = false
65
+ private set
66
+
67
+ private val currentActivity = appContext.throwingActivity
68
+ private val decorView = currentActivity.window.decorView
69
+ private val rootView = decorView.findViewById<ViewGroup>(android.R.id.content)
70
+ private val touchEventCoalescingKeyHelper = TouchEventCoalescingKeyHelper()
71
+
72
+ private val rootViewChildrenOriginalVisibility: ArrayList<Int> = arrayListOf()
73
+ private var pictureInPictureHelperTag: String? = null
74
+ private var reactNativeEventDispatcher: EventDispatcher? = null
75
+
76
+ // We need to keep track of the target surface view visibility, but only apply it when `useExoShutter` is false.
77
+ var shouldHideSurfaceView: Boolean = true
78
+
79
+ var useExoShutter: Boolean? = null
80
+ set(value) {
81
+ if (value == true) {
82
+ playerView.setShutterBackgroundColor(Color.BLACK)
83
+ } else {
84
+ playerView.setShutterBackgroundColor(Color.TRANSPARENT)
85
+ }
86
+ applySurfaceViewVisibility()
87
+ field = value
88
+ }
89
+
90
+ var autoEnterPiP: Boolean by IgnoreSameSet(false) { new, _ ->
91
+ applyPiPParams(currentActivity, new, calculateCurrentPipAspectRatio())
92
+ }
93
+
94
+ var contentFit: ContentFit = ContentFit.CONTAIN
95
+ set(value) {
96
+ playerView.resizeMode = value.toResizeMode()
97
+ field = value
98
+ }
99
+
100
+ var videoPlayer: VideoPlayer? = null
101
+ set(newPlayer) {
102
+ field?.let {
103
+ VideoManager.onVideoPlayerDetachedFromView(it, this)
104
+ }
105
+ videoPlayer?.removeListener(this)
106
+ newPlayer?.addListener(this)
107
+ field = newPlayer
108
+ shouldHideSurfaceView = true
109
+ attachPlayer()
110
+ newPlayer?.let {
111
+ VideoManager.onVideoPlayerAttachedToView(it, this)
112
+ }
113
+ }
114
+
115
+ var useNativeControls: Boolean = true
116
+ set(value) {
117
+ playerView.useController = value
118
+ playerView.setShowSubtitleButton(value)
119
+ field = value
120
+ }
121
+
122
+ var allowsFullscreen: Boolean = true
123
+ set(value) {
124
+ if (value) {
125
+ playerView.setFullscreenButtonClickListener { enterFullscreen() }
126
+ } else {
127
+ playerView.setFullscreenButtonClickListener(null)
128
+ // Setting listener to null should hide the button, but judging by ExoPlayer source code
129
+ // there is a bug and the button isn't hidden. We need to do it manually.
130
+ playerView.setFullscreenButtonVisibility(false)
131
+ }
132
+ field = value
133
+ }
134
+
135
+ private val mLayoutRunnable = Runnable {
136
+ measure(
137
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
138
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
139
+ )
140
+ layout(left, top, right, bottom)
141
+ }
142
+
143
+ init {
144
+ VideoManager.registerVideoView(this)
145
+ playerView.setFullscreenButtonClickListener { enterFullscreen() }
146
+ // The prop `useNativeControls` prop is sometimes applied after the view is created, and sometimes there is a visible
147
+ // flash of controls event when they are set to off. Initially we set it to `false` and apply it in `onAttachedToWindow` to avoid this.
148
+ this.playerView.useController = false
149
+
150
+ // Start with the SurfaceView being transparent to avoid any flickers when the prop value is delivered.
151
+ this.playerView.setShutterBackgroundColor(Color.TRANSPARENT)
152
+ this.playerView.videoSurfaceView?.alpha = 0f
153
+ addView(
154
+ playerView,
155
+ ViewGroup.LayoutParams(
156
+ ViewGroup.LayoutParams.MATCH_PARENT,
157
+ ViewGroup.LayoutParams.MATCH_PARENT
158
+ )
159
+ )
160
+
161
+ reactNativeEventDispatcher = UIManagerHelper.getEventDispatcher(appContext.reactContext as ReactContext, id)
162
+ }
163
+
164
+ fun applySurfaceViewVisibility() {
165
+ if (useExoShutter != true && shouldHideSurfaceView) {
166
+ playerView.videoSurfaceView?.alpha = 0f
167
+ } else {
168
+ playerView.videoSurfaceView?.alpha = 1f
169
+ }
170
+ }
171
+
172
+ fun enterFullscreen() {
173
+ val intent = Intent(context, FullscreenPlayerActivity::class.java)
174
+ intent.putExtra(VideoManager.INTENT_PLAYER_KEY, videoViewId)
175
+ // Set before starting the activity to avoid entering PiP unintentionally
176
+ isInFullscreen = true
177
+ currentActivity.startActivity(intent)
178
+
179
+ // Disable the enter transition
180
+ if (Build.VERSION.SDK_INT >= 34) {
181
+ currentActivity.overrideActivityTransition(Activity.OVERRIDE_TRANSITION_OPEN, 0, 0)
182
+ } else {
183
+ @Suppress("DEPRECATION")
184
+ currentActivity.overridePendingTransition(0, 0)
185
+ }
186
+ onFullscreenEnter(Unit)
187
+ applyPiPParams(currentActivity, false, calculateCurrentPipAspectRatio())
188
+ }
189
+
190
+ fun attachPlayer() {
191
+ videoPlayer?.changePlayerView(playerView)
192
+ }
193
+
194
+ fun exitFullscreen() {
195
+ // Fullscreen uses a different PlayerView instance, because of that we need to manually update the non-fullscreen player icon after exiting
196
+ val fullScreenButton: ImageButton = playerView.findViewById(androidx.media3.ui.R.id.exo_fullscreen)
197
+ fullScreenButton.setImageResource(androidx.media3.ui.R.drawable.exo_icon_fullscreen_enter)
198
+ attachPlayer()
199
+ onFullscreenExit(Unit)
200
+ isInFullscreen = false
201
+ applyPiPParams(currentActivity, autoEnterPiP, calculateCurrentPipAspectRatio())
202
+ }
203
+
204
+ fun enterPictureInPicture() {
205
+ if (!isPictureInPictureSupported(currentActivity)) {
206
+ throw PictureInPictureUnsupportedException()
207
+ }
208
+
209
+ val player = playerView.player
210
+ ?: throw PictureInPictureEnterException("No player attached to the VideoView")
211
+ playerView.useController = false
212
+ applyPiPParams(currentActivity, autoEnterPiP, calculateCurrentPipAspectRatio())
213
+ willEnterPiP = true
214
+
215
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
216
+ currentActivity.enterPictureInPictureMode(PictureInPictureParams.Builder().build())
217
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
218
+ @Suppress("DEPRECATION")
219
+ currentActivity.enterPictureInPictureMode()
220
+ }
221
+ }
222
+
223
+ private fun calculateCurrentPipAspectRatio(): Rational? {
224
+ val player = videoPlayer?.player ?: return null
225
+ return calculatePiPAspectRatio(player.videoSize, this.width, this.height, contentFit)
226
+ }
227
+
228
+ /**
229
+ * For optimal picture in picture experience it's best to only have one view. This method
230
+ * hides all children of the root view and makes the player the only visible child of the rootView.
231
+ */
232
+ fun layoutForPiPEnter() {
233
+ playerView.useController = false
234
+ (playerView.parent as? ViewGroup)?.removeView(playerView)
235
+ for (i in 0 until rootView.childCount) {
236
+ if (rootView.getChildAt(i) != playerView) {
237
+ rootViewChildrenOriginalVisibility.add(rootView.getChildAt(i).visibility)
238
+ rootView.getChildAt(i).visibility = View.GONE
239
+ }
240
+ }
241
+ rootView.addView(playerView, FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
242
+ }
243
+
244
+ fun layoutForPiPExit() {
245
+ playerView.useController = useNativeControls
246
+ rootView.removeView(playerView)
247
+ for (i in 0 until rootView.childCount) {
248
+ rootView.getChildAt(i).visibility = rootViewChildrenOriginalVisibility[i]
249
+ }
250
+ rootViewChildrenOriginalVisibility.clear()
251
+ this.addView(playerView)
252
+ }
253
+
254
+ override fun onVideoSourceLoaded(
255
+ player: VideoPlayer,
256
+ videoSource: VideoSource?,
257
+ duration: Double?,
258
+ availableVideoTracks: List<VideoTrack>,
259
+ availableSubtitleTracks: List<SubtitleTrack>,
260
+ availableAudioTracks: List<AudioTrack>
261
+ ) {
262
+ availableVideoTracks.firstOrNull()?.let {
263
+ val videoSize = VideoSize(it.size.width, it.size.height)
264
+ val aspectRatio = calculatePiPAspectRatio(videoSize, this.width, this.height, contentFit)
265
+ applyPiPParams(currentActivity, autoEnterPiP, aspectRatio)
266
+ }
267
+ super.onVideoSourceLoaded(player, videoSource, duration, availableVideoTracks, availableSubtitleTracks, availableAudioTracks)
268
+ }
269
+
270
+ override fun onTracksChanged(player: VideoPlayer, tracks: Tracks) {
271
+ showsSubtitlesButton = player.subtitles.availableSubtitleTracks.isNotEmpty()
272
+ showsAudioTracksButton = player.audioTracks.availableAudioTracks.size > 1
273
+ playerView.setShowSubtitleButton(showsSubtitlesButton)
274
+ super.onTracksChanged(player, tracks)
275
+ }
276
+
277
+ override fun onRenderedFirstFrame(player: VideoPlayer) {
278
+ shouldHideSurfaceView = false
279
+ applySurfaceViewVisibility()
280
+ onFirstFrameRender(Unit)
281
+ }
282
+
283
+ override fun requestLayout() {
284
+ super.requestLayout()
285
+
286
+ // Code borrowed from:
287
+ // https://github.com/facebook/react-native/blob/d19afc73f5048f81656d0b4424232ce6d69a6368/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/ReactToolbar.java#L166
288
+ // This fixes some layout issues with the exoplayer which caused the resizeMode to not work properly
289
+ post(mLayoutRunnable)
290
+ }
291
+
292
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
293
+ super.onLayout(changed, l, t, r, b)
294
+ // On every re-layout ExoPlayer resets the timeBar to be enabled.
295
+ // We need to disable it to keep scrubbing impossible.
296
+ playerView.setTimeBarInteractive(videoPlayer?.requiresLinearPlayback ?: true)
297
+ applyRectHint(currentActivity, calculateRectHint(playerView))
298
+ }
299
+
300
+ override fun onAttachedToWindow() {
301
+ super.onAttachedToWindow()
302
+ (currentActivity as? FragmentActivity)?.let {
303
+ val fragment = PictureInPictureHelperFragment(this)
304
+ pictureInPictureHelperTag = fragment.id
305
+ it.supportFragmentManager.beginTransaction()
306
+ .add(fragment, fragment.id)
307
+ .commitAllowingStateLoss()
308
+ }
309
+ applyPiPParams(currentActivity, autoEnterPiP)
310
+ }
311
+
312
+ override fun onDetachedFromWindow() {
313
+ super.onDetachedFromWindow()
314
+ (currentActivity as? FragmentActivity)?.let {
315
+ val fragment = it.supportFragmentManager.findFragmentByTag(pictureInPictureHelperTag ?: "")
316
+ ?: return
317
+ it.supportFragmentManager.beginTransaction()
318
+ .remove(fragment)
319
+ .commitAllowingStateLoss()
320
+ }
321
+ applyPiPParams(currentActivity, false)
322
+ }
323
+
324
+ // After adding the `PlayerView` to the hierarchy the touch events stop being emitted to the JS side.
325
+ // The only workaround I have found is to dispatch the touch events manually using the `EventDispatcher`.
326
+ // The behavior is different when the native controls are enabled and disabled.
327
+ override fun onTouchEvent(event: MotionEvent?): Boolean {
328
+ if (!useNativeControls) {
329
+ event?.eventTime?.let {
330
+ touchEventCoalescingKeyHelper.addCoalescingKey(it)
331
+ reactNativeEventDispatcher?.dispatchMotionEvent(this@VideoView, event, touchEventCoalescingKeyHelper)
332
+ }
333
+ }
334
+ if (event?.actionMasked == MotionEvent.ACTION_UP) {
335
+ performClick()
336
+ }
337
+ // Mark the event as handled
338
+ return true
339
+ }
340
+
341
+ override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
342
+ if (useNativeControls) {
343
+ event?.eventTime?.let {
344
+ touchEventCoalescingKeyHelper.addCoalescingKey(it)
345
+ reactNativeEventDispatcher?.dispatchMotionEvent(this@VideoView, MotionEvent.obtainNoHistory(event), touchEventCoalescingKeyHelper)
346
+ }
347
+ }
348
+ // Return false to receive all other events before the target `onTouchEvent`
349
+ return false
350
+ }
351
+
352
+ private fun getPlayerViewLayoutId(useTextureView: Boolean): Int {
353
+ return if (useTextureView) {
354
+ R.layout.texture_player_view
355
+ } else {
356
+ R.layout.surface_player_view
357
+ }
358
+ }
359
+
360
+ companion object {
361
+ fun isPictureInPictureSupported(currentActivity: Activity): Boolean {
362
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && currentActivity.packageManager.hasSystemFeature(
363
+ android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE
364
+ )
365
+ }
366
+ }
367
+ }
@@ -0,0 +1,24 @@
1
+ package expo.modules.video.delegates
2
+
3
+ import kotlin.reflect.KProperty
4
+
5
+ /**
6
+ * Property delegate, where the set is ignored unless the value has changed.
7
+ * @param T The type of the property.
8
+ * @param value The initial value of the property.
9
+ * @param propertyMapper A function that maps the new value to the property value.
10
+ * @param didSet A function that is called when the property value has changed.
11
+ */
12
+
13
+ class IgnoreSameSet<T : Any?>(private var value: T, val propertyMapper: ((T) -> T) = { v -> v }, val didSet: ((new: T, old: T) -> Unit)? = null) {
14
+ operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
15
+ return value
16
+ }
17
+
18
+ operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
19
+ if (this.value == propertyMapper(value)) return
20
+ val oldValue = this.value
21
+ this.value = propertyMapper(value)
22
+ didSet?.invoke(this.value, oldValue)
23
+ }
24
+ }
@@ -0,0 +1,217 @@
1
+ package expo.modules.video.drawing
2
+
3
+ import android.content.Context
4
+ import android.graphics.Canvas
5
+ import android.graphics.Outline
6
+ import android.graphics.Path
7
+ import android.graphics.RectF
8
+ import android.os.Build
9
+ import android.view.View
10
+ import android.view.ViewOutlineProvider
11
+ import com.facebook.react.modules.i18nmanager.I18nUtil
12
+ import com.facebook.react.uimanager.FloatUtil
13
+ import com.facebook.react.uimanager.PixelUtil
14
+ import com.facebook.yoga.YogaConstants
15
+ import expo.modules.video.utils.ifYogaUndefinedUse
16
+
17
+ class OutlineProvider(private val mContext: Context) : ViewOutlineProvider() {
18
+ enum class BorderRadiusConfig {
19
+ ALL,
20
+ TOP_LEFT,
21
+ TOP_RIGHT,
22
+ BOTTOM_RIGHT,
23
+ BOTTOM_LEFT,
24
+ TOP_START,
25
+ TOP_END,
26
+ BOTTOM_START,
27
+ BOTTOM_END
28
+ }
29
+
30
+ enum class CornerRadius {
31
+ TOP_LEFT,
32
+ TOP_RIGHT,
33
+ BOTTOM_RIGHT,
34
+ BOTTOM_LEFT
35
+ }
36
+
37
+ private var mLayoutDirection = View.LAYOUT_DIRECTION_LTR
38
+ private val mBounds = RectF()
39
+ val borderRadiiConfig = FloatArray(9) { YogaConstants.UNDEFINED }
40
+ private val mCornerRadii = FloatArray(4)
41
+ private var mCornerRadiiInvalidated = true
42
+ private val mConvexPath = Path()
43
+ private var mConvexPathInvalidated = true
44
+
45
+ init {
46
+ updateCornerRadiiIfNeeded()
47
+ }
48
+
49
+ private fun updateCornerRadiiIfNeeded() {
50
+ if (!mCornerRadiiInvalidated) {
51
+ return
52
+ }
53
+
54
+ val isRTL = mLayoutDirection == View.LAYOUT_DIRECTION_RTL
55
+ val isRTLSwap = I18nUtil.instance.doLeftAndRightSwapInRTL(mContext)
56
+ updateCornerRadius(
57
+ CornerRadius.TOP_LEFT,
58
+ BorderRadiusConfig.TOP_LEFT,
59
+ BorderRadiusConfig.TOP_RIGHT,
60
+ BorderRadiusConfig.TOP_START,
61
+ BorderRadiusConfig.TOP_END,
62
+ isRTL,
63
+ isRTLSwap
64
+ )
65
+ updateCornerRadius(
66
+ CornerRadius.TOP_RIGHT,
67
+ BorderRadiusConfig.TOP_RIGHT,
68
+ BorderRadiusConfig.TOP_LEFT,
69
+ BorderRadiusConfig.TOP_END,
70
+ BorderRadiusConfig.TOP_START,
71
+ isRTL,
72
+ isRTLSwap
73
+ )
74
+ updateCornerRadius(
75
+ CornerRadius.BOTTOM_LEFT,
76
+ BorderRadiusConfig.BOTTOM_LEFT,
77
+ BorderRadiusConfig.BOTTOM_RIGHT,
78
+ BorderRadiusConfig.BOTTOM_START,
79
+ BorderRadiusConfig.BOTTOM_END,
80
+ isRTL,
81
+ isRTLSwap
82
+ )
83
+ updateCornerRadius(
84
+ CornerRadius.BOTTOM_RIGHT,
85
+ BorderRadiusConfig.BOTTOM_RIGHT,
86
+ BorderRadiusConfig.BOTTOM_LEFT,
87
+ BorderRadiusConfig.BOTTOM_END,
88
+ BorderRadiusConfig.BOTTOM_START,
89
+ isRTL,
90
+ isRTLSwap
91
+ )
92
+ mCornerRadiiInvalidated = false
93
+ mConvexPathInvalidated = true
94
+ }
95
+
96
+ private fun updateCornerRadius(
97
+ outputPosition: CornerRadius,
98
+ inputPosition: BorderRadiusConfig,
99
+ oppositePosition: BorderRadiusConfig,
100
+ startPosition: BorderRadiusConfig,
101
+ endPosition: BorderRadiusConfig,
102
+ isRTL: Boolean,
103
+ isRTLSwap: Boolean
104
+ ) {
105
+ var radius = borderRadiiConfig[inputPosition.ordinal]
106
+ if (isRTL) {
107
+ if (isRTLSwap) {
108
+ radius = borderRadiiConfig[oppositePosition.ordinal]
109
+ }
110
+ if (YogaConstants.isUndefined(radius)) {
111
+ radius = borderRadiiConfig[endPosition.ordinal]
112
+ }
113
+ } else {
114
+ if (YogaConstants.isUndefined(radius)) {
115
+ radius = borderRadiiConfig[startPosition.ordinal]
116
+ }
117
+ }
118
+ radius = radius
119
+ .ifYogaUndefinedUse(borderRadiiConfig[BorderRadiusConfig.ALL.ordinal])
120
+ .ifYogaUndefinedUse(0f)
121
+ mCornerRadii[outputPosition.ordinal] = PixelUtil.toPixelFromDIP(radius)
122
+ }
123
+
124
+ private fun updateConvexPathIfNeeded() {
125
+ if (!mConvexPathInvalidated) {
126
+ return
127
+ }
128
+ mConvexPath.reset()
129
+ mConvexPath.addRoundRect(
130
+ mBounds,
131
+ floatArrayOf(
132
+ mCornerRadii[CornerRadius.TOP_LEFT.ordinal],
133
+ mCornerRadii[CornerRadius.TOP_LEFT.ordinal],
134
+ mCornerRadii[CornerRadius.TOP_RIGHT.ordinal],
135
+ mCornerRadii[CornerRadius.TOP_RIGHT.ordinal],
136
+ mCornerRadii[CornerRadius.BOTTOM_RIGHT.ordinal],
137
+ mCornerRadii[CornerRadius.BOTTOM_RIGHT.ordinal],
138
+ mCornerRadii[CornerRadius.BOTTOM_LEFT.ordinal],
139
+ mCornerRadii[CornerRadius.BOTTOM_LEFT.ordinal]
140
+ ),
141
+ Path.Direction.CW
142
+ )
143
+ mConvexPathInvalidated = false
144
+ }
145
+
146
+ fun hasEqualCorners(): Boolean {
147
+ updateCornerRadiiIfNeeded()
148
+ val initialCornerRadius = mCornerRadii[0]
149
+ return mCornerRadii.all { initialCornerRadius == it }
150
+ }
151
+
152
+ fun setBorderRadius(radius: Float, position: Int): Boolean {
153
+ if (!FloatUtil.floatsEqual(borderRadiiConfig[position], radius)) {
154
+ borderRadiiConfig[position] = radius
155
+ mCornerRadiiInvalidated = true
156
+ return true
157
+ }
158
+ return false
159
+ }
160
+
161
+ private fun updateBoundsAndLayoutDirection(view: View) {
162
+ // Update layout direction
163
+ val layoutDirection = view.layoutDirection
164
+ if (mLayoutDirection != layoutDirection) {
165
+ mLayoutDirection = layoutDirection
166
+ mCornerRadiiInvalidated = true
167
+ }
168
+
169
+ // Update size
170
+ val left = 0
171
+ val top = 0
172
+ val right = view.width
173
+ val bottom = view.height
174
+ if (mBounds.left != left.toFloat() ||
175
+ mBounds.top != top.toFloat() ||
176
+ mBounds.right != right.toFloat() ||
177
+ mBounds.bottom != bottom.toFloat()
178
+ ) {
179
+ mBounds[left.toFloat(), top.toFloat(), right.toFloat()] = bottom.toFloat()
180
+ mCornerRadiiInvalidated = true
181
+ }
182
+ }
183
+
184
+ override fun getOutline(view: View, outline: Outline) {
185
+ updateBoundsAndLayoutDirection(view)
186
+
187
+ // Calculate outline
188
+ updateCornerRadiiIfNeeded()
189
+ if (hasEqualCorners()) {
190
+ val cornerRadius = mCornerRadii[0]
191
+ if (cornerRadius > 0) {
192
+ outline.setRoundRect(0, 0, mBounds.width().toInt(), mBounds.height().toInt(), cornerRadius)
193
+ } else {
194
+ outline.setRect(0, 0, mBounds.width().toInt(), mBounds.height().toInt())
195
+ }
196
+ } else {
197
+ // Clipping is not supported when using a convex path, but drawing the elevation
198
+ // shadow is. For the particular case, we fallback to canvas clipping in the view
199
+ // which is supposed to call `clipCanvasIfNeeded` in its `draw` method.
200
+ updateConvexPathIfNeeded()
201
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
202
+ outline.setPath(mConvexPath)
203
+ } else {
204
+ outline.setConvexPath(mConvexPath)
205
+ }
206
+ }
207
+ }
208
+
209
+ fun clipCanvasIfNeeded(canvas: Canvas, view: View) {
210
+ updateBoundsAndLayoutDirection(view)
211
+ updateCornerRadiiIfNeeded()
212
+ if (!hasEqualCorners()) {
213
+ updateConvexPathIfNeeded()
214
+ canvas.clipPath(mConvexPath)
215
+ }
216
+ }
217
+ }
@@ -0,0 +1,20 @@
1
+ package expo.modules.video.enums
2
+
3
+ import expo.modules.kotlin.types.Enumerable
4
+
5
+ enum class AudioMixingMode(val value: String) : Enumerable {
6
+ MIX_WITH_OTHERS("mixWithOthers"),
7
+ DUCK_OTHERS("duckOthers"),
8
+ AUTO("auto"),
9
+ DO_NOT_MIX("doNotMix");
10
+
11
+ val priority: Int
12
+ get() {
13
+ return when (this) {
14
+ DO_NOT_MIX -> 3
15
+ AUTO -> 2
16
+ DUCK_OTHERS -> 1
17
+ MIX_WITH_OTHERS -> 0
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,19 @@
1
+ package expo.modules.video.enums
2
+
3
+ import androidx.media3.ui.AspectRatioFrameLayout
4
+ import expo.modules.kotlin.types.Enumerable
5
+
6
+ @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
7
+ enum class ContentFit(val value: String) : Enumerable {
8
+ CONTAIN("contain"),
9
+ FILL("fill"),
10
+ COVER("cover");
11
+
12
+ fun toResizeMode(): Int {
13
+ return when (this) {
14
+ CONTAIN -> AspectRatioFrameLayout.RESIZE_MODE_FIT
15
+ FILL -> AspectRatioFrameLayout.RESIZE_MODE_FILL
16
+ COVER -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,22 @@
1
+ package expo.modules.video.enums
2
+
3
+ import androidx.media3.common.MimeTypes
4
+ import expo.modules.kotlin.types.Enumerable
5
+
6
+ enum class ContentType(val value: String) : Enumerable {
7
+ AUTO("auto"),
8
+ PROGRESSIVE("progressive"),
9
+ HLS("hls"),
10
+ DASH("dash"),
11
+ SMOOTH_STREAMING("smoothStreaming");
12
+
13
+ fun toMimeTypeString(): String? {
14
+ return when (this) {
15
+ AUTO -> null
16
+ PROGRESSIVE -> null
17
+ HLS -> MimeTypes.APPLICATION_M3U8
18
+ DASH -> MimeTypes.APPLICATION_MPD
19
+ SMOOTH_STREAMING -> MimeTypes.APPLICATION_SS
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,26 @@
1
+ package expo.modules.video.enums
2
+
3
+ import androidx.media3.common.C
4
+ import expo.modules.kotlin.types.Enumerable
5
+ import expo.modules.video.UnsupportedDRMTypeException
6
+ import java.util.UUID
7
+
8
+ enum class DRMType(val value: String) : Enumerable {
9
+ CLEARKEY("clearkey"),
10
+ FAIRPLAY("fairplay"),
11
+ PLAYREADY("playready"),
12
+ WIDEVINE("widevine");
13
+
14
+ fun isSupported(): Boolean {
15
+ return this != FAIRPLAY
16
+ }
17
+
18
+ fun toUUID(): UUID {
19
+ return when (this) {
20
+ CLEARKEY -> C.CLEARKEY_UUID
21
+ FAIRPLAY -> throw UnsupportedDRMTypeException(this)
22
+ PLAYREADY -> C.PLAYREADY_UUID
23
+ WIDEVINE -> C.WIDEVINE_UUID
24
+ }
25
+ }
26
+ }