@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.
- package/README.md +45 -0
- package/android/build.gradle +32 -0
- package/android/src/main/AndroidManifest.xml +20 -0
- package/android/src/main/java/expo/modules/video/AudioFocusManager.kt +241 -0
- package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +145 -0
- package/android/src/main/java/expo/modules/video/IntervalUpdateClock.kt +54 -0
- package/android/src/main/java/expo/modules/video/MediaMetadataRetriever.kt +89 -0
- package/android/src/main/java/expo/modules/video/PictureInPictureHelperFragment.kt +26 -0
- package/android/src/main/java/expo/modules/video/PlayerViewExtension.kt +36 -0
- package/android/src/main/java/expo/modules/video/VideoCache.kt +104 -0
- package/android/src/main/java/expo/modules/video/VideoExceptions.kt +34 -0
- package/android/src/main/java/expo/modules/video/VideoManager.kt +133 -0
- package/android/src/main/java/expo/modules/video/VideoModule.kt +414 -0
- package/android/src/main/java/expo/modules/video/VideoThumbnail.kt +20 -0
- package/android/src/main/java/expo/modules/video/VideoView.kt +367 -0
- package/android/src/main/java/expo/modules/video/delegates/IgnoreSameSet.kt +24 -0
- package/android/src/main/java/expo/modules/video/drawing/OutlineProvider.kt +217 -0
- package/android/src/main/java/expo/modules/video/enums/AudioMixingMode.kt +20 -0
- package/android/src/main/java/expo/modules/video/enums/ContentFit.kt +19 -0
- package/android/src/main/java/expo/modules/video/enums/ContentType.kt +22 -0
- package/android/src/main/java/expo/modules/video/enums/DRMType.kt +26 -0
- package/android/src/main/java/expo/modules/video/enums/PlayerStatus.kt +10 -0
- package/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt +184 -0
- package/android/src/main/java/expo/modules/video/playbackService/PlaybackServiceConnection.kt +39 -0
- package/android/src/main/java/expo/modules/video/playbackService/VideoMediaSessionCallback.kt +47 -0
- package/android/src/main/java/expo/modules/video/player/FirstFrameEventGenerator.kt +93 -0
- package/android/src/main/java/expo/modules/video/player/PlayerEvent.kt +164 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayer.kt +460 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerAudioTracks.kt +125 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerListener.kt +32 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerLoadControl.kt +525 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerSubtitles.kt +125 -0
- package/android/src/main/java/expo/modules/video/records/BufferOptions.kt +15 -0
- package/android/src/main/java/expo/modules/video/records/DRMOptions.kt +25 -0
- package/android/src/main/java/expo/modules/video/records/PlaybackError.kt +19 -0
- package/android/src/main/java/expo/modules/video/records/Tracks.kt +81 -0
- package/android/src/main/java/expo/modules/video/records/VideoEventPayloads.kt +79 -0
- package/android/src/main/java/expo/modules/video/records/VideoMetadata.kt +12 -0
- package/android/src/main/java/expo/modules/video/records/VideoSize.kt +14 -0
- package/android/src/main/java/expo/modules/video/records/VideoSource.kt +104 -0
- package/android/src/main/java/expo/modules/video/records/VideoThumbnailOptions.kt +24 -0
- package/android/src/main/java/expo/modules/video/utils/DataSourceUtils.kt +75 -0
- package/android/src/main/java/expo/modules/video/utils/EventDispatcherUtils.kt +43 -0
- package/android/src/main/java/expo/modules/video/utils/MutableWeakReference.kt +15 -0
- package/android/src/main/java/expo/modules/video/utils/PictureInPictureUtils.kt +96 -0
- package/android/src/main/java/expo/modules/video/utils/YogaUtils.kt +20 -0
- package/android/src/main/res/drawable/seek_backwards_10s.xml +25 -0
- package/android/src/main/res/drawable/seek_backwards_15s.xml +25 -0
- package/android/src/main/res/drawable/seek_backwards_5s.xml +25 -0
- package/android/src/main/res/drawable/seek_forwards_10s.xml +30 -0
- package/android/src/main/res/drawable/seek_forwards_15s.xml +31 -0
- package/android/src/main/res/drawable/seek_forwards_5s.xml +30 -0
- package/android/src/main/res/layout/fullscreen_player_activity.xml +16 -0
- package/android/src/main/res/layout/surface_player_view.xml +7 -0
- package/android/src/main/res/layout/texture_player_view.xml +7 -0
- package/android/src/main/res/values/styles.xml +9 -0
- package/app.plugin.js +1 -0
- package/build/NativeVideoModule.d.ts +16 -0
- package/build/NativeVideoModule.d.ts.map +1 -0
- package/build/NativeVideoModule.js +3 -0
- package/build/NativeVideoModule.js.map +1 -0
- package/build/NativeVideoModule.web.d.ts +3 -0
- package/build/NativeVideoModule.web.d.ts.map +1 -0
- package/build/NativeVideoModule.web.js +2 -0
- package/build/NativeVideoModule.web.js.map +1 -0
- package/build/NativeVideoView.d.ts +4 -0
- package/build/NativeVideoView.d.ts.map +1 -0
- package/build/NativeVideoView.js +6 -0
- package/build/NativeVideoView.js.map +1 -0
- package/build/VideoModule.d.ts +38 -0
- package/build/VideoModule.d.ts.map +1 -0
- package/build/VideoModule.js +53 -0
- package/build/VideoModule.js.map +1 -0
- package/build/VideoPlayer.d.ts +15 -0
- package/build/VideoPlayer.d.ts.map +1 -0
- package/build/VideoPlayer.js +52 -0
- package/build/VideoPlayer.js.map +1 -0
- package/build/VideoPlayer.types.d.ts +532 -0
- package/build/VideoPlayer.types.d.ts.map +1 -0
- package/build/VideoPlayer.types.js +2 -0
- package/build/VideoPlayer.types.js.map +1 -0
- package/build/VideoPlayer.web.d.ts +75 -0
- package/build/VideoPlayer.web.d.ts.map +1 -0
- package/build/VideoPlayer.web.js +376 -0
- package/build/VideoPlayer.web.js.map +1 -0
- package/build/VideoPlayerEvents.types.d.ts +262 -0
- package/build/VideoPlayerEvents.types.d.ts.map +1 -0
- package/build/VideoPlayerEvents.types.js +2 -0
- package/build/VideoPlayerEvents.types.js.map +1 -0
- package/build/VideoThumbnail.d.ts +29 -0
- package/build/VideoThumbnail.d.ts.map +1 -0
- package/build/VideoThumbnail.js +3 -0
- package/build/VideoThumbnail.js.map +1 -0
- package/build/VideoView.d.ts +44 -0
- package/build/VideoView.d.ts.map +1 -0
- package/build/VideoView.js +76 -0
- package/build/VideoView.js.map +1 -0
- package/build/VideoView.types.d.ts +147 -0
- package/build/VideoView.types.d.ts.map +1 -0
- package/build/VideoView.types.js +2 -0
- package/build/VideoView.types.js.map +1 -0
- package/build/VideoView.web.d.ts +9 -0
- package/build/VideoView.web.d.ts.map +1 -0
- package/build/VideoView.web.js +180 -0
- package/build/VideoView.web.js.map +1 -0
- package/build/index.d.ts +9 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +7 -0
- package/build/index.js.map +1 -0
- package/build/resolveAssetSource.d.ts +3 -0
- package/build/resolveAssetSource.d.ts.map +1 -0
- package/build/resolveAssetSource.js +3 -0
- package/build/resolveAssetSource.js.map +1 -0
- package/build/resolveAssetSource.web.d.ts +4 -0
- package/build/resolveAssetSource.web.d.ts.map +1 -0
- package/build/resolveAssetSource.web.js +16 -0
- package/build/resolveAssetSource.web.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/Cache/CachableRequest.swift +44 -0
- package/ios/Cache/CachedResource.swift +97 -0
- package/ios/Cache/CachingHelpers.swift +92 -0
- package/ios/Cache/MediaFileHandle.swift +94 -0
- package/ios/Cache/MediaInfo.swift +147 -0
- package/ios/Cache/ResourceLoaderDelegate.swift +274 -0
- package/ios/Cache/SynchronizedHashTable.swift +23 -0
- package/ios/Cache/VideoCacheManager.swift +338 -0
- package/ios/ContentKeyDelegate.swift +214 -0
- package/ios/ContentKeyManager.swift +21 -0
- package/ios/Enums/AudioMixingMode.swift +37 -0
- package/ios/Enums/ContentType.swift +12 -0
- package/ios/Enums/DRMType.swift +20 -0
- package/ios/Enums/PlayerStatus.swift +10 -0
- package/ios/Enums/VideoContentFit.swift +39 -0
- package/ios/ExpoVideo.podspec +29 -0
- package/ios/NowPlayingManager.swift +296 -0
- package/ios/Records/BufferOptions.swift +12 -0
- package/ios/Records/DRMOptions.swift +24 -0
- package/ios/Records/PlaybackError.swift +10 -0
- package/ios/Records/Tracks.swift +176 -0
- package/ios/Records/VideoEventPayloads.swift +76 -0
- package/ios/Records/VideoMetadata.swift +16 -0
- package/ios/Records/VideoSize.swift +15 -0
- package/ios/Records/VideoSource.swift +25 -0
- package/ios/Thumbnails/VideoThumbnail.swift +27 -0
- package/ios/Thumbnails/VideoThumbnailGenerator.swift +68 -0
- package/ios/Thumbnails/VideoThumbnailOptions.swift +15 -0
- package/ios/VideoAsset.swift +123 -0
- package/ios/VideoExceptions.swift +53 -0
- package/ios/VideoItem.swift +11 -0
- package/ios/VideoManager.swift +140 -0
- package/ios/VideoModule.swift +383 -0
- package/ios/VideoPlayer/DangerousPropertiesStore.swift +19 -0
- package/ios/VideoPlayer.swift +435 -0
- package/ios/VideoPlayerAudioTracks.swift +72 -0
- package/ios/VideoPlayerItem.swift +97 -0
- package/ios/VideoPlayerObserver.swift +523 -0
- package/ios/VideoPlayerSubtitles.swift +71 -0
- package/ios/VideoSourceLoader.swift +89 -0
- package/ios/VideoSourceLoaderListener.swift +34 -0
- package/ios/VideoView.swift +224 -0
- package/package.json +59 -0
- package/plugin/build/tsconfig.tsbuildinfo +1 -0
- package/plugin/build/withExpoVideo.d.ts +7 -0
- package/plugin/build/withExpoVideo.js +38 -0
- package/src/NativeVideoModule.ts +20 -0
- package/src/NativeVideoModule.web.ts +1 -0
- package/src/NativeVideoView.ts +8 -0
- package/src/VideoModule.ts +59 -0
- package/src/VideoPlayer.tsx +67 -0
- package/src/VideoPlayer.types.ts +613 -0
- package/src/VideoPlayer.web.tsx +451 -0
- package/src/VideoPlayerEvents.types.ts +313 -0
- package/src/VideoThumbnail.ts +31 -0
- package/src/VideoView.tsx +86 -0
- package/src/VideoView.types.ts +165 -0
- package/src/VideoView.web.tsx +214 -0
- package/src/index.ts +46 -0
- package/src/resolveAssetSource.ts +2 -0
- package/src/resolveAssetSource.web.ts +17 -0
- package/src/ts-declarations/react-native-assets.d.ts +1 -0
|
@@ -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
|
+
}
|