expo-video-subtitle 0.1.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 (255) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/FORK.md +149 -0
  3. package/LICENSE +21 -0
  4. package/README.md +108 -0
  5. package/android/build.gradle +32 -0
  6. package/android/src/main/AndroidManifest.xml +10 -0
  7. package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +258 -0
  8. package/android/src/main/java/expo/modules/video/IntervalUpdateClock.kt +54 -0
  9. package/android/src/main/java/expo/modules/video/MediaMetadataRetriever.kt +89 -0
  10. package/android/src/main/java/expo/modules/video/PlayerViewExtension.kt +87 -0
  11. package/android/src/main/java/expo/modules/video/VideoCache.kt +109 -0
  12. package/android/src/main/java/expo/modules/video/VideoExceptions.kt +45 -0
  13. package/android/src/main/java/expo/modules/video/VideoModule.kt +446 -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 +457 -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/FullscreenOrientation.kt +25 -0
  23. package/android/src/main/java/expo/modules/video/enums/PlayerStatus.kt +10 -0
  24. package/android/src/main/java/expo/modules/video/enums/VideoRange.kt +27 -0
  25. package/android/src/main/java/expo/modules/video/listeners/VideoManagerListener.kt +10 -0
  26. package/android/src/main/java/expo/modules/video/listeners/VideoPlayerListener.kt +35 -0
  27. package/android/src/main/java/expo/modules/video/listeners/VideoViewListener.kt +13 -0
  28. package/android/src/main/java/expo/modules/video/managers/AudioFocusManager.kt +242 -0
  29. package/android/src/main/java/expo/modules/video/managers/PictureInPictureManager.kt +329 -0
  30. package/android/src/main/java/expo/modules/video/managers/VideoManager.kt +194 -0
  31. package/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt +267 -0
  32. package/android/src/main/java/expo/modules/video/playbackService/PlaybackServiceConnection.kt +62 -0
  33. package/android/src/main/java/expo/modules/video/playbackService/VideoMediaSessionCallback.kt +47 -0
  34. package/android/src/main/java/expo/modules/video/player/DefaultLoadControl.java +552 -0
  35. package/android/src/main/java/expo/modules/video/player/FirstFrameEventGenerator.kt +116 -0
  36. package/android/src/main/java/expo/modules/video/player/PlayerEvent.kt +172 -0
  37. package/android/src/main/java/expo/modules/video/player/VideoPlayer.kt +598 -0
  38. package/android/src/main/java/expo/modules/video/player/VideoPlayerAudioTracks.kt +126 -0
  39. package/android/src/main/java/expo/modules/video/player/VideoPlayerKeepAwake.kt +91 -0
  40. package/android/src/main/java/expo/modules/video/player/VideoPlayerLoadControl.kt +57 -0
  41. package/android/src/main/java/expo/modules/video/player/VideoPlayerSubtitles.kt +126 -0
  42. package/android/src/main/java/expo/modules/video/records/BufferOptions.kt +18 -0
  43. package/android/src/main/java/expo/modules/video/records/ButtonOptions.kt +17 -0
  44. package/android/src/main/java/expo/modules/video/records/DRMOptions.kt +27 -0
  45. package/android/src/main/java/expo/modules/video/records/FullscreenOptions.kt +14 -0
  46. package/android/src/main/java/expo/modules/video/records/PiPParams.kt +38 -0
  47. package/android/src/main/java/expo/modules/video/records/PlaybackError.kt +21 -0
  48. package/android/src/main/java/expo/modules/video/records/PlayerBuilderOptions.kt +15 -0
  49. package/android/src/main/java/expo/modules/video/records/ScrubbingModeOptions.kt +33 -0
  50. package/android/src/main/java/expo/modules/video/records/SeekTolerance.kt +30 -0
  51. package/android/src/main/java/expo/modules/video/records/SubtitleSource.kt +54 -0
  52. package/android/src/main/java/expo/modules/video/records/SubtitleStyle.kt +98 -0
  53. package/android/src/main/java/expo/modules/video/records/Tracks.kt +115 -0
  54. package/android/src/main/java/expo/modules/video/records/VideoEventPayloads.kt +79 -0
  55. package/android/src/main/java/expo/modules/video/records/VideoMetadata.kt +14 -0
  56. package/android/src/main/java/expo/modules/video/records/VideoSize.kt +16 -0
  57. package/android/src/main/java/expo/modules/video/records/VideoSource.kt +113 -0
  58. package/android/src/main/java/expo/modules/video/records/VideoThumbnailOptions.kt +26 -0
  59. package/android/src/main/java/expo/modules/video/utils/DataSourceUtils.kt +76 -0
  60. package/android/src/main/java/expo/modules/video/utils/EventDispatcherUtils.kt +43 -0
  61. package/android/src/main/java/expo/modules/video/utils/FullscreenActivityOrientationHelper.kt +120 -0
  62. package/android/src/main/java/expo/modules/video/utils/MediaSessionUtils.kt +14 -0
  63. package/android/src/main/java/expo/modules/video/utils/MutableWeakReference.kt +26 -0
  64. package/android/src/main/java/expo/modules/video/utils/PictureInPictureHelperFragment.kt +49 -0
  65. package/android/src/main/java/expo/modules/video/utils/PictureInPictureUtils.kt +96 -0
  66. package/android/src/main/java/expo/modules/video/utils/SubtitleUtils.kt +68 -0
  67. package/android/src/main/java/expo/modules/video/utils/ViewVisibilityUtils.kt +28 -0
  68. package/android/src/main/java/expo/modules/video/utils/WeakMutableSet.kt +120 -0
  69. package/android/src/main/java/expo/modules/video/utils/YogaUtils.kt +20 -0
  70. package/android/src/main/res/drawable/seek_backwards_10s.xml +25 -0
  71. package/android/src/main/res/drawable/seek_backwards_15s.xml +25 -0
  72. package/android/src/main/res/drawable/seek_backwards_5s.xml +25 -0
  73. package/android/src/main/res/drawable/seek_forwards_10s.xml +30 -0
  74. package/android/src/main/res/drawable/seek_forwards_15s.xml +31 -0
  75. package/android/src/main/res/drawable/seek_forwards_5s.xml +30 -0
  76. package/android/src/main/res/layout/fullscreen_player_activity.xml +20 -0
  77. package/android/src/main/res/layout/surface_player_view.xml +7 -0
  78. package/android/src/main/res/layout/texture_player_view.xml +7 -0
  79. package/android/src/main/res/values/styles.xml +9 -0
  80. package/app.plugin.js +1 -0
  81. package/build/NativeVideoAirPlayButtonView.d.ts +3 -0
  82. package/build/NativeVideoAirPlayButtonView.d.ts.map +1 -0
  83. package/build/NativeVideoAirPlayButtonView.js +3 -0
  84. package/build/NativeVideoAirPlayButtonView.js.map +1 -0
  85. package/build/NativeVideoModule.d.ts +13 -0
  86. package/build/NativeVideoModule.d.ts.map +1 -0
  87. package/build/NativeVideoModule.js +3 -0
  88. package/build/NativeVideoModule.js.map +1 -0
  89. package/build/NativeVideoModule.web.d.ts +5 -0
  90. package/build/NativeVideoModule.web.d.ts.map +1 -0
  91. package/build/NativeVideoModule.web.js +5 -0
  92. package/build/NativeVideoModule.web.js.map +1 -0
  93. package/build/NativeVideoView.d.ts +4 -0
  94. package/build/NativeVideoView.d.ts.map +1 -0
  95. package/build/NativeVideoView.js +6 -0
  96. package/build/NativeVideoView.js.map +1 -0
  97. package/build/VideoAirPlayButton.d.ts +10 -0
  98. package/build/VideoAirPlayButton.d.ts.map +1 -0
  99. package/build/VideoAirPlayButton.ios.d.ts +3 -0
  100. package/build/VideoAirPlayButton.ios.d.ts.map +1 -0
  101. package/build/VideoAirPlayButton.ios.js +6 -0
  102. package/build/VideoAirPlayButton.ios.js.map +1 -0
  103. package/build/VideoAirPlayButton.js +13 -0
  104. package/build/VideoAirPlayButton.js.map +1 -0
  105. package/build/VideoAirPlayButton.types.d.ts +35 -0
  106. package/build/VideoAirPlayButton.types.d.ts.map +1 -0
  107. package/build/VideoAirPlayButton.types.js +2 -0
  108. package/build/VideoAirPlayButton.types.js.map +1 -0
  109. package/build/VideoModule.d.ts +35 -0
  110. package/build/VideoModule.d.ts.map +1 -0
  111. package/build/VideoModule.js +44 -0
  112. package/build/VideoModule.js.map +1 -0
  113. package/build/VideoPlayer.d.ts +17 -0
  114. package/build/VideoPlayer.d.ts.map +1 -0
  115. package/build/VideoPlayer.js +55 -0
  116. package/build/VideoPlayer.js.map +1 -0
  117. package/build/VideoPlayer.types.d.ts +782 -0
  118. package/build/VideoPlayer.types.d.ts.map +1 -0
  119. package/build/VideoPlayer.types.js +2 -0
  120. package/build/VideoPlayer.types.js.map +1 -0
  121. package/build/VideoPlayer.web.d.ts +79 -0
  122. package/build/VideoPlayer.web.d.ts.map +1 -0
  123. package/build/VideoPlayer.web.js +388 -0
  124. package/build/VideoPlayer.web.js.map +1 -0
  125. package/build/VideoPlayerEvents.types.d.ts +278 -0
  126. package/build/VideoPlayerEvents.types.d.ts.map +1 -0
  127. package/build/VideoPlayerEvents.types.js +2 -0
  128. package/build/VideoPlayerEvents.types.js.map +1 -0
  129. package/build/VideoThumbnail.d.ts +29 -0
  130. package/build/VideoThumbnail.d.ts.map +1 -0
  131. package/build/VideoThumbnail.js +4 -0
  132. package/build/VideoThumbnail.js.map +1 -0
  133. package/build/VideoView.d.ts +69 -0
  134. package/build/VideoView.d.ts.map +1 -0
  135. package/build/VideoView.js +100 -0
  136. package/build/VideoView.js.map +1 -0
  137. package/build/VideoView.types.d.ts +362 -0
  138. package/build/VideoView.types.d.ts.map +1 -0
  139. package/build/VideoView.types.js +2 -0
  140. package/build/VideoView.types.js.map +1 -0
  141. package/build/VideoView.web.d.ts +9 -0
  142. package/build/VideoView.web.d.ts.map +1 -0
  143. package/build/VideoView.web.js +239 -0
  144. package/build/VideoView.web.js.map +1 -0
  145. package/build/index.d.ts +11 -0
  146. package/build/index.d.ts.map +1 -0
  147. package/build/index.js +6 -0
  148. package/build/index.js.map +1 -0
  149. package/build/resolveAssetSource.d.ts +3 -0
  150. package/build/resolveAssetSource.d.ts.map +1 -0
  151. package/build/resolveAssetSource.js +3 -0
  152. package/build/resolveAssetSource.js.map +1 -0
  153. package/build/resolveAssetSource.web.d.ts +4 -0
  154. package/build/resolveAssetSource.web.d.ts.map +1 -0
  155. package/build/resolveAssetSource.web.js +16 -0
  156. package/build/resolveAssetSource.web.js.map +1 -0
  157. package/expo-module.config.json +9 -0
  158. package/ios/Cache/CachableRequest.swift +46 -0
  159. package/ios/Cache/CacheVideoAssetTransportProvider.swift +64 -0
  160. package/ios/Cache/CachedResource.swift +164 -0
  161. package/ios/Cache/CachingHelpers.swift +107 -0
  162. package/ios/Cache/MediaFileHandle.swift +139 -0
  163. package/ios/Cache/MediaInfo.swift +185 -0
  164. package/ios/Cache/ResourceLoaderDelegate.swift +306 -0
  165. package/ios/Cache/SynchronizedHashTable.swift +23 -0
  166. package/ios/Cache/VideoCacheManager.swift +192 -0
  167. package/ios/ContentKeyDelegate.swift +214 -0
  168. package/ios/ContentKeyManager.swift +21 -0
  169. package/ios/Enums/AudioMixingMode.swift +37 -0
  170. package/ios/Enums/ContentType.swift +11 -0
  171. package/ios/Enums/DRMType.swift +20 -0
  172. package/ios/Enums/FullscreenOrientation.swift +34 -0
  173. package/ios/Enums/KeepFullscreenOnPiPStopBehavior.swift +20 -0
  174. package/ios/Enums/PlayerStatus.swift +10 -0
  175. package/ios/Enums/VideoContentFit.swift +39 -0
  176. package/ios/Enums/VideoRange.swift +21 -0
  177. package/ios/ExpoVideo.podspec +27 -0
  178. package/ios/NowPlayingManager.swift +304 -0
  179. package/ios/OrientationAVPlayerViewController.swift +263 -0
  180. package/ios/Records/BufferOptions.swift +12 -0
  181. package/ios/Records/DRMOptions.swift +24 -0
  182. package/ios/Records/FullscreenOptions.swift +14 -0
  183. package/ios/Records/PlaybackError.swift +10 -0
  184. package/ios/Records/ScrubbingModeOptions.swift +7 -0
  185. package/ios/Records/SeekTolerance.swift +20 -0
  186. package/ios/Records/SubtitleSource.swift +32 -0
  187. package/ios/Records/SubtitleStyle.swift +118 -0
  188. package/ios/Records/Tracks.swift +229 -0
  189. package/ios/Records/VideoEventPayloads.swift +81 -0
  190. package/ios/Records/VideoMetadata.swift +16 -0
  191. package/ios/Records/VideoSize.swift +15 -0
  192. package/ios/Records/VideoSource.swift +28 -0
  193. package/ios/Thumbnails/VideoThumbnail.swift +27 -0
  194. package/ios/Thumbnails/VideoThumbnailGenerator.swift +68 -0
  195. package/ios/Thumbnails/VideoThumbnailOptions.swift +15 -0
  196. package/ios/Utils/AVAssetVariant+VideoTracks.swift +20 -0
  197. package/ios/Utils/AvAssetTrack+VideoTracks.swift +79 -0
  198. package/ios/Utils/FourCharCode+toString.swift +25 -0
  199. package/ios/Utils/HlsUriUtils.swift +27 -0
  200. package/ios/Utils/URL+MediaLibraryAssets.swift +68 -0
  201. package/ios/VideoAirPlayButtonView.swift +70 -0
  202. package/ios/VideoAsset.swift +98 -0
  203. package/ios/VideoAssetTransport/VideoAssetLoadPlan.swift +69 -0
  204. package/ios/VideoAssetTransport/VideoAssetSourceDescriptor.swift +29 -0
  205. package/ios/VideoAssetTransport/VideoAssetTransportProvider.swift +24 -0
  206. package/ios/VideoAssetTransport/VideoAssetTransportRegistry.swift +103 -0
  207. package/ios/VideoExceptions.swift +53 -0
  208. package/ios/VideoItem.swift +11 -0
  209. package/ios/VideoManager.swift +182 -0
  210. package/ios/VideoModule.swift +413 -0
  211. package/ios/VideoPlayer/DangerousPropertiesStore.swift +19 -0
  212. package/ios/VideoPlayer.swift +485 -0
  213. package/ios/VideoPlayerAudioTracks.swift +109 -0
  214. package/ios/VideoPlayerItem.swift +169 -0
  215. package/ios/VideoPlayerObserver.swift +612 -0
  216. package/ios/VideoPlayerSeeker.swift +77 -0
  217. package/ios/VideoPlayerSubtitleSideload.swift +203 -0
  218. package/ios/VideoPlayerSubtitles.swift +88 -0
  219. package/ios/VideoSourceLoader.swift +90 -0
  220. package/ios/VideoSourceLoaderListener.swift +34 -0
  221. package/ios/VideoView.swift +201 -0
  222. package/package.json +51 -0
  223. package/plugin/build/index.d.ts +3 -0
  224. package/plugin/build/index.js +3 -0
  225. package/plugin/build/withExpoVideo.d.ts +9 -0
  226. package/plugin/build/withExpoVideo.js +73 -0
  227. package/plugin/index.d.ts +1 -0
  228. package/plugin/index.js +1 -0
  229. package/plugin/jest.config.js +1 -0
  230. package/plugin/src/index.ts +3 -0
  231. package/plugin/src/withExpoVideo.ts +103 -0
  232. package/plugin/tsconfig.json +10 -0
  233. package/src/NativeVideoAirPlayButtonView.ts +3 -0
  234. package/src/NativeVideoModule.ts +16 -0
  235. package/src/NativeVideoModule.web.ts +4 -0
  236. package/src/NativeVideoView.ts +8 -0
  237. package/src/VideoAirPlayButton.ios.tsx +8 -0
  238. package/src/VideoAirPlayButton.tsx +14 -0
  239. package/src/VideoAirPlayButton.types.ts +39 -0
  240. package/src/VideoModule.ts +47 -0
  241. package/src/VideoPlayer.tsx +74 -0
  242. package/src/VideoPlayer.types.ts +893 -0
  243. package/src/VideoPlayer.web.tsx +466 -0
  244. package/src/VideoPlayerEvents.types.ts +332 -0
  245. package/src/VideoThumbnail.ts +31 -0
  246. package/src/VideoView.tsx +111 -0
  247. package/src/VideoView.types.ts +405 -0
  248. package/src/VideoView.web.tsx +293 -0
  249. package/src/index.ts +26 -0
  250. package/src/resolveAssetSource.ts +2 -0
  251. package/src/resolveAssetSource.web.ts +17 -0
  252. package/src/ts-declarations/react-native-assets.d.ts +22 -0
  253. package/tsconfig.all.json +11 -0
  254. package/tsconfig.json +10 -0
  255. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,457 @@
