expo-video-subtitle 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/FORK.md +149 -0
  3. package/LICENSE +21 -0
  4. package/README.md +108 -0
  5. package/android/build.gradle +32 -0
  6. package/android/src/main/AndroidManifest.xml +10 -0
  7. package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +258 -0
  8. package/android/src/main/java/expo/modules/video/IntervalUpdateClock.kt +54 -0
  9. package/android/src/main/java/expo/modules/video/MediaMetadataRetriever.kt +89 -0
  10. package/android/src/main/java/expo/modules/video/PlayerViewExtension.kt +87 -0
  11. package/android/src/main/java/expo/modules/video/VideoCache.kt +109 -0
  12. package/android/src/main/java/expo/modules/video/VideoExceptions.kt +45 -0
  13. package/android/src/main/java/expo/modules/video/VideoModule.kt +446 -0
  14. package/android/src/main/java/expo/modules/video/VideoThumbnail.kt +20 -0
  15. package/android/src/main/java/expo/modules/video/VideoView.kt +457 -0
  16. package/android/src/main/java/expo/modules/video/delegates/IgnoreSameSet.kt +24 -0
  17. package/android/src/main/java/expo/modules/video/drawing/OutlineProvider.kt +217 -0
  18. package/android/src/main/java/expo/modules/video/enums/AudioMixingMode.kt +20 -0
  19. package/android/src/main/java/expo/modules/video/enums/ContentFit.kt +19 -0
  20. package/android/src/main/java/expo/modules/video/enums/ContentType.kt +22 -0
  21. package/android/src/main/java/expo/modules/video/enums/DRMType.kt +26 -0
  22. package/android/src/main/java/expo/modules/video/enums/FullscreenOrientation.kt +25 -0
  23. package/android/src/main/java/expo/modules/video/enums/PlayerStatus.kt +10 -0
  24. package/android/src/main/java/expo/modules/video/enums/VideoRange.kt +27 -0
  25. package/android/src/main/java/expo/modules/video/listeners/VideoManagerListener.kt +10 -0
  26. package/android/src/main/java/expo/modules/video/listeners/VideoPlayerListener.kt +35 -0
  27. package/android/src/main/java/expo/modules/video/listeners/VideoViewListener.kt +13 -0
  28. package/android/src/main/java/expo/modules/video/managers/AudioFocusManager.kt +242 -0
  29. package/android/src/main/java/expo/modules/video/managers/PictureInPictureManager.kt +329 -0
  30. package/android/src/main/java/expo/modules/video/managers/VideoManager.kt +194 -0
  31. package/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt +267 -0
  32. package/android/src/main/java/expo/modules/video/playbackService/PlaybackServiceConnection.kt +62 -0
  33. package/android/src/main/java/expo/modules/video/playbackService/VideoMediaSessionCallback.kt +47 -0
  34. package/android/src/main/java/expo/modules/video/player/DefaultLoadControl.java +552 -0
  35. package/android/src/main/java/expo/modules/video/player/FirstFrameEventGenerator.kt +116 -0
  36. package/android/src/main/java/expo/modules/video/player/PlayerEvent.kt +172 -0
  37. package/android/src/main/java/expo/modules/video/player/VideoPlayer.kt +598 -0
  38. package/android/src/main/java/expo/modules/video/player/VideoPlayerAudioTracks.kt +126 -0
  39. package/android/src/main/java/expo/modules/video/player/VideoPlayerKeepAwake.kt +91 -0
  40. package/android/src/main/java/expo/modules/video/player/VideoPlayerLoadControl.kt +57 -0
  41. package/android/src/main/java/expo/modules/video/player/VideoPlayerSubtitles.kt +126 -0
  42. package/android/src/main/java/expo/modules/video/records/BufferOptions.kt +18 -0
  43. package/android/src/main/java/expo/modules/video/records/ButtonOptions.kt +17 -0
  44. package/android/src/main/java/expo/modules/video/records/DRMOptions.kt +27 -0
  45. package/android/src/main/java/expo/modules/video/records/FullscreenOptions.kt +14 -0
  46. package/android/src/main/java/expo/modules/video/records/PiPParams.kt +38 -0
  47. package/android/src/main/java/expo/modules/video/records/PlaybackError.kt +21 -0
  48. package/android/src/main/java/expo/modules/video/records/PlayerBuilderOptions.kt +15 -0
  49. package/android/src/main/java/expo/modules/video/records/ScrubbingModeOptions.kt +33 -0
  50. package/android/src/main/java/expo/modules/video/records/SeekTolerance.kt +30 -0
  51. package/android/src/main/java/expo/modules/video/records/SubtitleSource.kt +54 -0
  52. package/android/src/main/java/expo/modules/video/records/SubtitleStyle.kt +98 -0
  53. package/android/src/main/java/expo/modules/video/records/Tracks.kt +115 -0
  54. package/android/src/main/java/expo/modules/video/records/VideoEventPayloads.kt +79 -0
  55. package/android/src/main/java/expo/modules/video/records/VideoMetadata.kt +14 -0
  56. package/android/src/main/java/expo/modules/video/records/VideoSize.kt +16 -0
  57. package/android/src/main/java/expo/modules/video/records/VideoSource.kt +113 -0
  58. package/android/src/main/java/expo/modules/video/records/VideoThumbnailOptions.kt +26 -0
  59. package/android/src/main/java/expo/modules/video/utils/DataSourceUtils.kt +76 -0
  60. package/android/src/main/java/expo/modules/video/utils/EventDispatcherUtils.kt +43 -0
  61. package/android/src/main/java/expo/modules/video/utils/FullscreenActivityOrientationHelper.kt +120 -0
  62. package/android/src/main/java/expo/modules/video/utils/MediaSessionUtils.kt +14 -0
  63. package/android/src/main/java/expo/modules/video/utils/MutableWeakReference.kt +26 -0
  64. package/android/src/main/java/expo/modules/video/utils/PictureInPictureHelperFragment.kt +49 -0
  65. package/android/src/main/java/expo/modules/video/utils/PictureInPictureUtils.kt +96 -0
  66. package/android/src/main/java/expo/modules/video/utils/SubtitleUtils.kt +68 -0
  67. package/android/src/main/java/expo/modules/video/utils/ViewVisibilityUtils.kt +28 -0
  68. package/android/src/main/java/expo/modules/video/utils/WeakMutableSet.kt +120 -0
  69. package/android/src/main/java/expo/modules/video/utils/YogaUtils.kt +20 -0
  70. package/android/src/main/res/drawable/seek_backwards_10s.xml +25 -0
  71. package/android/src/main/res/drawable/seek_backwards_15s.xml +25 -0
  72. package/android/src/main/res/drawable/seek_backwards_5s.xml +25 -0
  73. package/android/src/main/res/drawable/seek_forwards_10s.xml +30 -0
  74. package/android/src/main/res/drawable/seek_forwards_15s.xml +31 -0
  75. package/android/src/main/res/drawable/seek_forwards_5s.xml +30 -0
  76. package/android/src/main/res/layout/fullscreen_player_activity.xml +20 -0
  77. package/android/src/main/res/layout/surface_player_view.xml +7 -0
  78. package/android/src/main/res/layout/texture_player_view.xml +7 -0
  79. package/android/src/main/res/values/styles.xml +9 -0
  80. package/app.plugin.js +1 -0
  81. package/build/NativeVideoAirPlayButtonView.d.ts +3 -0
  82. package/build/NativeVideoAirPlayButtonView.d.ts.map +1 -0
  83. package/build/NativeVideoAirPlayButtonView.js +3 -0
  84. package/build/NativeVideoAirPlayButtonView.js.map +1 -0
  85. package/build/NativeVideoModule.d.ts +13 -0
  86. package/build/NativeVideoModule.d.ts.map +1 -0
  87. package/build/NativeVideoModule.js +3 -0
  88. package/build/NativeVideoModule.js.map +1 -0
  89. package/build/NativeVideoModule.web.d.ts +5 -0
  90. package/build/NativeVideoModule.web.d.ts.map +1 -0
  91. package/build/NativeVideoModule.web.js +5 -0
  92. package/build/NativeVideoModule.web.js.map +1 -0
  93. package/build/NativeVideoView.d.ts +4 -0
  94. package/build/NativeVideoView.d.ts.map +1 -0
  95. package/build/NativeVideoView.js +6 -0
  96. package/build/NativeVideoView.js.map +1 -0
  97. package/build/VideoAirPlayButton.d.ts +10 -0
  98. package/build/VideoAirPlayButton.d.ts.map +1 -0
  99. package/build/VideoAirPlayButton.ios.d.ts +3 -0
  100. package/build/VideoAirPlayButton.ios.d.ts.map +1 -0
  101. package/build/VideoAirPlayButton.ios.js +6 -0
  102. package/build/VideoAirPlayButton.ios.js.map +1 -0
  103. package/build/VideoAirPlayButton.js +13 -0
  104. package/build/VideoAirPlayButton.js.map +1 -0
  105. package/build/VideoAirPlayButton.types.d.ts +35 -0
  106. package/build/VideoAirPlayButton.types.d.ts.map +1 -0
  107. package/build/VideoAirPlayButton.types.js +2 -0
  108. package/build/VideoAirPlayButton.types.js.map +1 -0
  109. package/build/VideoModule.d.ts +35 -0
  110. package/build/VideoModule.d.ts.map +1 -0
  111. package/build/VideoModule.js +44 -0
  112. package/build/VideoModule.js.map +1 -0
  113. package/build/VideoPlayer.d.ts +17 -0
  114. package/build/VideoPlayer.d.ts.map +1 -0
  115. package/build/VideoPlayer.js +55 -0
  116. package/build/VideoPlayer.js.map +1 -0
  117. package/build/VideoPlayer.types.d.ts +782 -0
  118. package/build/VideoPlayer.types.d.ts.map +1 -0
  119. package/build/VideoPlayer.types.js +2 -0
  120. package/build/VideoPlayer.types.js.map +1 -0
  121. package/build/VideoPlayer.web.d.ts +79 -0
  122. package/build/VideoPlayer.web.d.ts.map +1 -0
  123. package/build/VideoPlayer.web.js +388 -0
  124. package/build/VideoPlayer.web.js.map +1 -0
  125. package/build/VideoPlayerEvents.types.d.ts +278 -0
  126. package/build/VideoPlayerEvents.types.d.ts.map +1 -0
  127. package/build/VideoPlayerEvents.types.js +2 -0
  128. package/build/VideoPlayerEvents.types.js.map +1 -0
  129. package/build/VideoThumbnail.d.ts +29 -0
  130. package/build/VideoThumbnail.d.ts.map +1 -0
  131. package/build/VideoThumbnail.js +4 -0
  132. package/build/VideoThumbnail.js.map +1 -0
  133. package/build/VideoView.d.ts +69 -0
  134. package/build/VideoView.d.ts.map +1 -0
  135. package/build/VideoView.js +100 -0
  136. package/build/VideoView.js.map +1 -0
  137. package/build/VideoView.types.d.ts +362 -0
  138. package/build/VideoView.types.d.ts.map +1 -0
  139. package/build/VideoView.types.js +2 -0
  140. package/build/VideoView.types.js.map +1 -0
  141. package/build/VideoView.web.d.ts +9 -0
  142. package/build/VideoView.web.d.ts.map +1 -0
  143. package/build/VideoView.web.js +239 -0
  144. package/build/VideoView.web.js.map +1 -0
  145. package/build/index.d.ts +11 -0
  146. package/build/index.d.ts.map +1 -0
  147. package/build/index.js +6 -0
  148. package/build/index.js.map +1 -0
  149. package/build/resolveAssetSource.d.ts +3 -0
  150. package/build/resolveAssetSource.d.ts.map +1 -0
  151. package/build/resolveAssetSource.js +3 -0
  152. package/build/resolveAssetSource.js.map +1 -0
  153. package/build/resolveAssetSource.web.d.ts +4 -0
  154. package/build/resolveAssetSource.web.d.ts.map +1 -0
  155. package/build/resolveAssetSource.web.js +16 -0
  156. package/build/resolveAssetSource.web.js.map +1 -0
  157. package/expo-module.config.json +9 -0
  158. package/ios/Cache/CachableRequest.swift +46 -0
  159. package/ios/Cache/CacheVideoAssetTransportProvider.swift +64 -0
  160. package/ios/Cache/CachedResource.swift +164 -0
  161. package/ios/Cache/CachingHelpers.swift +107 -0
  162. package/ios/Cache/MediaFileHandle.swift +139 -0
  163. package/ios/Cache/MediaInfo.swift +185 -0
  164. package/ios/Cache/ResourceLoaderDelegate.swift +306 -0
  165. package/ios/Cache/SynchronizedHashTable.swift +23 -0
  166. package/ios/Cache/VideoCacheManager.swift +192 -0
  167. package/ios/ContentKeyDelegate.swift +214 -0
  168. package/ios/ContentKeyManager.swift +21 -0
  169. package/ios/Enums/AudioMixingMode.swift +37 -0
  170. package/ios/Enums/ContentType.swift +11 -0
  171. package/ios/Enums/DRMType.swift +20 -0
  172. package/ios/Enums/FullscreenOrientation.swift +34 -0
  173. package/ios/Enums/KeepFullscreenOnPiPStopBehavior.swift +20 -0
  174. package/ios/Enums/PlayerStatus.swift +10 -0
  175. package/ios/Enums/VideoContentFit.swift +39 -0
  176. package/ios/Enums/VideoRange.swift +21 -0
  177. package/ios/ExpoVideo.podspec +27 -0
  178. package/ios/NowPlayingManager.swift +304 -0
  179. package/ios/OrientationAVPlayerViewController.swift +263 -0
  180. package/ios/Records/BufferOptions.swift +12 -0
  181. package/ios/Records/DRMOptions.swift +24 -0
  182. package/ios/Records/FullscreenOptions.swift +14 -0
  183. package/ios/Records/PlaybackError.swift +10 -0
  184. package/ios/Records/ScrubbingModeOptions.swift +7 -0
  185. package/ios/Records/SeekTolerance.swift +20 -0
  186. package/ios/Records/SubtitleSource.swift +32 -0
  187. package/ios/Records/SubtitleStyle.swift +118 -0
  188. package/ios/Records/Tracks.swift +229 -0
  189. package/ios/Records/VideoEventPayloads.swift +81 -0
  190. package/ios/Records/VideoMetadata.swift +16 -0
  191. package/ios/Records/VideoSize.swift +15 -0
  192. package/ios/Records/VideoSource.swift +28 -0
  193. package/ios/Thumbnails/VideoThumbnail.swift +27 -0
  194. package/ios/Thumbnails/VideoThumbnailGenerator.swift +68 -0
  195. package/ios/Thumbnails/VideoThumbnailOptions.swift +15 -0
  196. package/ios/Utils/AVAssetVariant+VideoTracks.swift +20 -0
  197. package/ios/Utils/AvAssetTrack+VideoTracks.swift +79 -0
  198. package/ios/Utils/FourCharCode+toString.swift +25 -0
  199. package/ios/Utils/HlsUriUtils.swift +27 -0
  200. package/ios/Utils/URL+MediaLibraryAssets.swift +68 -0
  201. package/ios/VideoAirPlayButtonView.swift +70 -0
  202. package/ios/VideoAsset.swift +98 -0
  203. package/ios/VideoAssetTransport/VideoAssetLoadPlan.swift +69 -0
  204. package/ios/VideoAssetTransport/VideoAssetSourceDescriptor.swift +29 -0
  205. package/ios/VideoAssetTransport/VideoAssetTransportProvider.swift +24 -0
  206. package/ios/VideoAssetTransport/VideoAssetTransportRegistry.swift +103 -0
  207. package/ios/VideoExceptions.swift +53 -0
  208. package/ios/VideoItem.swift +11 -0
  209. package/ios/VideoManager.swift +182 -0
  210. package/ios/VideoModule.swift +413 -0
  211. package/ios/VideoPlayer/DangerousPropertiesStore.swift +19 -0
  212. package/ios/VideoPlayer.swift +485 -0
  213. package/ios/VideoPlayerAudioTracks.swift +109 -0
  214. package/ios/VideoPlayerItem.swift +169 -0
  215. package/ios/VideoPlayerObserver.swift +612 -0
  216. package/ios/VideoPlayerSeeker.swift +77 -0
  217. package/ios/VideoPlayerSubtitleSideload.swift +203 -0
  218. package/ios/VideoPlayerSubtitles.swift +88 -0
  219. package/ios/VideoSourceLoader.swift +90 -0
  220. package/ios/VideoSourceLoaderListener.swift +34 -0
  221. package/ios/VideoView.swift +201 -0
  222. package/package.json +51 -0
  223. package/plugin/build/index.d.ts +3 -0
  224. package/plugin/build/index.js +3 -0
  225. package/plugin/build/withExpoVideo.d.ts +9 -0
  226. package/plugin/build/withExpoVideo.js +73 -0
  227. package/plugin/index.d.ts +1 -0
  228. package/plugin/index.js +1 -0
  229. package/plugin/jest.config.js +1 -0
  230. package/plugin/src/index.ts +3 -0
  231. package/plugin/src/withExpoVideo.ts +103 -0
  232. package/plugin/tsconfig.json +10 -0
  233. package/src/NativeVideoAirPlayButtonView.ts +3 -0
  234. package/src/NativeVideoModule.ts +16 -0
  235. package/src/NativeVideoModule.web.ts +4 -0
  236. package/src/NativeVideoView.ts +8 -0
  237. package/src/VideoAirPlayButton.ios.tsx +8 -0
  238. package/src/VideoAirPlayButton.tsx +14 -0
  239. package/src/VideoAirPlayButton.types.ts +39 -0
  240. package/src/VideoModule.ts +47 -0
  241. package/src/VideoPlayer.tsx +74 -0
  242. package/src/VideoPlayer.types.ts +893 -0
  243. package/src/VideoPlayer.web.tsx +466 -0
  244. package/src/VideoPlayerEvents.types.ts +332 -0
  245. package/src/VideoThumbnail.ts +31 -0
  246. package/src/VideoView.tsx +111 -0
  247. package/src/VideoView.types.ts +405 -0
  248. package/src/VideoView.web.tsx +293 -0
  249. package/src/index.ts +26 -0
  250. package/src/resolveAssetSource.ts +2 -0
  251. package/src/resolveAssetSource.web.ts +17 -0
  252. package/src/ts-declarations/react-native-assets.d.ts +22 -0
  253. package/tsconfig.all.json +11 -0
  254. package/tsconfig.json +10 -0
  255. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,267 @@
