@stepincto/expo-video 2.2.2-sicto.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 +41 -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 +13 -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 +35 -0
  71. package/build/VideoModule.d.ts.map +1 -0
  72. package/build/VideoModule.js +44 -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 +46 -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 +142 -0
  124. package/ios/Cache/ResourceLoaderDelegate.swift +274 -0
  125. package/ios/Cache/SynchronizedHashTable.swift +23 -0
  126. package/ios/Cache/VideoCacheManager.swift +192 -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 +357 -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 +58 -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 +16 -0
  166. package/src/NativeVideoModule.web.ts +1 -0
  167. package/src/NativeVideoView.ts +8 -0
  168. package/src/VideoModule.ts +47 -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 +43 -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
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ <p>
2
+ <a href="https://docs.expo.dev/versions/unversioned/sdk/video/">
3
+ <img
4
+ src="../../.github/resources/expo-video.svg"
5
+ alt="expo-video"
6
+ height="64" />
7
+ </a>
8
+ </p>
9
+
10
+ A cross-platform, performant video component for React Native and Expo with Web support
11
+
12
+ # API documentation
13
+
14
+ - [Documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/video/)
15
+ - [Documentation for the main branch](https://docs.expo.dev/versions/unversioned/sdk/video/)
16
+
17
+ # Installation in managed Expo projects
18
+
19
+ For [managed](https://docs.expo.dev/archive/managed-vs-bare/) Expo projects, please follow the installation instructions in the [API documentation for the latest stable release](#api-documentation). If you follow the link and there is no documentation available then this library is not yet usable within managed projects &mdash; it is likely to be included in an upcoming Expo SDK release.
20
+
21
+ # Installation in bare React Native projects
22
+
23
+ For bare React Native projects, you must ensure that you have [installed and configured the `expo` package](https://docs.expo.dev/bare/installing-expo-modules/) before continuing.
24
+
25
+ ### Add the package to your npm dependencies
26
+
27
+ ```
28
+ npm install expo-video
29
+ ```
30
+
31
+ # Contributing
32
+
33
+ Contributions are very welcome! Please refer to guidelines described in the [contributing guide](https://github.com/expo/expo#contributing).
34
+
35
+
36
+ # building with npm
37
+ ```bash
38
+ npm run build:plugin # should succeed now
39
+ npm run build:once # or run build:lib then build:plugin
40
+ npm run pack:local
41
+ ```
@@ -0,0 +1,32 @@
1
+ plugins {
2
+ id 'com.android.library'
3
+ id 'expo-module-gradle-plugin'
4
+ }
5
+
6
+ group = 'host.exp.exponent'
7
+ version = '2.2.2'
8
+
9
+ android {
10
+ namespace "expo.modules.video"
11
+ defaultConfig {
12
+ versionCode 1
13
+ versionName '2.2.2'
14
+ }
15
+ }
16
+
17
+ dependencies {
18
+ implementation 'com.facebook.react:react-android'
19
+
20
+ // Remember to keep this in sync with the version in `expo-audio`
21
+ def androidxMedia3Version = "1.4.0"
22
+ implementation "androidx.media3:media3-session:${androidxMedia3Version}"
23
+ implementation "androidx.media3:media3-exoplayer:${androidxMedia3Version}"
24
+ implementation "androidx.media3:media3-exoplayer-dash:${androidxMedia3Version}"
25
+ implementation "androidx.media3:media3-exoplayer-hls:${androidxMedia3Version}"
26
+ implementation "androidx.media3:media3-ui:${androidxMedia3Version}"
27
+ implementation "androidx.media3:media3-datasource-okhttp:${androidxMedia3Version}"
28
+
29
+ def fragment_version = "1.6.2"
30
+ implementation "androidx.fragment:fragment:$fragment_version"
31
+ implementation "androidx.fragment:fragment-ktx:$fragment_version"
32
+ }
@@ -0,0 +1,20 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.INTERNET" />
3
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
4
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
5
+
6
+ <application>
7
+ <activity android:name=".FullscreenPlayerActivity"
8
+ android:supportsPictureInPicture="true"
9
+ android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
10
+ android:theme="@style/Fullscreen"/>
11
+ <service
12
+ android:name=".playbackService.ExpoVideoPlaybackService"
13
+ android:exported="false"
14
+ android:foregroundServiceType="mediaPlayback">
15
+ <intent-filter>
16
+ <action android:name="androidx.media3.session.MediaSessionService" />
17
+ </intent-filter>
18
+ </service>
19
+ </application>
20
+ </manifest>
@@ -0,0 +1,241 @@
1
+ package expo.modules.video
2
+
3
+ import android.content.Context
4
+ import android.media.AudioAttributes
5
+ import android.media.AudioFocusRequest
6
+ import android.media.AudioManager
7
+ import android.os.Build
8
+ import androidx.media3.common.util.UnstableApi
9
+ import expo.modules.kotlin.AppContext
10
+ import expo.modules.video.enums.AudioMixingMode
11
+ import expo.modules.video.player.VideoPlayer
12
+ import expo.modules.video.player.VideoPlayerListener
13
+ import kotlinx.coroutines.launch
14
+ import java.lang.ref.WeakReference
15
+
16
+ @UnstableApi
17
+ class AudioFocusManager(private val appContext: AppContext) : AudioManager.OnAudioFocusChangeListener, VideoPlayerListener {
18
+ private val audioManager by lazy {
19
+ appContext.reactContext?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager ?: run {
20
+ throw FailedToGetAudioFocusManagerException()
21
+ }
22
+ }
23
+
24
+ private var players: MutableList<WeakReference<VideoPlayer>> = mutableListOf()
25
+ private var currentFocusRequest: AudioFocusRequest? = null
26
+ private var currentMixingMode: AudioMixingMode = AudioMixingMode.MIX_WITH_OTHERS
27
+ private val anyPlayerRequiresFocus: Boolean
28
+ get() = players.toList().any {
29
+ playerRequiresFocus(it)
30
+ }
31
+
32
+ private fun requestAudioFocus() {
33
+ val audioMixingMode = findAudioMixingMode()
34
+
35
+ // We don't request AudioFocus if we want to mix the audio with others
36
+ if (audioMixingMode == AudioMixingMode.MIX_WITH_OTHERS || !anyPlayerRequiresFocus) {
37
+ abandonAudioFocus()
38
+ currentMixingMode = audioMixingMode
39
+ return
40
+ }
41
+ val audioFocusType = when (audioMixingMode) {
42
+ AudioMixingMode.DUCK_OTHERS -> AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
43
+ AudioMixingMode.AUTO -> AudioManager.AUDIOFOCUS_GAIN
44
+ AudioMixingMode.DO_NOT_MIX -> AudioManager.AUDIOFOCUS_GAIN
45
+ else -> AudioManager.AUDIOFOCUS_GAIN
46
+ }
47
+
48
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
49
+ // We already have audio focus
50
+ currentFocusRequest?.let {
51
+ if (it.focusGain == audioFocusType) {
52
+ return
53
+ }
54
+ }
55
+
56
+ val newFocusRequest = AudioFocusRequest.Builder(audioFocusType).run {
57
+ setAudioAttributes(
58
+ AudioAttributes.Builder().run {
59
+ setUsage(AudioAttributes.USAGE_MEDIA)
60
+ setContentType(AudioAttributes.CONTENT_TYPE_MOVIE)
61
+ setOnAudioFocusChangeListener(this@AudioFocusManager)
62
+ build()
63
+ }
64
+ ).build()
65
+ }
66
+ currentFocusRequest = newFocusRequest
67
+ audioManager.requestAudioFocus(newFocusRequest)
68
+ } else {
69
+ @Suppress("DEPRECATION")
70
+ audioManager.requestAudioFocus(
71
+ this,
72
+ AudioManager.STREAM_MUSIC,
73
+ audioFocusType
74
+ )
75
+ }
76
+ currentMixingMode = audioMixingMode
77
+ }
78
+
79
+ private fun abandonAudioFocus() {
80
+ currentFocusRequest?.let {
81
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
82
+ audioManager.abandonAudioFocusRequest(it)
83
+ } else {
84
+ @Suppress("DEPRECATION")
85
+ audioManager.abandonAudioFocus(this)
86
+ }
87
+ }
88
+ currentFocusRequest = null
89
+ }
90
+
91
+ fun registerPlayer(player: VideoPlayer) {
92
+ players.find { it.get() == player } ?: run {
93
+ players.add(WeakReference(player))
94
+ }
95
+ player.addListener(this)
96
+ updateAudioFocus()
97
+ }
98
+
99
+ fun unregisterPlayer(player: VideoPlayer) {
100
+ player.removeListener(this)
101
+ players.removeAll { it.get() == player }
102
+ updateAudioFocus()
103
+ }
104
+
105
+ // VideoPlayerListener
106
+
107
+ override fun onAudioMixingModeChanged(player: VideoPlayer, audioMixingMode: AudioMixingMode, oldAudioMixingMode: AudioMixingMode?) {
108
+ requestAudioFocus()
109
+ super.onAudioMixingModeChanged(player, audioMixingMode, oldAudioMixingMode)
110
+ }
111
+
112
+ override fun onIsPlayingChanged(player: VideoPlayer, isPlaying: Boolean, oldIsPlaying: Boolean?) {
113
+ // we can't use `updateAudioFocus`, because when losing focus the videos are paused sequentially,
114
+ // which can lead to unexpected results.
115
+ if (!isPlaying && !anyPlayerRequiresFocus) {
116
+ abandonAudioFocus()
117
+ } else if (isPlaying && anyPlayerRequiresFocus) {
118
+ requestAudioFocus()
119
+ }
120
+ }
121
+
122
+ override fun onVolumeChanged(player: VideoPlayer, volume: Float, oldVolume: Float?) {
123
+ updateAudioFocus()
124
+ }
125
+
126
+ override fun onMutedChanged(player: VideoPlayer, muted: Boolean, oldMuted: Boolean?) {
127
+ updateAudioFocus()
128
+ }
129
+
130
+ // AudioManager.OnAudioFocusChangeListener
131
+
132
+ override fun onAudioFocusChange(focusChange: Int) {
133
+ when (focusChange) {
134
+ AudioManager.AUDIOFOCUS_LOSS -> {
135
+ appContext.mainQueue.launch {
136
+ players.forEach {
137
+ pausePlayerIfUnmuted(it)
138
+ }
139
+ currentFocusRequest = null
140
+ }
141
+ }
142
+
143
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
144
+ // W could pause/mix the players here individually, but we will keep the behaviour in line with iOS,
145
+ // find the dominant audioMixingMode and apply it to all players.
146
+ val audioMixingMode = findAudioMixingMode()
147
+ if (audioMixingMode == AudioMixingMode.MIX_WITH_OTHERS) {
148
+ return
149
+ }
150
+ appContext.mainQueue.launch {
151
+ players.forEach {
152
+ pausePlayerIfUnmuted(it)
153
+ }
154
+ currentFocusRequest = null
155
+ }
156
+ }
157
+
158
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
159
+ val audioMixingMode = findAudioMixingMode()
160
+
161
+ appContext.mainQueue.launch {
162
+ players.forEach {
163
+ if (audioMixingMode == AudioMixingMode.DO_NOT_MIX) {
164
+ pausePlayerIfUnmuted(it)
165
+ } else {
166
+ duckPlayer(it)
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ AudioManager.AUDIOFOCUS_GAIN -> {
173
+ // TODO: For now this behaves like iOS and doesn't resume playback automatically
174
+ // In future versions we can add a prop to control this behavior.
175
+ appContext.mainQueue.launch {
176
+ players.forEach {
177
+ unduckPlayer(it)
178
+ }
179
+ }
180
+ }
181
+ }
182
+ }
183
+
184
+ // Utils
185
+
186
+ private fun playerRequiresFocus(weakPlayer: WeakReference<VideoPlayer>): Boolean {
187
+ val player = weakPlayer?.get() ?: return false // Return false if player is null
188
+ return (!player.muted && player.playing && player.volume > 0) || player.audioMixingMode == AudioMixingMode.DO_NOT_MIX
189
+ }
190
+
191
+ private fun pausePlayerIfUnmuted(weakPlayer: WeakReference<VideoPlayer>) {
192
+ weakPlayer.get()?.let { videoPlayer ->
193
+ if (!videoPlayer.muted) {
194
+ appContext.mainQueue.launch {
195
+ videoPlayer.player.pause()
196
+ }
197
+ }
198
+ }
199
+ }
200
+
201
+ private fun duckPlayer(weakPlayer: WeakReference<VideoPlayer>) {
202
+ weakPlayer.get()?.let { player ->
203
+ appContext.mainQueue.launch {
204
+ player.volume /= 2f
205
+ }
206
+ }
207
+ }
208
+
209
+ private fun unduckPlayer(weakPlayer: WeakReference<VideoPlayer>) {
210
+ weakPlayer.get()?.let { player ->
211
+ if (!player.muted) {
212
+ appContext.mainQueue.launch {
213
+ player.volume = player.userVolume
214
+ }
215
+ }
216
+ }
217
+ }
218
+
219
+ private fun updateAudioFocus() {
220
+ if (anyPlayerRequiresFocus || findAudioMixingMode() != currentMixingMode) {
221
+ requestAudioFocus()
222
+ } else {
223
+ abandonAudioFocus()
224
+ }
225
+ }
226
+
227
+ private fun findAudioMixingMode(): AudioMixingMode {
228
+ val playersSnapshot = players.toList()
229
+
230
+ val mixingModes = playersSnapshot.mapNotNull { player ->
231
+ player.get()?.takeIf { it.playing }?.audioMixingMode
232
+ }
233
+ if (mixingModes.isEmpty()) {
234
+ return AudioMixingMode.MIX_WITH_OTHERS
235
+ }
236
+
237
+ return mixingModes.reduce { currentAudioMixingMode, next ->
238
+ next.takeIf { it.priority > currentAudioMixingMode.priority } ?: currentAudioMixingMode
239
+ }
240
+ }
241
+ }
@@ -0,0 +1,145 @@
1
+ package expo.modules.video
2
+
3
+ import android.app.Activity
4
+ import android.content.res.Configuration
5
+ import android.os.Build
6
+ import android.os.Bundle
7
+ import android.util.Log
8
+ import android.view.View
9
+ import android.view.WindowInsets
10
+ import android.view.WindowInsetsController
11
+ import android.widget.ImageButton
12
+ import androidx.media3.ui.PlayerView
13
+ import expo.modules.kotlin.exception.CodedException
14
+ import expo.modules.video.player.VideoPlayer
15
+ import expo.modules.video.utils.applyPiPParams
16
+ import expo.modules.video.utils.applyRectHint
17
+ import expo.modules.video.utils.calculatePiPAspectRatio
18
+ import expo.modules.video.utils.calculateRectHint
19
+
20
+ @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
21
+ class FullscreenPlayerActivity : Activity() {
22
+ private lateinit var mContentView: View
23
+ private lateinit var videoViewId: String
24
+ private var videoPlayer: VideoPlayer? = null
25
+ private lateinit var playerView: PlayerView
26
+ private lateinit var videoView: VideoView
27
+ private var didFinish = false
28
+ private var wasAutoPaused = false
29
+
30
+ override fun onCreate(savedInstanceState: Bundle?) {
31
+ super.onCreate(savedInstanceState)
32
+ setContentView(R.layout.fullscreen_player_activity)
33
+ mContentView = findViewById(R.id.enclosing_layout)
34
+ playerView = findViewById(R.id.player_view)
35
+
36
+ try {
37
+ videoViewId = intent.getStringExtra(VideoManager.INTENT_PLAYER_KEY)
38
+ ?: throw FullScreenVideoViewNotFoundException()
39
+ videoView = VideoManager.getVideoView(videoViewId)
40
+ } catch (e: CodedException) {
41
+ Log.e("ExpoVideo", "${e.message}", e)
42
+ finish()
43
+ return
44
+ }
45
+ videoPlayer = videoView.videoPlayer
46
+ videoPlayer?.changePlayerView(playerView)
47
+ VideoManager.registerFullscreenPlayerActivity(hashCode().toString(), this)
48
+ playerView.player?.let {
49
+ val aspectRatio = calculatePiPAspectRatio(it.videoSize, playerView.width, playerView.height, videoView.contentFit)
50
+ applyPiPParams(this, videoView.autoEnterPiP, aspectRatio)
51
+ }
52
+ }
53
+
54
+ override fun onPostCreate(savedInstanceState: Bundle?) {
55
+ super.onPostCreate(savedInstanceState)
56
+ hideStatusBar()
57
+ setupFullscreenButton()
58
+ playerView.applyRequiresLinearPlayback(videoPlayer?.requiresLinearPlayback ?: false)
59
+ playerView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
60
+ // On every re-layout ExoPlayer makes the timeBar interactive.
61
+ // We need to disable it to keep scrubbing off.
62
+ playerView.setTimeBarInteractive(videoPlayer?.requiresLinearPlayback ?: true)
63
+ }
64
+ playerView.setShowSubtitleButton(videoView.showsSubtitlesButton)
65
+
66
+ playerView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
67
+ applyRectHint(this, calculateRectHint(playerView))
68
+ }
69
+ }
70
+
71
+ override fun finish() {
72
+ super.finish()
73
+ didFinish = true
74
+ VideoManager.getVideoView(videoViewId).attachPlayer()
75
+
76
+ // Disable the exit transition
77
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
78
+ overrideActivityTransition(OVERRIDE_TRANSITION_CLOSE, 0, 0)
79
+ } else {
80
+ @Suppress("DEPRECATION")
81
+ overridePendingTransition(0, 0)
82
+ }
83
+ }
84
+
85
+ override fun onResume() {
86
+ playerView.useController = videoView.useNativeControls
87
+ super.onResume()
88
+ }
89
+
90
+ override fun onPause() {
91
+ if (videoPlayer?.staysActiveInBackground != true && !didFinish) {
92
+ wasAutoPaused = videoPlayer?.player?.isPlaying == true
93
+ if (wasAutoPaused) {
94
+ playerView.useController = false
95
+ videoPlayer?.player?.pause()
96
+ }
97
+ }
98
+ super.onPause()
99
+ }
100
+
101
+ override fun onDestroy() {
102
+ super.onDestroy()
103
+ videoView.exitFullscreen()
104
+ VideoManager.unregisterFullscreenPlayerActivity(hashCode().toString())
105
+ }
106
+
107
+ private fun setupFullscreenButton() {
108
+ playerView.setFullscreenButtonClickListener { finish() }
109
+
110
+ val fullScreenButton: ImageButton = playerView.findViewById(androidx.media3.ui.R.id.exo_fullscreen)
111
+ fullScreenButton.setImageResource(androidx.media3.ui.R.drawable.exo_icon_fullscreen_exit)
112
+ }
113
+
114
+ override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration?) {
115
+ if (!isInPictureInPictureMode) {
116
+ playerView.useController = videoView.useNativeControls
117
+ } else {
118
+ playerView.useController = false
119
+ }
120
+ if (wasAutoPaused && isInPictureInPictureMode) {
121
+ videoPlayer?.player?.play()
122
+ }
123
+ super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
124
+ }
125
+
126
+ private fun hideStatusBar() {
127
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
128
+ val controller = mContentView.windowInsetsController
129
+ controller?.apply {
130
+ systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
131
+ hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())
132
+ }
133
+ } else {
134
+ @Suppress("DEPRECATION")
135
+ mContentView.systemUiVisibility = (
136
+ View.SYSTEM_UI_FLAG_LOW_PROFILE
137
+ or View.SYSTEM_UI_FLAG_FULLSCREEN
138
+ or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
139
+ or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
140
+ or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
141
+ or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
142
+ )
143
+ }
144
+ }
145
+ }
@@ -0,0 +1,54 @@
1
+ package expo.modules.video
2
+
3
+ import android.os.Handler
4
+ import android.os.Looper
5
+ import expo.modules.video.delegates.IgnoreSameSet
6
+ import java.lang.ref.WeakReference
7
+
8
+ fun interface IntervalUpdateEmitter {
9
+ fun emitTimeUpdate()
10
+ }
11
+
12
+ class IntervalUpdateClock(emitter: IntervalUpdateEmitter) {
13
+ private val emitter: WeakReference<IntervalUpdateEmitter> = WeakReference(emitter)
14
+ private var handler: Handler = Handler(Looper.getMainLooper())
15
+
16
+ // TODO: @behenate - Once the Player instance is available in OnStartObserving we can automatically start/stop the clock.
17
+ var interval by IgnoreSameSet(0L) { new: Long, _: Long? ->
18
+ if (new <= 0) {
19
+ stop()
20
+ } else {
21
+ startOrUpdate()
22
+ }
23
+ }
24
+
25
+ private var isRunning: Boolean = false
26
+
27
+ private fun stop() {
28
+ handler.removeCallbacksAndMessages(null)
29
+ isRunning = false
30
+ }
31
+
32
+ private fun startOrUpdate() {
33
+ if (!isRunning) {
34
+ emitter.get()?.emitTimeUpdate()
35
+ } else {
36
+ handler.removeCallbacksAndMessages(null)
37
+ }
38
+ isRunning = true
39
+ scheduleNextUpdate()
40
+ }
41
+
42
+ private fun scheduleNextUpdate() {
43
+ if (interval <= 0L) {
44
+ return
45
+ }
46
+
47
+ val update = {
48
+ emitter.get()?.emitTimeUpdate()
49
+ scheduleNextUpdate()
50
+ }
51
+
52
+ handler.postDelayed(update, interval)
53
+ }
54
+ }
@@ -0,0 +1,89 @@
1
+ package expo.modules.video
2
+
3
+ import android.graphics.Bitmap
4
+ import android.media.MediaMetadataRetriever
5
+ import android.os.Build
6
+ import expo.modules.video.records.VideoThumbnailOptions
7
+ import kotlin.math.max
8
+ import kotlin.math.roundToLong
9
+ import kotlin.time.Duration
10
+ import kotlin.time.DurationUnit
11
+ import kotlin.time.toDuration
12
+
13
+ suspend fun <T> MediaMetadataRetriever.safeUse(block: suspend MediaMetadataRetriever.() -> T): T {
14
+ try {
15
+ return block()
16
+ } finally {
17
+ try {
18
+ this.close()
19
+ } finally {
20
+ // ignore
21
+ }
22
+ }
23
+ }
24
+
25
+ fun MediaMetadataRetriever.generateThumbnailAtTime(
26
+ requestedTime: Duration,
27
+ options: VideoThumbnailOptions? = null
28
+ ): VideoThumbnail {
29
+ val sizeLimit = options?.toNativeSizeLimit()
30
+
31
+ val bitmap = if (sizeLimit != null) {
32
+ val (maxWidth, maxHeight) = sizeLimit
33
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
34
+ getScaledFrameAtTime(
35
+ requestedTime.inWholeMicroseconds,
36
+ MediaMetadataRetriever.OPTION_CLOSEST,
37
+ maxWidth,
38
+ maxHeight
39
+ )
40
+ } else {
41
+ getFrameAtTime(requestedTime.inWholeMicroseconds, MediaMetadataRetriever.OPTION_CLOSEST)
42
+ ?.constrainToDimensions(maxWidth, maxHeight)
43
+ }
44
+ } else {
45
+ getFrameAtTime(requestedTime.inWholeMicroseconds, MediaMetadataRetriever.OPTION_CLOSEST)
46
+ } ?: throw IllegalStateException("Failed to generate thumbnail")
47
+
48
+ val actualTime = calculateActualFrameTime(this, requestedTime)
49
+ return VideoThumbnail(bitmap, requestedTime, actualTime)
50
+ }
51
+
52
+ private fun calculateActualFrameTime(mediaMetadataRetriever: MediaMetadataRetriever, time: Duration): Duration {
53
+ // if we can't get the frame rate, we are returning the requested time
54
+ val frameTime = mediaMetadataRetriever.frameTime() ?: return time
55
+
56
+ // calculate closest frame index
57
+ val frameIndex = (time.inWholeMicroseconds.toDouble() / frameTime).roundToLong()
58
+
59
+ return (frameIndex * frameTime).toDuration(DurationUnit.MICROSECONDS)
60
+ }
61
+
62
+ private fun MediaMetadataRetriever.frameTime(): Double? {
63
+ // frame count is not available on Android SDK < 28
64
+ if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.P) {
65
+ return null
66
+ }
67
+
68
+ val duration = this.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toDouble()
69
+ ?: return null
70
+ val frameCount = this.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_FRAME_COUNT)?.toDouble()
71
+ ?: return null
72
+
73
+ return (duration * 1000) / frameCount
74
+ }
75
+
76
+ private fun Bitmap.constrainToDimensions(maxWidth: Int, maxHeight: Int): Bitmap {
77
+ val width = this.width
78
+ val height = this.height
79
+
80
+ val ratio = max(width / maxWidth.toFloat(), height / maxHeight.toFloat())
81
+ if (ratio <= 1) {
82
+ return this
83
+ }
84
+
85
+ val newWidth = (width / ratio).toInt()
86
+ val newHeight = (height / ratio).toInt()
87
+
88
+ return Bitmap.createScaledBitmap(this, newWidth, newHeight, true)
89
+ }
@@ -0,0 +1,26 @@
1
+ package expo.modules.video
2
+
3
+ import androidx.fragment.app.Fragment
4
+ import java.util.UUID
5
+
6
+ class PictureInPictureHelperFragment(private val videoView: VideoView) : Fragment() {
7
+ val id = "${PictureInPictureHelperFragment::class.java.simpleName}_${UUID.randomUUID()}"
8
+
9
+ override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
10
+ super.onPictureInPictureModeChanged(isInPictureInPictureMode)
11
+
12
+ if (isInPictureInPictureMode) {
13
+ // We can't reliably detect when the PiP transition starts (while keeping the transition smooth 🙄), so we have to
14
+ // unpause the playback after the onPause event, is called right after onPause. So the pause is not noticeable
15
+ if (videoView.wasAutoPaused) {
16
+ videoView.playerView.player?.play()
17
+ }
18
+ videoView.layoutForPiPEnter()
19
+ videoView.onPictureInPictureStart(Unit)
20
+ } else {
21
+ videoView.willEnterPiP = false
22
+ videoView.layoutForPiPExit()
23
+ videoView.onPictureInPictureStop(Unit)
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,36 @@
1
+ package expo.modules.video
2
+
3
+ import android.graphics.Color
4
+ import androidx.media3.ui.DefaultTimeBar
5
+ import androidx.media3.ui.PlayerView
6
+
7
+ @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
8
+ internal fun PlayerView.applyRequiresLinearPlayback(requireLinearPlayback: Boolean) {
9
+ setShowFastForwardButton(!requireLinearPlayback)
10
+ setShowRewindButton(!requireLinearPlayback)
11
+ setShowPreviousButton(!requireLinearPlayback)
12
+ setShowNextButton(!requireLinearPlayback)
13
+ setTimeBarInteractive(requireLinearPlayback)
14
+ }
15
+
16
+ @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
17
+ internal fun PlayerView.setTimeBarInteractive(interactive: Boolean) {
18
+ val timeBar = findViewById<DefaultTimeBar>(androidx.media3.ui.R.id.exo_progress)
19
+ if (interactive) {
20
+ timeBar?.setScrubberColor(Color.TRANSPARENT)
21
+ timeBar?.isEnabled = false
22
+ } else {
23
+ timeBar?.setScrubberColor(Color.WHITE)
24
+ timeBar?.isEnabled = true
25
+ }
26
+ }
27
+
28
+ @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
29
+ internal fun PlayerView.setFullscreenButtonVisibility(visible: Boolean) {
30
+ val fullscreenButton = findViewById<android.widget.ImageButton>(androidx.media3.ui.R.id.exo_fullscreen)
31
+ fullscreenButton?.visibility = if (visible) {
32
+ android.view.View.VISIBLE
33
+ } else {
34
+ android.view.View.GONE
35
+ }
36
+ }