@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,10 @@
1
+ package expo.modules.video.enums
2
+
3
+ import expo.modules.kotlin.types.Enumerable
4
+
5
+ enum class PlayerStatus(val value: String) : Enumerable {
6
+ IDLE("idle"),
7
+ LOADING("loading"),
8
+ READY_TO_PLAY("readyToPlay"),
9
+ ERROR("error")
10
+ }
@@ -0,0 +1,184 @@
1
+ package expo.modules.video.playbackService
2
+
3
+ import android.app.NotificationChannel
4
+ import android.app.NotificationManager
5
+ import android.content.Context
6
+ import android.content.Intent
7
+ import android.os.Binder
8
+ import android.os.Build
9
+ import android.os.Bundle
10
+ import android.os.IBinder
11
+ import androidx.annotation.OptIn
12
+ import androidx.core.app.NotificationCompat
13
+ import androidx.media3.common.util.UnstableApi
14
+ import androidx.media3.exoplayer.ExoPlayer
15
+ import androidx.media3.session.CommandButton
16
+ import androidx.media3.session.MediaSession
17
+ import androidx.media3.session.MediaSessionService
18
+ import androidx.media3.session.MediaStyleNotificationHelper
19
+ import androidx.media3.session.SessionCommand
20
+ import com.google.common.collect.ImmutableList
21
+ import expo.modules.kotlin.AppContext
22
+ import expo.modules.video.R
23
+ import expo.modules.video.player.VideoPlayer
24
+
25
+ class PlaybackServiceBinder(val service: ExpoVideoPlaybackService) : Binder()
26
+
27
+ @OptIn(UnstableApi::class)
28
+ class ExpoVideoPlaybackService : MediaSessionService() {
29
+ private val mediaSessions = mutableMapOf<ExoPlayer, MediaSession>()
30
+ private val binder = PlaybackServiceBinder(this)
31
+
32
+ private val commandSeekForward = SessionCommand(SEEK_FORWARD_COMMAND, Bundle.EMPTY)
33
+ private val commandSeekBackward = SessionCommand(SEEK_BACKWARD_COMMAND, Bundle.EMPTY)
34
+ private val seekForwardButton = CommandButton.Builder()
35
+ .setDisplayName("rewind")
36
+ .setSessionCommand(commandSeekForward)
37
+ .setIconResId(R.drawable.seek_forwards_10s)
38
+ .build()
39
+
40
+ private val seekBackwardButton = CommandButton.Builder()
41
+ .setDisplayName("forward")
42
+ .setSessionCommand(commandSeekBackward)
43
+ .setIconResId(R.drawable.seek_backwards_10s)
44
+ .build()
45
+
46
+ fun setShowNotification(showNotification: Boolean, player: ExoPlayer) {
47
+ val sessionExtras = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
48
+ mediaSessions[player]?.sessionExtras?.deepCopy() ?: Bundle()
49
+ } else {
50
+ Bundle()
51
+ }
52
+ sessionExtras.putBoolean(SESSION_SHOW_NOTIFICATION, showNotification)
53
+ mediaSessions[player]?.let {
54
+ it.sessionExtras = sessionExtras
55
+ onUpdateNotification(it, showNotification)
56
+ }
57
+ }
58
+
59
+ fun registerPlayer(videoPlayer: VideoPlayer) {
60
+ val player = videoPlayer.player
61
+ if (mediaSessions[player] != null) {
62
+ return
63
+ }
64
+
65
+ val mediaSession = MediaSession.Builder(this, player)
66
+ .setId("ExpoVideoPlaybackService_${player.hashCode()}")
67
+ .setCallback(VideoMediaSessionCallback())
68
+ .setCustomLayout(ImmutableList.of(seekBackwardButton, seekForwardButton))
69
+ .build()
70
+
71
+ mediaSessions[player] = mediaSession
72
+ addSession(mediaSession)
73
+ setShowNotification(videoPlayer.showNowPlayingNotification, player)
74
+ }
75
+
76
+ fun unregisterPlayer(player: ExoPlayer) {
77
+ hidePlayerNotification(player)
78
+ val session = mediaSessions.remove(player)
79
+ session?.release()
80
+ if (mediaSessions.isEmpty()) {
81
+ cleanup()
82
+ stopSelf()
83
+ }
84
+ }
85
+
86
+ override fun onBind(intent: Intent?): IBinder {
87
+ super.onBind(intent)
88
+ return binder
89
+ }
90
+
91
+ override fun onUpdateNotification(session: MediaSession, startInForegroundRequired: Boolean) {
92
+ if (session.sessionExtras.getBoolean(SESSION_SHOW_NOTIFICATION, false)) {
93
+ createNotification(session)
94
+ } else {
95
+ (session.player as? ExoPlayer)?.let {
96
+ hidePlayerNotification(it)
97
+ }
98
+ }
99
+ }
100
+
101
+ override fun onTaskRemoved(rootIntent: Intent?) {
102
+ cleanup()
103
+ stopSelf()
104
+ }
105
+
106
+ override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
107
+ return null
108
+ }
109
+
110
+ override fun onDestroy() {
111
+ cleanup()
112
+ super.onDestroy()
113
+ }
114
+
115
+ private fun createNotification(session: MediaSession) {
116
+ if (session.player.currentMediaItem == null) {
117
+ return
118
+ }
119
+
120
+ val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
121
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
122
+ notificationManager.createNotificationChannel(NotificationChannel(CHANNEL_ID, CHANNEL_ID, NotificationManager.IMPORTANCE_LOW))
123
+ }
124
+
125
+ // If the title is null android sets the notification to "<AppName> is running..." we want to keep the notification empty.
126
+ val contentTitle = session.player.currentMediaItem?.mediaMetadata?.title ?: "\u200E"
127
+ val notificationCompat = NotificationCompat.Builder(this, CHANNEL_ID)
128
+ .setSmallIcon(androidx.media3.session.R.drawable.media3_icon_circular_play)
129
+ .setContentTitle(contentTitle)
130
+ .setStyle(MediaStyleNotificationHelper.MediaStyle(session))
131
+ .build()
132
+
133
+ // Each of the players has it's own notification when playing.
134
+ notificationManager.notify(session.player.hashCode(), notificationCompat)
135
+ }
136
+
137
+ private fun cleanup() {
138
+ hideAllNotifications()
139
+
140
+ val sessionsToRelease = mediaSessions.values.toList()
141
+ mediaSessions.clear()
142
+ for (session in sessionsToRelease) {
143
+ session.release()
144
+ }
145
+ }
146
+
147
+ private fun hidePlayerNotification(player: ExoPlayer) {
148
+ val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
149
+ notificationManager.cancel(player.hashCode())
150
+ }
151
+
152
+ private fun hideAllNotifications() {
153
+ val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
154
+ notificationManager.cancelAll()
155
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
156
+ notificationManager.deleteNotificationChannel(CHANNEL_ID)
157
+ }
158
+ }
159
+
160
+ companion object {
161
+ const val SEEK_FORWARD_COMMAND = "SEEK_FORWARD"
162
+ const val SEEK_BACKWARD_COMMAND = "SEEK_REWIND"
163
+ const val CHANNEL_ID = "PlaybackService"
164
+ const val SESSION_SHOW_NOTIFICATION = "showNotification"
165
+ const val SEEK_INTERVAL_MS = 10000L
166
+
167
+ fun startService(appContext: AppContext, context: Context, serviceConnection: PlaybackServiceConnection) {
168
+ appContext.reactContext?.apply {
169
+ val intent = Intent(context, ExpoVideoPlaybackService::class.java)
170
+ intent.action = SERVICE_INTERFACE
171
+
172
+ startService(intent)
173
+
174
+ val flags = if (Build.VERSION.SDK_INT >= 29) {
175
+ BIND_AUTO_CREATE or Context.BIND_INCLUDE_CAPABILITIES
176
+ } else {
177
+ BIND_AUTO_CREATE
178
+ }
179
+
180
+ bindService(intent, serviceConnection, flags)
181
+ }
182
+ }
183
+ }
184
+ }
@@ -0,0 +1,39 @@
1
+ package expo.modules.video.playbackService
2
+
3
+ import android.content.ComponentName
4
+ import android.content.ServiceConnection
5
+ import android.os.IBinder
6
+ import android.util.Log
7
+ import androidx.annotation.OptIn
8
+ import androidx.media3.common.util.UnstableApi
9
+ import expo.modules.video.player.VideoPlayer
10
+ import java.lang.ref.WeakReference
11
+
12
+ @OptIn(UnstableApi::class)
13
+ class PlaybackServiceConnection(val player: WeakReference<VideoPlayer>) : ServiceConnection {
14
+ var playbackServiceBinder: PlaybackServiceBinder? = null
15
+
16
+ override fun onServiceConnected(componentName: ComponentName, binder: IBinder) {
17
+ val player = player.get() ?: return
18
+ playbackServiceBinder = binder as? PlaybackServiceBinder
19
+ playbackServiceBinder?.service?.registerPlayer(player) ?: run {
20
+ Log.w(
21
+ "ExpoVideo",
22
+ "Expo Video could not bind to the playback service. " +
23
+ "This will cause issues with playback notifications and sustaining background playback."
24
+ )
25
+ }
26
+ }
27
+
28
+ override fun onServiceDisconnected(componentName: ComponentName) {
29
+ playbackServiceBinder = null
30
+ }
31
+
32
+ override fun onNullBinding(componentName: ComponentName) {
33
+ Log.w(
34
+ "ExpoVideo",
35
+ "Expo Video could not bind to the playback service. " +
36
+ "This will cause issues with playback notifications and sustaining background playback."
37
+ )
38
+ }
39
+ }
@@ -0,0 +1,47 @@
1
+ package expo.modules.video.playbackService
2
+
3
+ import android.os.Bundle
4
+ import androidx.media3.common.Player
5
+ import androidx.media3.session.MediaSession
6
+ import androidx.media3.session.SessionCommand
7
+ import androidx.media3.session.SessionResult
8
+ import com.google.common.util.concurrent.ListenableFuture
9
+ import androidx.annotation.OptIn
10
+ import androidx.media3.common.util.UnstableApi
11
+
12
+ @OptIn(UnstableApi::class)
13
+ class VideoMediaSessionCallback : MediaSession.Callback {
14
+ override fun onConnect(
15
+ session: MediaSession,
16
+ controller: MediaSession.ControllerInfo
17
+ ): MediaSession.ConnectionResult {
18
+ try {
19
+ // TODO @behenate: For now we're only allowing seek forward and back by 10 seconds and going to the
20
+ // beginning of the video. In the future we should add more customization options for the users.
21
+ return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
22
+ .setAvailablePlayerCommands(
23
+ MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
24
+ .add(Player.COMMAND_SEEK_FORWARD)
25
+ .add(Player.COMMAND_SEEK_BACK)
26
+ .build()
27
+ )
28
+ .setAvailableSessionCommands(
29
+ MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
30
+ .add(SessionCommand(ExpoVideoPlaybackService.SEEK_BACKWARD_COMMAND, Bundle.EMPTY))
31
+ .add(SessionCommand(ExpoVideoPlaybackService.SEEK_FORWARD_COMMAND, Bundle.EMPTY))
32
+ .build()
33
+ )
34
+ .build()
35
+ } catch (e: Exception) {
36
+ return MediaSession.ConnectionResult.reject()
37
+ }
38
+ }
39
+
40
+ override fun onCustomCommand(session: MediaSession, controller: MediaSession.ControllerInfo, customCommand: SessionCommand, args: Bundle): ListenableFuture<SessionResult> {
41
+ when (customCommand.customAction) {
42
+ ExpoVideoPlaybackService.SEEK_FORWARD_COMMAND -> session.player.seekTo(session.player.currentPosition + ExpoVideoPlaybackService.SEEK_INTERVAL_MS)
43
+ ExpoVideoPlaybackService.SEEK_BACKWARD_COMMAND -> session.player.seekTo(session.player.currentPosition - ExpoVideoPlaybackService.SEEK_INTERVAL_MS)
44
+ }
45
+ return super.onCustomCommand(session, controller, customCommand, args)
46
+ }
47
+ }
@@ -0,0 +1,93 @@
1
+ package expo.modules.video.player
2
+
3
+ import androidx.annotation.OptIn
4
+ import androidx.media3.common.MediaItem
5
+ import androidx.media3.common.Player
6
+ import androidx.media3.common.util.UnstableApi
7
+ import androidx.media3.exoplayer.ExoPlayer
8
+ import androidx.media3.ui.PlayerView
9
+ import expo.modules.video.enums.ContentFit
10
+ import expo.modules.video.utils.MutableWeakReference
11
+ import java.lang.ref.WeakReference
12
+ import kotlin.math.abs
13
+
14
+ /**
15
+ * Workaround around the `onRenderedFirstFrame` and `SurfaceView` layout race condition bug.
16
+ * Ensures that the `onFirstFrame` event is sent after the `SurfaceView` is fully laid out.
17
+ * ExoPlayer sometimes emits `onRenderedFirstFrame` before the `SurfaceView` is laid out.
18
+ * If the frame is rendered before layout it becomes stretched to the size of the parent view.
19
+ * We want our event to be emitted only after we are sure that the frame is being displayed correctly.
20
+ * https://github.com/google/ExoPlayer/issues/5222
21
+ */
22
+ @OptIn(UnstableApi::class)
23
+ internal class FirstFrameEventGenerator(
24
+ player: ExoPlayer,
25
+ private val currentViewReference: MutableWeakReference<PlayerView?>,
26
+ private val onFirstFrameRendered: () -> Unit
27
+ ) : Player.Listener {
28
+ val playerReference = WeakReference(player)
29
+ private var hasPendingOnFirstFrame = false
30
+ private var hasSentFirstFrameForCurrentMediaItem = false
31
+
32
+ init {
33
+ player.addListener(this)
34
+ }
35
+
36
+ override fun onRenderedFirstFrame() {
37
+ if (isPlayerSurfaceLayoutValid()) {
38
+ maybeCallOnFirstFrameRendered()
39
+ } else {
40
+ hasPendingOnFirstFrame = true
41
+ }
42
+ }
43
+
44
+ override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
45
+ hasSentFirstFrameForCurrentMediaItem = false
46
+ super.onMediaItemTransition(mediaItem, reason)
47
+ }
48
+
49
+ override fun onSurfaceSizeChanged(width: Int, height: Int) {
50
+ if (isPlayerSurfaceLayoutValid() && hasPendingOnFirstFrame) {
51
+ maybeCallOnFirstFrameRendered()
52
+ }
53
+ }
54
+
55
+ // Unlike iOS, android calls `onRenderedFirstFrame` multiple times for the same media item (after seeking).
56
+ // We want to match the behavior across platforms, so we limit the number of event emissions.
57
+ private fun maybeCallOnFirstFrameRendered() {
58
+ if (!hasSentFirstFrameForCurrentMediaItem) {
59
+ onFirstFrameRendered()
60
+ }
61
+ hasPendingOnFirstFrame = false
62
+ hasSentFirstFrameForCurrentMediaItem = true
63
+ }
64
+
65
+ private fun isPlayerSurfaceLayoutValid(): Boolean {
66
+ // Sometimes the video size announced by the track will is 1px off the render size.
67
+ val epsilon = 0.05
68
+ val player = playerReference.get() ?: run {
69
+ return false
70
+ }
71
+ val currentPlayerView = currentViewReference.get() ?: run {
72
+ return false
73
+ }
74
+ val surfaceWidth = player.surfaceSize.width
75
+ val surfaceHeight = player.surfaceSize.height
76
+ val sourceWidth = player.videoSize.width
77
+ val sourceHeight = player.videoSize.height
78
+ val sourcePixelWidthHeightRatio = player.videoSize.pixelWidthHeightRatio
79
+
80
+ if (surfaceWidth == 0 || surfaceHeight == 0) {
81
+ return false
82
+ }
83
+
84
+ val surfaceAspectRatio = surfaceWidth.toFloat() / surfaceHeight
85
+ val trackAspectRatio = sourceWidth.toFloat() / sourceHeight * sourcePixelWidthHeightRatio
86
+
87
+ val videoSizeIsUnknown = sourceWidth == 0 || sourceHeight == 0
88
+ val hasFillContentFit = currentPlayerView.resizeMode == ContentFit.FILL.toResizeMode()
89
+ val hasCorrectRatio = abs(trackAspectRatio - surfaceAspectRatio) < epsilon
90
+
91
+ return (hasCorrectRatio || hasFillContentFit || videoSizeIsUnknown)
92
+ }
93
+ }
@@ -0,0 +1,164 @@
1
+ package expo.modules.video.player
2
+
3
+ import androidx.annotation.OptIn
4
+ import androidx.media3.common.TrackSelectionParameters
5
+ import androidx.media3.common.Tracks
6
+ import androidx.media3.common.util.UnstableApi
7
+ import expo.modules.video.enums.AudioMixingMode
8
+ import expo.modules.video.enums.PlayerStatus
9
+ import expo.modules.video.records.AudioTrack
10
+ import expo.modules.video.records.AvailableSubtitleTracksChangedEventPayload
11
+ import expo.modules.video.records.AvailableAudioTracksChangedEventPayload
12
+ import expo.modules.video.records.IsPlayingEventPayload
13
+ import expo.modules.video.records.MutedChangedEventPayload
14
+ import expo.modules.video.records.PlaybackError
15
+ import expo.modules.video.records.PlaybackRateChangedEventPayload
16
+ import expo.modules.video.records.SourceChangedEventPayload
17
+ import expo.modules.video.records.StatusChangedEventPayload
18
+ import expo.modules.video.records.SubtitleTrack
19
+ import expo.modules.video.records.SubtitleTrackChangedEventPayload
20
+ import expo.modules.video.records.AudioTrackChangedEventPayload
21
+ import expo.modules.video.records.TimeUpdate
22
+ import expo.modules.video.records.VideoEventPayload
23
+ import expo.modules.video.records.VideoSource
24
+ import expo.modules.video.records.VideoSourceLoadedEventPayload
25
+ import expo.modules.video.records.VideoTrack
26
+ import expo.modules.video.records.VideoTrackChangedEventPayload
27
+ import expo.modules.video.records.VolumeChangedEventPayload
28
+
29
+ @OptIn(UnstableApi::class)
30
+ sealed class PlayerEvent {
31
+ open val name: String = ""
32
+ open val jsEventPayload: VideoEventPayload? = null
33
+ open val emitToJS: Boolean = true
34
+
35
+ data class StatusChanged(val status: PlayerStatus, val oldStatus: PlayerStatus?, val error: PlaybackError?) : PlayerEvent() {
36
+ override val name = "statusChange"
37
+ override val jsEventPayload = StatusChangedEventPayload(status, oldStatus, error)
38
+ }
39
+
40
+ data class IsPlayingChanged(val isPlaying: Boolean, val oldIsPlaying: Boolean?) : PlayerEvent() {
41
+ override val name = "playingChange"
42
+ override val jsEventPayload = IsPlayingEventPayload(isPlaying, oldIsPlaying)
43
+ }
44
+
45
+ data class VolumeChanged(val volume: Float, val oldVolume: Float?) : PlayerEvent() {
46
+ override val name = "volumeChange"
47
+ override val jsEventPayload = VolumeChangedEventPayload(volume, oldVolume)
48
+ }
49
+
50
+ data class MutedChanged(val muted: Boolean, val oldMuted: Boolean?) : PlayerEvent() {
51
+ override val name = "mutedChange"
52
+ override val jsEventPayload = MutedChangedEventPayload(muted, oldMuted)
53
+ }
54
+
55
+ data class SourceChanged(val source: VideoSource?, val oldSource: VideoSource?) : PlayerEvent() {
56
+ override val name = "sourceChange"
57
+ override val jsEventPayload = SourceChangedEventPayload(source, oldSource)
58
+ }
59
+
60
+ data class PlaybackRateChanged(val rate: Float, val oldRate: Float?) : PlayerEvent() {
61
+ override val name = "playbackRateChange"
62
+ override val jsEventPayload = PlaybackRateChangedEventPayload(rate, oldRate)
63
+ }
64
+
65
+ data class TracksChanged(val tracks: Tracks) : PlayerEvent() {
66
+ override val name = "tracksChange"
67
+ override val emitToJS = false
68
+ }
69
+
70
+ data class TrackSelectionParametersChanged(val trackSelectionParameters: TrackSelectionParameters) : PlayerEvent() {
71
+ override val name = "trackSelectionParametersChange"
72
+ override val emitToJS = false
73
+ }
74
+
75
+ data class SubtitleTrackChanged(val subtitleTrack: SubtitleTrack?, val oldSubtitleTrack: SubtitleTrack?) : PlayerEvent() {
76
+ override val name = "subtitleTrackChange"
77
+ override val jsEventPayload = SubtitleTrackChangedEventPayload(subtitleTrack, oldSubtitleTrack)
78
+ }
79
+
80
+ data class AudioTrackChanged(val audioTrack: AudioTrack?, val oldAudioTrack: AudioTrack?) : PlayerEvent() {
81
+ override val name = "audioTrackChange"
82
+ override val jsEventPayload = AudioTrackChangedEventPayload(audioTrack, oldAudioTrack)
83
+ }
84
+
85
+ data class VideoTrackChanged(val videoTrack: VideoTrack?, val oldVideoTrack: VideoTrack?) : PlayerEvent() {
86
+ override val name = "videoTrackChange"
87
+ override val jsEventPayload = VideoTrackChangedEventPayload(videoTrack, oldVideoTrack)
88
+ }
89
+
90
+ class RenderedFirstFrame : PlayerEvent() {
91
+ override val name = "renderFirstFrame"
92
+
93
+ // This Event is emitted through the view (we are matching the AVKit API behavior)
94
+ override val emitToJS = false
95
+ }
96
+
97
+ data class AvailableSubtitleTracksChanged(
98
+ val availableSubtitleTracks: List<SubtitleTrack>,
99
+ val oldAvailableSubtitleTracks: List<SubtitleTrack>
100
+ ) : PlayerEvent() {
101
+ override val name = "availableSubtitleTracksChange"
102
+ override val jsEventPayload = AvailableSubtitleTracksChangedEventPayload(availableSubtitleTracks, oldAvailableSubtitleTracks)
103
+ }
104
+
105
+ data class AvailableAudioTracksChanged(
106
+ val availableAudioTracks: List<AudioTrack>,
107
+ val oldAvailableAudioTracks: List<AudioTrack>
108
+ ) : PlayerEvent() {
109
+ override val name = "availableAudioTracksChange"
110
+ override val jsEventPayload = AvailableAudioTracksChangedEventPayload(availableAudioTracks, oldAvailableAudioTracks)
111
+ }
112
+
113
+ data class VideoSourceLoaded(
114
+ val videoSource: VideoSource?,
115
+ val duration: Double,
116
+ val availableVideoTracks: List<VideoTrack>,
117
+ val availableSubtitleTracks: List<SubtitleTrack>,
118
+ val availableAudioTracks: List<AudioTrack>
119
+ ) : PlayerEvent() {
120
+ override val name = "sourceLoad"
121
+ override val jsEventPayload = VideoSourceLoadedEventPayload(
122
+ videoSource,
123
+ duration,
124
+ availableVideoTracks,
125
+ availableSubtitleTracks,
126
+ availableAudioTracks
127
+ )
128
+ }
129
+
130
+ data class TimeUpdated(val timeUpdate: TimeUpdate) : PlayerEvent() {
131
+ override val name = "timeUpdate"
132
+ override val jsEventPayload = timeUpdate
133
+ }
134
+
135
+ data class AudioMixingModeChanged(val audioMixingMode: AudioMixingMode, val oldAudioMixingMode: AudioMixingMode?) : PlayerEvent() {
136
+ override val name = "audioMixingModeChange"
137
+ override val emitToJS = false
138
+ }
139
+
140
+ class PlayedToEnd : PlayerEvent() {
141
+ override val name = "playToEnd"
142
+ }
143
+
144
+ fun emit(player: VideoPlayer, listeners: List<VideoPlayerListener>) {
145
+ when (this) {
146
+ is StatusChanged -> listeners.forEach { it.onStatusChanged(player, status, oldStatus, error) }
147
+ is IsPlayingChanged -> listeners.forEach { it.onIsPlayingChanged(player, isPlaying, oldIsPlaying) }
148
+ is VolumeChanged -> listeners.forEach { it.onVolumeChanged(player, volume, oldVolume) }
149
+ is SourceChanged -> listeners.forEach { it.onSourceChanged(player, source, oldSource) }
150
+ is PlaybackRateChanged -> listeners.forEach { it.onPlaybackRateChanged(player, rate, oldRate) }
151
+ is TracksChanged -> listeners.forEach { it.onTracksChanged(player, tracks) }
152
+ is TrackSelectionParametersChanged -> listeners.forEach { it.onTrackSelectionParametersChanged(player, trackSelectionParameters) }
153
+ is TimeUpdated -> listeners.forEach { it.onTimeUpdate(player, timeUpdate) }
154
+ is PlayedToEnd -> listeners.forEach { it.onPlayedToEnd(player) }
155
+ is MutedChanged -> listeners.forEach { it.onMutedChanged(player, muted, oldMuted) }
156
+ is AudioMixingModeChanged -> listeners.forEach { it.onAudioMixingModeChanged(player, audioMixingMode, oldAudioMixingMode) }
157
+ is VideoTrackChanged -> listeners.forEach { it.onVideoTrackChanged(player, videoTrack, oldVideoTrack) }
158
+ is RenderedFirstFrame -> listeners.forEach { it.onRenderedFirstFrame(player) }
159
+ is VideoSourceLoaded -> listeners.forEach { it.onVideoSourceLoaded(player, videoSource, duration, availableVideoTracks, availableSubtitleTracks, availableAudioTracks) }
160
+ // JS-only events - SubtitleTrackChanged - In the native events the TracksChanged can be used instead
161
+ else -> Unit
162
+ }
163
+ }
164
+ }