1
+ package expo.modules.video
2
+
3
+ import android.app.Activity
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.graphics.Color
7
+ import android.os.Build
8
+ import android.view.accessibility.CaptioningManager
9
+ import android.view.LayoutInflater
10
+ import android.view.MotionEvent
11
+ import android.view.View
12
+ import android.view.ViewGroup
13
+ import android.widget.ImageButton
14
+ import androidx.media3.common.Tracks
15
+ import androidx.media3.ui.PlayerView
16
+ import com.facebook.react.bridge.ReactContext
17
+ import com.facebook.react.uimanager.UIManagerHelper
18
+ import com.facebook.react.uimanager.events.EventDispatcher
19
+ import com.facebook.react.uimanager.events.TouchEventCoalescingKeyHelper
20
+ import expo.modules.kotlin.AppContext
21
+ import expo.modules.kotlin.viewevent.EventDispatcher
22
+ import expo.modules.kotlin.views.ExpoView
23
+ import expo.modules.video.delegates.IgnoreSameSet
24
+ import expo.modules.video.enums.ContentFit
25
+ import expo.modules.video.player.VideoPlayer
26
+ import expo.modules.video.listeners.VideoPlayerListener
27
+ import expo.modules.video.listeners.VideoViewListener
28
+ import expo.modules.video.records.AudioTrack
29
+ import expo.modules.video.records.ButtonOptions
30
+ import expo.modules.video.records.SubtitleStyle
31
+ import expo.modules.video.records.SubtitleTrack
32
+ import expo.modules.video.records.VideoSource
33
+ import expo.modules.video.records.VideoTrack
34
+ import expo.modules.video.records.FullscreenOptions
35
+ import expo.modules.video.utils.SubtitleUtils
36
+ import expo.modules.video.utils.dispatchMotionEvent
37
+ import expo.modules.video.managers.VideoManager
38
+ import expo.modules.video.managers.calculateCurrentPipAspectRatio
39
+ import expo.modules.video.records.PiPParams
40
+ import expo.modules.video.utils.calculateRectHint
41
+ import java.lang.ref.WeakReference
42
+ import java.util.UUID
43
+
44
+ class SurfaceVideoView(context: Context, appContext: AppContext) : VideoView(context, appContext)
45
+ class TextureVideoView(context: Context, appContext: AppContext) : VideoView(context, appContext, true)
46
+
47
+ @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
48
+ open class VideoView(context: Context, appContext: AppContext, useTextureView: Boolean = false) : ExpoView(context, appContext), VideoPlayerListener {
49
+ val videoViewId: String = UUID.randomUUID().toString()
50
+ val playerView: PlayerView = LayoutInflater.from(context.applicationContext).inflate(getPlayerViewLayoutId(useTextureView), null) as PlayerView
51
+ val onPictureInPictureStart by EventDispatcher<Unit>()
52
+ val onPictureInPictureStop by EventDispatcher<Unit>()
53
+ val onFullscreenEnter by EventDispatcher<Unit>()
54
+ val onFullscreenExit by EventDispatcher<Unit>()
55
+ val onFirstFrameRender by EventDispatcher<Unit>()
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 currentTrackHasSubtitles = false
63
+ private set
64
+ var showsAudioTracksButton = false
65
+ private set
66
+
67
+ var requiresLinearPlayback: Boolean = false
68
+ set(value) {
69
+ field = value
70
+ videoPlayer?.requiresLinearPlayback = value
71
+ playerView.applyRequiresLinearPlayback(value)
72
+ applyButtonSettings()
73
+ }
74
+
75
+ var buttonOptions: ButtonOptions = ButtonOptions()
76
+ set(value) {
77
+ field = value
78
+ applyButtonSettings()
79
+ }
80
+
81
+ var subtitleStyle: SubtitleStyle? = null
82
+ set(value) {
83
+ field = value
84
+ SubtitleUtils.configureSubtitleView(playerView, context, value)
85
+ }
86
+
87
+ private var listeners = mutableListOf<WeakReference<VideoViewListener>>()
88
+ private val currentActivity = appContext.throwingActivity
89
+ private val decorView = currentActivity.window.decorView
90
+ private val touchEventCoalescingKeyHelper = TouchEventCoalescingKeyHelper()
91
+ private var reactNativeEventDispatcher: EventDispatcher? = null
92
+ private var captioningChangeListener: CaptioningManager.CaptioningChangeListener? = null
93
+
94
+ private val windowFocusChangeListener = View.OnFocusChangeListener { _, hasFocus ->
95
+ if (hasFocus) {
96
+ // Reconfigure when window gains focus (returning from settings)
97
+ SubtitleUtils.configureSubtitleView(playerView, context, subtitleStyle)
98
+ }
99
+ }
100
+
101
+ var pipParams by IgnoreSameSet(PiPParams()) { new, old ->
102
+ listeners.forEach {
103
+ it.get()?.onPiPParamsChanged(this, old, new)
104
+ }
105
+ }
106
+ private set
107
+
108
+ // We need to keep track of the target surface view visibility, but only apply it when `useExoShutter` is false.
109
+ var shouldHideSurfaceView: Boolean = true
110
+
111
+ var useExoShutter: Boolean? = null
112
+ set(value) {
113
+ if (value == true) {
114
+ playerView.setShutterBackgroundColor(Color.BLACK)
115
+ } else {
116
+ playerView.setShutterBackgroundColor(Color.TRANSPARENT)
117
+ }
118
+ applySurfaceViewVisibility()
119
+ field = value
120
+ }
121
+
122
+ var autoEnterPiP: Boolean by IgnoreSameSet(false) { new, _ ->
123
+ pipParams = pipParams.copy(autoEnter = new)
124
+ }
125
+
126
+ var contentFit: ContentFit = ContentFit.CONTAIN
127
+ set(value) {
128
+ playerView.resizeMode = value.toResizeMode()
129
+ field = value
130
+ }
131
+
132
+ var videoPlayer: VideoPlayer? = null
133
+ set(newPlayer) {
134
+ field?.let {
135
+ VideoManager.onVideoPlayerDetachedFromView(it, this)
136
+ }
137
+ val oldPlayer = videoPlayer
138
+ val hasEmittedFirstFrame = newPlayer?.firstFrameEventGenerator?.hasSentFirstFrameForCurrentMediaItem
139
+ ?: false
140
+ oldPlayer?.removeListener(this)
141
+ newPlayer?.addListener(this)
142
+ field = newPlayer
143
+ shouldHideSurfaceView = !hasEmittedFirstFrame
144
+ applySurfaceViewVisibility()
145
+ attachPlayer()
146
+ newPlayer?.let {
147
+ VideoManager.onVideoPlayerAttachedToView(it, this)
148
+ }
149
+ if (oldPlayer != newPlayer) {
150
+ oldPlayer?.hasBeenDisconnectedFromVideoView()
151
+ }
152
+ }
153
+
154
+ var useNativeControls: Boolean = true
155
+ set(value) {
156
+ val shouldShowSubtitle = value && (buttonOptions.showSubtitles ?: currentTrackHasSubtitles)
157
+ playerView.useController = value
158
+ playerView.setShowSubtitleButton(shouldShowSubtitle)
159
+ field = value
160
+ }
161
+
162
+ var allowsFullscreen: Boolean = true
163
+ set(value) {
164
+ if (value) {
165
+ playerView.setFullscreenButtonClickListener { enterFullscreen() }
166
+ } else {
167
+ playerView.setFullscreenButtonClickListener(null)
168
+ // Setting listener to null should hide the button, but judging by ExoPlayer source code
169
+ // there is a bug and the button isn't hidden. We need to do it manually.
170
+ playerView.setFullscreenButtonVisibility(false)
171
+ }
172
+ field = value
173
+ }
174
+
175
+ var fullscreenOptions: FullscreenOptions = FullscreenOptions()
176
+ set(value) {
177
+ field = value
178
+ if (value.enable) {
179
+ playerView.setFullscreenButtonClickListener { enterFullscreen() }
180
+ } else {
181
+ playerView.setFullscreenButtonClickListener(null)
182
+ playerView.setFullscreenButtonVisibility(false)
183
+ }
184
+ }
185
+
186
+ private val mLayoutRunnable = Runnable {
187
+ measure(
188
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
189
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
190
+ )
191
+ layout(left, top, right, bottom)
192
+ }
193
+
194
+ init {
195
+ VideoManager.registerVideoView(this)
196
+ playerView.setFullscreenButtonClickListener { enterFullscreen() }
197
+ // The prop `useNativeControls` prop is sometimes applied after the view is created, and sometimes there is a visible
198
+ // flash of controls event when they are set to off. Initially we set it to `false` and apply it in `onAttachedToWindow` to avoid this.
199
+ this.playerView.useController = false
200
+
201
+ // Start with the SurfaceView being transparent to avoid any flickers when the prop value is delivered.
202
+ this.playerView.setShutterBackgroundColor(Color.TRANSPARENT)
203
+ this.playerView.videoSurfaceView?.alpha = 0f
204
+
205
+ // Configure subtitle view to fix sizing issues with embedded styles
206
+ SubtitleUtils.configureSubtitleView(playerView, context, subtitleStyle)
207
+ addView(
208
+ playerView,
209
+ ViewGroup.LayoutParams(
210
+ ViewGroup.LayoutParams.MATCH_PARENT,
211
+ ViewGroup.LayoutParams.MATCH_PARENT
212
+ )
213
+ )
214
+
215
+ reactNativeEventDispatcher = UIManagerHelper.getEventDispatcher(appContext.reactContext as ReactContext, id)
216
+ }
217
+
218
+ fun applySurfaceViewVisibility() {
219
+ if (useExoShutter != true && shouldHideSurfaceView) {
220
+ playerView.videoSurfaceView?.alpha = 0f
221
+ } else {
222
+ playerView.videoSurfaceView?.alpha = 1f
223
+ }
224
+ }
225
+
226
+ fun enterFullscreen() {
227
+ val intent = Intent(context, FullscreenPlayerActivity::class.java)
228
+ intent.putExtra(VideoManager.INTENT_PLAYER_KEY, videoViewId)
229
+ intent.putExtra(FullscreenPlayerActivity.INTENT_FULLSCREEN_OPTIONS_KEY, fullscreenOptions)
230
+ // Set before starting the activity to avoid entering PiP unintentionally
231
+ isInFullscreen = true
232
+ currentActivity.startActivity(intent)
233
+
234
+ // Disable the enter transition
235
+ if (Build.VERSION.SDK_INT >= 34) {
236
+ currentActivity.overrideActivityTransition(Activity.OVERRIDE_TRANSITION_OPEN, 0, 0)
237
+ } else {
238
+ @Suppress("DEPRECATION")
239
+ currentActivity.overridePendingTransition(0, 0)
240
+ }
241
+ onFullscreenEnter(Unit)
242
+ pipParams = pipParams.copy(blocksAppFromEntering = true)
243
+ }
244
+
245
+ fun attachPlayer() {
246
+ videoPlayer?.changeVideoView(this)
247
+ }
248
+
249
+ fun exitFullscreen() {
250
+ // Fullscreen uses a different PlayerView instance, because of that we need to manually update the non-fullscreen player icon after exiting
251
+ val fullScreenButton: ImageButton = playerView.findViewById(androidx.media3.ui.R.id.exo_fullscreen)
252
+ fullScreenButton.setImageResource(androidx.media3.ui.R.drawable.exo_icon_fullscreen_enter)
253
+ attachPlayer()
254
+ onFullscreenExit(Unit)
255
+ isInFullscreen = false
256
+ pipParams = pipParams.copy(blocksAppFromEntering = false)
257
+ }
258
+
259
+ fun enterPictureInPicture() {
260
+ if (!isPictureInPictureSupported(currentActivity)) {
261
+ throw PictureInPictureUnsupportedException()
262
+ }
263
+
264
+ if (playerView.player == null) {
265
+ throw PictureInPictureEnterException("No player attached to the VideoView")
266
+ }
267
+
268
+ pipParams = pipParams.copy(willEnter = true)
269
+ VideoManager.pictureInPicture.enterPictureInPicture(this)
270
+ }
271
+
272
+ /**
273
+ * @param pipCandidate - VideoView that was elected to be displayed in PiP
274
+ */
275
+ fun onStartPictureInPicture(pipCandidate: VideoView?) {
276
+ onPictureInPictureStart(Unit)
277
+ }
278
+
279
+ /**
280
+ * @param pipCandidate - VideoView that was being displayed in PiP
281
+ */
282
+ fun onStopPictureInPicture(pipCandidate: VideoView?) {
283
+ pipParams = pipParams.copy(willEnter = false)
284
+ onPictureInPictureStop(Unit)
285
+ }
286
+
287
+ fun addVideoViewListener(listener: VideoViewListener) {
288
+ if (listeners.any { it.get() == listener }) {
289
+ return
290
+ }
291
+ listeners.add(WeakReference(listener))
292
+ }
293
+
294
+ fun removeVideoViewListener(listener: VideoViewListener) {
295
+ listeners.retainAll { it.get() != listener }
296
+ }
297
+
298
+ override fun onSourceChanged(player: VideoPlayer, source: VideoSource?, oldSource: VideoSource?) {
299
+ // When replacing the media on the same player, keep the surface hidden until the first frame of
300
+ // the new item is rendered. Otherwise the previous stream can remain visible (often mis-scaled)
301
+ // while the new stream buffers — e.g. switching from a higher-resolution URL to a lower one.
302
+ // See https://github.com/expo/expo/issues/44385
303
+ if (player == videoPlayer && oldSource != null) {
304
+ shouldHideSurfaceView = true
305
+ applySurfaceViewVisibility()
306
+ }
307
+ }
308
+
309
+ override fun onVideoSourceLoaded(
310
+ player: VideoPlayer,
311
+ videoSource: VideoSource?,
312
+ duration: Double?,
313
+ availableVideoTracks: List<VideoTrack>,
314
+ availableSubtitleTracks: List<SubtitleTrack>,
315
+ availableAudioTracks: List<AudioTrack>
316
+ ) {
317
+ pipParams = pipParams.copy(aspectRatio = calculateCurrentPipAspectRatio())
318
+ super.onVideoSourceLoaded(player, videoSource, duration, availableVideoTracks, availableSubtitleTracks, availableAudioTracks)
319
+ }
320
+
321
+ override fun onIsPlayingChanged(player: VideoPlayer, isPlaying: Boolean, oldIsPlaying: Boolean?) {
322
+ if (player == videoPlayer && isPlaying) {
323
+ wasAutoPaused = false
324
+ }
325
+ }
326
+
327
+ override fun onTracksChanged(player: VideoPlayer, tracks: Tracks) {
328
+ currentTrackHasSubtitles = player.subtitles.availableSubtitleTracks.isNotEmpty()
329
+ showsAudioTracksButton = player.audioTracks.availableAudioTracks.size > 1
330
+ playerView.setShowSubtitleButton(buttonOptions.showSubtitles ?: currentTrackHasSubtitles)
331
+ super.onTracksChanged(player, tracks)
332
+ }
333
+
334
+ override fun onRenderedFirstFrame(player: VideoPlayer) {
335
+ if (player.currentVideoView == this) {
336
+ shouldHideSurfaceView = false
337
+ applySurfaceViewVisibility()
338
+ onFirstFrameRender(Unit)
339
+ }
340
+ }
341
+
342
+ override fun requestLayout() {
343
+ super.requestLayout()
344
+
345
+ // Code borrowed from:
346
+ // https://github.com/facebook/react-native/blob/d19afc73f5048f81656d0b4424232ce6d69a6368/ReactAndroid/src/main/java/com/facebook/react/views/toolbar/ReactToolbar.java#L166
347
+ // This fixes some layout issues with the exoplayer which caused the resizeMode to not work properly
348
+ post(mLayoutRunnable)
349
+ }
350
+
351
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
352
+ super.onLayout(changed, l, t, r, b)
353
+ // On every re-layout ExoPlayer resets the timeBar to be enabled.
354
+ // We need to disable it to keep scrubbing impossible.
355
+
356
+ pipParams = pipParams.copy(rectHint = calculateRectHint(playerView))
357
+ playerView.setTimeBarInteractive(videoPlayer?.requiresLinearPlayback ?: true)
358
+ }
359
+
360
+ override fun onAttachedToWindow() {
361
+ super.onAttachedToWindow()
362
+
363
+ // Set up listener for accessibility caption changes when attached to window
364
+ setupCaptioningChangeListener()
365
+ // Reconfigure when view is attached (handles returning from settings)
366
+ SubtitleUtils.configureSubtitleView(playerView, context, subtitleStyle)
367
+
368
+ // Set up window focus change listener
369
+ decorView.onFocusChangeListener = windowFocusChangeListener
370
+
371
+ pipParams = pipParams.copy(canEnter = true)
372
+ }
373
+
374
+ override fun onVisibilityChanged(changedView: View, visibility: Int) {
375
+ super.onVisibilityChanged(changedView, visibility)
376
+ if (visibility == View.VISIBLE) {
377
+ // Reconfigure subtitles when view becomes visible (immediate response)
378
+ SubtitleUtils.configureSubtitleView(playerView, context, subtitleStyle)
379
+ }
380
+ }
381
+
382
+ override fun onDetachedFromWindow() {
383
+ super.onDetachedFromWindow()
384
+
385
+ // Clean up captioning change listener
386
+ captioningChangeListener?.let {
387
+ val captioningManager = context.getSystemService(Context.CAPTIONING_SERVICE) as? CaptioningManager
388
+ captioningManager?.removeCaptioningChangeListener(it)
389
+ captioningChangeListener = null
390
+ }
391
+
392
+ // Clean up window focus listener
393
+ decorView.onFocusChangeListener = null
394
+
395
+ pipParams = pipParams.copy(canEnter = false)
396
+ }
397
+
398
+ // After adding the `PlayerView` to the hierarchy the touch events stop being emitted to the JS side.
399
+ // The only workaround I have found is to dispatch the touch events manually using the `EventDispatcher`.
400
+ // The behavior is different when the native controls are enabled and disabled.
401
+ override fun onTouchEvent(event: MotionEvent?): Boolean {
402
+ if (!useNativeControls) {
403
+ event?.eventTime?.let {
404
+ touchEventCoalescingKeyHelper.addCoalescingKey(it)
405
+ reactNativeEventDispatcher?.dispatchMotionEvent(this@VideoView, event, touchEventCoalescingKeyHelper)
406
+ }
407
+ }
408
+ if (event?.actionMasked == MotionEvent.ACTION_UP) {
409
+ performClick()
410
+ }
411
+ // Mark the event as handled
412
+ return true
413
+ }
414
+
415
+ override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
416
+ if (useNativeControls) {
417
+ event?.eventTime?.let {
418
+ touchEventCoalescingKeyHelper.addCoalescingKey(it)
419
+ reactNativeEventDispatcher?.dispatchMotionEvent(this@VideoView, MotionEvent.obtainNoHistory(event), touchEventCoalescingKeyHelper)
420
+ }
421
+ }
422
+ // Return false to receive all other events before the target `onTouchEvent`
423
+ return false
424
+ }
425
+
426
+ private fun getPlayerViewLayoutId(useTextureView: Boolean): Int {
427
+ return if (useTextureView) {
428
+ R.layout.texture_player_view
429
+ } else {
430
+ R.layout.surface_player_view
431
+ }
432
+ }
433
+
434
+ private fun setupCaptioningChangeListener() {
435
+ val captioningManager = context.getSystemService(Context.CAPTIONING_SERVICE) as? CaptioningManager
436
+
437
+ captioningChangeListener = SubtitleUtils.createCaptioningChangeListener(playerView, context) { subtitleStyle }
438
+
439
+ captioningChangeListener?.let { listener ->
440
+ captioningManager?.addCaptioningChangeListener(listener)
441
+ }
442
+ }
443
+
444
+ private fun applyButtonSettings() {
445
+ val shouldShowSubtitle = buttonOptions.showSubtitles ?: currentTrackHasSubtitles
446
+ playerView.applyButtonOptions(buttonOptions, requiresLinearPlayback)
447
+ playerView.setShowSubtitleButton(shouldShowSubtitle)
448
+ }
449
+
450
+ companion object {
451
+ fun isPictureInPictureSupported(currentActivity: Activity): Boolean {
452
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && currentActivity.packageManager.hasSystemFeature(
453
+ android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE
454
+ )
455
+ }
456
+ }
457
+ }
@@ -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?.equals(propertyMapper(value)) ?: false) 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
+ }