1
+ package expo.modules.video.playbackService
2
+
3
+ import android.app.NotificationChannel
4
+ import android.app.NotificationManager
5
+ import android.app.Service
6
+ import android.content.Context
7
+ import android.content.Intent
8
+ import android.os.Binder
9
+ import android.os.Build
10
+ import android.os.Bundle
11
+ import android.os.IBinder
12
+ import androidx.annotation.MainThread
13
+ import androidx.annotation.OptIn
14
+ import androidx.core.app.NotificationCompat
15
+ import androidx.media3.common.util.UnstableApi
16
+ import androidx.media3.exoplayer.ExoPlayer
17
+ import androidx.media3.session.CommandButton
18
+ import androidx.media3.session.MediaSession
19
+ import androidx.media3.session.MediaSessionService
20
+ import androidx.media3.session.MediaStyleNotificationHelper
21
+ import androidx.media3.session.SessionCommand
22
+ import com.google.common.collect.ImmutableList
23
+ import expo.modules.kotlin.AppContext
24
+ import expo.modules.kotlin.exception.Exceptions
25
+ import expo.modules.video.R
26
+ import expo.modules.video.getPlaybackServiceErrorMessage
27
+ import expo.modules.video.player.VideoPlayer
28
+ import kotlinx.coroutines.launch
29
+ import java.lang.ref.WeakReference
30
+
31
+ class PlaybackServiceBinder(val service: ExpoVideoPlaybackService) : Binder()
32
+
33
+ @OptIn(UnstableApi::class)
34
+ class ExpoVideoPlaybackService : MediaSessionService() {
35
+ private lateinit var weakContext: WeakReference<AppContext>
36
+ var appContext: AppContext
37
+ get() = weakContext.get() ?: throw Exceptions.AppContextLost()
38
+ set(value) {
39
+ weakContext = WeakReference(value)
40
+ }
41
+ private val mediaSessions = mutableMapOf<ExoPlayer, MediaSession>()
42
+ private val binder = PlaybackServiceBinder(this)
43
+ private var mostRecentInteractionSession: MediaSession? = null
44
+ private var isForeground: Boolean = false
45
+
46
+ private val commandSeekForward = SessionCommand(SEEK_FORWARD_COMMAND, Bundle.EMPTY)
47
+ private val commandSeekBackward = SessionCommand(SEEK_BACKWARD_COMMAND, Bundle.EMPTY)
48
+ private val seekForwardButton = CommandButton.Builder()
49
+ .setDisplayName("rewind")
50
+ .setSessionCommand(commandSeekForward)
51
+ .setIconResId(R.drawable.seek_forwards_10s)
52
+ .build()
53
+
54
+ private val seekBackwardButton = CommandButton.Builder()
55
+ .setDisplayName("forward")
56
+ .setSessionCommand(commandSeekBackward)
57
+ .setIconResId(R.drawable.seek_backwards_10s)
58
+ .build()
59
+
60
+ fun setShowNotification(showNotification: Boolean, player: ExoPlayer) {
61
+ appContext.mainQueue.launch {
62
+ val sessionExtras = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
63
+ mediaSessions[player]?.sessionExtras?.deepCopy() ?: Bundle()
64
+ } else {
65
+ Bundle()
66
+ }
67
+ sessionExtras.putBoolean(SESSION_SHOW_NOTIFICATION, showNotification)
68
+ mediaSessions[player]?.let {
69
+ it.sessionExtras = sessionExtras
70
+ onUpdateNotification(it, showNotification && player.playWhenReady)
71
+ }
72
+ }
73
+ }
74
+
75
+ fun registerPlayer(videoPlayer: VideoPlayer) {
76
+ appContext.mainQueue.launch {
77
+ val player = videoPlayer.player
78
+ if (mediaSessions[player] != null) {
79
+ return@launch
80
+ }
81
+
82
+ val mediaSession = MediaSession.Builder(this@ExpoVideoPlaybackService, player)
83
+ .setId("ExpoVideoPlaybackService_${player.hashCode()}")
84
+ .setCallback(VideoMediaSessionCallback())
85
+ .setCustomLayout(ImmutableList.of(seekBackwardButton, seekForwardButton))
86
+ .build()
87
+
88
+ // Replace the basic media session with a session connected to our playback service.
89
+ videoPlayer.mediaSession.release()
90
+ videoPlayer.mediaSession = mediaSession
91
+
92
+ mediaSessions[player] = mediaSession
93
+ addSession(mediaSession)
94
+ setShowNotification(videoPlayer.showNowPlayingNotification, player)
95
+ }
96
+ }
97
+
98
+ fun unregisterPlayer(player: ExoPlayer) {
99
+ appContext.mainQueue.launch {
100
+ hidePlayerNotification(player)
101
+ val session = mediaSessions.remove(player)
102
+ session?.release()
103
+ if (mediaSessions.isEmpty()) {
104
+ cleanup()
105
+ stopSelf()
106
+ } else {
107
+ setMostRecentInteractionSession(findMostRecentInteractionSession())
108
+ }
109
+ }
110
+ }
111
+
112
+ override fun onBind(intent: Intent?): IBinder {
113
+ super.onBind(intent)
114
+ return binder
115
+ }
116
+
117
+ override fun onUpdateNotification(session: MediaSession, startInForegroundRequired: Boolean) {
118
+ appContext.mainQueue.launch {
119
+ if (startInForegroundRequired && session.wantsToShowNotification()) {
120
+ setMostRecentInteractionSession(session)
121
+ } else {
122
+ setMostRecentInteractionSession(findMostRecentInteractionSession())
123
+ }
124
+ }
125
+ }
126
+
127
+ @MainThread
128
+ private fun setMostRecentInteractionSession(session: MediaSession?) {
129
+ if (session?.player?.playWhenReady == false) {
130
+ stopForeground(STOP_FOREGROUND_DETACH)
131
+ isForeground = false
132
+ }
133
+
134
+ if (mostRecentInteractionSession != session) {
135
+ hideAllNotifications()
136
+ }
137
+
138
+ mostRecentInteractionSession = session
139
+ session?.let {
140
+ createNotification(it, it.player.playWhenReady)
141
+ } ?: run {
142
+ stopForeground(STOP_FOREGROUND_REMOVE)
143
+ isForeground = false
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Finds a session that is playing media. And wants to show a notification
149
+ * If none exists, returns a session that wants to show a notification.
150
+ * The current mostRecentInteractionSession always has priority to reduce the number of unnecessary notification changes.
151
+ */
152
+ @MainThread
153
+ private fun findMostRecentInteractionSession(): MediaSession? {
154
+ val prioritizedSessions = (listOfNotNull(mostRecentInteractionSession) + mediaSessions.values.toList()).distinct()
155
+
156
+ return prioritizedSessions.firstOrNull { it.wantsToShowNotification() && it.player.playWhenReady }
157
+ ?: prioritizedSessions.firstOrNull { it.wantsToShowNotification() }
158
+ }
159
+
160
+ override fun onTaskRemoved(rootIntent: Intent?) {
161
+ cleanup()
162
+ stopSelf()
163
+ }
164
+
165
+ override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
166
+ return null
167
+ }
168
+
169
+ override fun onDestroy() {
170
+ cleanup()
171
+ super.onDestroy()
172
+ }
173
+
174
+ @MainThread
175
+ private fun createNotification(session: MediaSession, startInForegroundRequired: Boolean = false) {
176
+ if (session.player.currentMediaItem == null) {
177
+ return
178
+ }
179
+
180
+ val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
181
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
182
+ notificationManager.createNotificationChannel(NotificationChannel(CHANNEL_ID, CHANNEL_ID, NotificationManager.IMPORTANCE_LOW))
183
+ }
184
+
185
+ // If the title is null android sets the notification to "<AppName> is running..." we want to keep the notification empty.
186
+ val contentTitle = session.player.currentMediaItem?.mediaMetadata?.title ?: "\u200E"
187
+ val notificationCompat = NotificationCompat.Builder(this, CHANNEL_ID)
188
+ .setSmallIcon(androidx.media3.session.R.drawable.media3_icon_circular_play)
189
+ .setContentTitle(contentTitle)
190
+ .setStyle(MediaStyleNotificationHelper.MediaStyle(session))
191
+ .build()
192
+
193
+ val notificationId = session.player.hashCode()
194
+
195
+ if (startInForegroundRequired) {
196
+ try {
197
+ startForeground(notificationId, notificationCompat)
198
+ isForeground = true
199
+ } catch (e: Exception) {
200
+ appContext.jsLogger?.error(getPlaybackServiceErrorMessage("Failed to start the expo-video foreground service"), e)
201
+ }
202
+ } else {
203
+ notificationManager.notify(notificationId, notificationCompat)
204
+ }
205
+ }
206
+
207
+ private fun cleanup() {
208
+ appContext.mainQueue.launch {
209
+ stopForeground(Service.STOP_FOREGROUND_REMOVE)
210
+ isForeground = false
211
+
212
+ hideAllNotifications()
213
+
214
+ val sessionsToRelease = mediaSessions.values.toList()
215
+ mediaSessions.clear()
216
+ for (session in sessionsToRelease) {
217
+ session.release()
218
+ }
219
+
220
+ val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
221
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
222
+ notificationManager.deleteNotificationChannel(CHANNEL_ID)
223
+ }
224
+ }
225
+ }
226
+
227
+ @MainThread
228
+ private fun hidePlayerNotification(player: ExoPlayer) {
229
+ val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
230
+ notificationManager.cancel(player.hashCode())
231
+ }
232
+
233
+ @MainThread
234
+ private fun hideAllNotifications() {
235
+ val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
236
+ notificationManager.cancelAll()
237
+ }
238
+
239
+ private fun MediaSession.wantsToShowNotification(): Boolean =
240
+ this.sessionExtras.getBoolean(SESSION_SHOW_NOTIFICATION, false)
241
+
242
+ companion object {
243
+ const val SEEK_FORWARD_COMMAND = "SEEK_FORWARD"
244
+ const val SEEK_BACKWARD_COMMAND = "SEEK_REWIND"
245
+ const val CHANNEL_ID = "PlaybackService"
246
+ const val SESSION_SHOW_NOTIFICATION = "showNotification"
247
+ const val SEEK_INTERVAL_MS = 10000L
248
+
249
+ fun startService(appContext: AppContext, context: Context, serviceConnection: PlaybackServiceConnection): Boolean {
250
+ appContext.reactContext?.apply {
251
+ val intent = Intent(context, ExpoVideoPlaybackService::class.java)
252
+ intent.action = SERVICE_INTERFACE
253
+
254
+ startService(intent)
255
+
256
+ val flags = if (Build.VERSION.SDK_INT >= 29) {
257
+ BIND_AUTO_CREATE or BIND_INCLUDE_CAPABILITIES
258
+ } else {
259
+ BIND_AUTO_CREATE
260
+ }
261
+
262
+ return bindService(intent, serviceConnection, flags)
263
+ }
264
+ return false
265
+ }
266
+ }
267
+ }
@@ -0,0 +1,62 @@
1
+ package expo.modules.video.playbackService
2
+
3
+ import android.content.ComponentName
4
+ import android.content.ServiceConnection
5
+ import android.os.IBinder
6
+ import androidx.annotation.OptIn
7
+ import androidx.media3.common.util.UnstableApi
8
+ import expo.modules.kotlin.AppContext
9
+ import expo.modules.kotlin.exception.Exceptions
10
+ import expo.modules.video.getPlaybackServiceErrorMessage
11
+ import expo.modules.video.player.VideoPlayer
12
+ import java.lang.ref.WeakReference
13
+
14
+ @OptIn(UnstableApi::class)
15
+ class PlaybackServiceConnection(val player: WeakReference<VideoPlayer>, appContext: AppContext) : ServiceConnection {
16
+ var playbackServiceBinder: PlaybackServiceBinder? = null
17
+ private set
18
+ var isConnected = false
19
+ private set
20
+ private val _appContext = WeakReference(appContext)
21
+ private val appContext: AppContext
22
+ get() = _appContext.get() ?: throw Exceptions.AppContextLost()
23
+
24
+ override fun onServiceConnected(componentName: ComponentName, binder: IBinder) {
25
+ val player = player.get() ?: return
26
+ val serviceBinder: PlaybackServiceBinder = binder as? PlaybackServiceBinder ?: run {
27
+ appContext.jsLogger?.error(
28
+ getPlaybackServiceErrorMessage("Expo-video could not bind to the playback service")
29
+ )
30
+ return
31
+ }
32
+
33
+ isConnected = true
34
+ playbackServiceBinder = serviceBinder
35
+ serviceBinder.service.appContext = appContext
36
+ serviceBinder.service.registerPlayer(player)
37
+ }
38
+
39
+ override fun onServiceDisconnected(componentName: ComponentName) {
40
+ playbackServiceBinder = null
41
+ isConnected = false
42
+ }
43
+
44
+ override fun onBindingDied(name: ComponentName?) {
45
+ isConnected = false
46
+ appContext.jsLogger?.error(
47
+ getPlaybackServiceErrorMessage(
48
+ "Expo-video has lost connection to the playback service binder",
49
+ "This will cause issues with now playing notification and sustaining background playback."
50
+ )
51
+ )
52
+ super.onBindingDied(name)
53
+ }
54
+
55
+ override fun onNullBinding(componentName: ComponentName) {
56
+ isConnected = false
57
+ appContext.jsLogger?.error(
58
+ getPlaybackServiceErrorMessage("Expo Video could not bind to the playback service")
59
+ )
60
+ super.onNullBinding(componentName)
61
+ }
62
+ }
@@ -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
+ }