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.
- package/CHANGELOG.md +34 -0
- package/FORK.md +149 -0
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/android/build.gradle +32 -0
- package/android/src/main/AndroidManifest.xml +10 -0
- package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +258 -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/PlayerViewExtension.kt +87 -0
- package/android/src/main/java/expo/modules/video/VideoCache.kt +109 -0
- package/android/src/main/java/expo/modules/video/VideoExceptions.kt +45 -0
- package/android/src/main/java/expo/modules/video/VideoModule.kt +446 -0
- package/android/src/main/java/expo/modules/video/VideoThumbnail.kt +20 -0
- package/android/src/main/java/expo/modules/video/VideoView.kt +457 -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/FullscreenOrientation.kt +25 -0
- package/android/src/main/java/expo/modules/video/enums/PlayerStatus.kt +10 -0
- package/android/src/main/java/expo/modules/video/enums/VideoRange.kt +27 -0
- package/android/src/main/java/expo/modules/video/listeners/VideoManagerListener.kt +10 -0
- package/android/src/main/java/expo/modules/video/listeners/VideoPlayerListener.kt +35 -0
- package/android/src/main/java/expo/modules/video/listeners/VideoViewListener.kt +13 -0
- package/android/src/main/java/expo/modules/video/managers/AudioFocusManager.kt +242 -0
- package/android/src/main/java/expo/modules/video/managers/PictureInPictureManager.kt +329 -0
- package/android/src/main/java/expo/modules/video/managers/VideoManager.kt +194 -0
- package/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt +267 -0
- package/android/src/main/java/expo/modules/video/playbackService/PlaybackServiceConnection.kt +62 -0
- package/android/src/main/java/expo/modules/video/playbackService/VideoMediaSessionCallback.kt +47 -0
- package/android/src/main/java/expo/modules/video/player/DefaultLoadControl.java +552 -0
- package/android/src/main/java/expo/modules/video/player/FirstFrameEventGenerator.kt +116 -0
- package/android/src/main/java/expo/modules/video/player/PlayerEvent.kt +172 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayer.kt +598 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerAudioTracks.kt +126 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerKeepAwake.kt +91 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerLoadControl.kt +57 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerSubtitles.kt +126 -0
- package/android/src/main/java/expo/modules/video/records/BufferOptions.kt +18 -0
- package/android/src/main/java/expo/modules/video/records/ButtonOptions.kt +17 -0
- package/android/src/main/java/expo/modules/video/records/DRMOptions.kt +27 -0
- package/android/src/main/java/expo/modules/video/records/FullscreenOptions.kt +14 -0
- package/android/src/main/java/expo/modules/video/records/PiPParams.kt +38 -0
- package/android/src/main/java/expo/modules/video/records/PlaybackError.kt +21 -0
- package/android/src/main/java/expo/modules/video/records/PlayerBuilderOptions.kt +15 -0
- package/android/src/main/java/expo/modules/video/records/ScrubbingModeOptions.kt +33 -0
- package/android/src/main/java/expo/modules/video/records/SeekTolerance.kt +30 -0
- package/android/src/main/java/expo/modules/video/records/SubtitleSource.kt +54 -0
- package/android/src/main/java/expo/modules/video/records/SubtitleStyle.kt +98 -0
- package/android/src/main/java/expo/modules/video/records/Tracks.kt +115 -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 +14 -0
- package/android/src/main/java/expo/modules/video/records/VideoSize.kt +16 -0
- package/android/src/main/java/expo/modules/video/records/VideoSource.kt +113 -0
- package/android/src/main/java/expo/modules/video/records/VideoThumbnailOptions.kt +26 -0
- package/android/src/main/java/expo/modules/video/utils/DataSourceUtils.kt +76 -0
- package/android/src/main/java/expo/modules/video/utils/EventDispatcherUtils.kt +43 -0
- package/android/src/main/java/expo/modules/video/utils/FullscreenActivityOrientationHelper.kt +120 -0
- package/android/src/main/java/expo/modules/video/utils/MediaSessionUtils.kt +14 -0
- package/android/src/main/java/expo/modules/video/utils/MutableWeakReference.kt +26 -0
- package/android/src/main/java/expo/modules/video/utils/PictureInPictureHelperFragment.kt +49 -0
- package/android/src/main/java/expo/modules/video/utils/PictureInPictureUtils.kt +96 -0
- package/android/src/main/java/expo/modules/video/utils/SubtitleUtils.kt +68 -0
- package/android/src/main/java/expo/modules/video/utils/ViewVisibilityUtils.kt +28 -0
- package/android/src/main/java/expo/modules/video/utils/WeakMutableSet.kt +120 -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 +20 -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/NativeVideoAirPlayButtonView.d.ts +3 -0
- package/build/NativeVideoAirPlayButtonView.d.ts.map +1 -0
- package/build/NativeVideoAirPlayButtonView.js +3 -0
- package/build/NativeVideoAirPlayButtonView.js.map +1 -0
- package/build/NativeVideoModule.d.ts +13 -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 +5 -0
- package/build/NativeVideoModule.web.d.ts.map +1 -0
- package/build/NativeVideoModule.web.js +5 -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/VideoAirPlayButton.d.ts +10 -0
- package/build/VideoAirPlayButton.d.ts.map +1 -0
- package/build/VideoAirPlayButton.ios.d.ts +3 -0
- package/build/VideoAirPlayButton.ios.d.ts.map +1 -0
- package/build/VideoAirPlayButton.ios.js +6 -0
- package/build/VideoAirPlayButton.ios.js.map +1 -0
- package/build/VideoAirPlayButton.js +13 -0
- package/build/VideoAirPlayButton.js.map +1 -0
- package/build/VideoAirPlayButton.types.d.ts +35 -0
- package/build/VideoAirPlayButton.types.d.ts.map +1 -0
- package/build/VideoAirPlayButton.types.js +2 -0
- package/build/VideoAirPlayButton.types.js.map +1 -0
- package/build/VideoModule.d.ts +35 -0
- package/build/VideoModule.d.ts.map +1 -0
- package/build/VideoModule.js +44 -0
- package/build/VideoModule.js.map +1 -0
- package/build/VideoPlayer.d.ts +17 -0
- package/build/VideoPlayer.d.ts.map +1 -0
- package/build/VideoPlayer.js +55 -0
- package/build/VideoPlayer.js.map +1 -0
- package/build/VideoPlayer.types.d.ts +782 -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 +79 -0
- package/build/VideoPlayer.web.d.ts.map +1 -0
- package/build/VideoPlayer.web.js +388 -0
- package/build/VideoPlayer.web.js.map +1 -0
- package/build/VideoPlayerEvents.types.d.ts +278 -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 +4 -0
- package/build/VideoThumbnail.js.map +1 -0
- package/build/VideoView.d.ts +69 -0
- package/build/VideoView.d.ts.map +1 -0
- package/build/VideoView.js +100 -0
- package/build/VideoView.js.map +1 -0
- package/build/VideoView.types.d.ts +362 -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 +239 -0
- package/build/VideoView.web.js.map +1 -0
- package/build/index.d.ts +11 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +6 -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 +46 -0
- package/ios/Cache/CacheVideoAssetTransportProvider.swift +64 -0
- package/ios/Cache/CachedResource.swift +164 -0
- package/ios/Cache/CachingHelpers.swift +107 -0
- package/ios/Cache/MediaFileHandle.swift +139 -0
- package/ios/Cache/MediaInfo.swift +185 -0
- package/ios/Cache/ResourceLoaderDelegate.swift +306 -0
- package/ios/Cache/SynchronizedHashTable.swift +23 -0
- package/ios/Cache/VideoCacheManager.swift +192 -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 +11 -0
- package/ios/Enums/DRMType.swift +20 -0
- package/ios/Enums/FullscreenOrientation.swift +34 -0
- package/ios/Enums/KeepFullscreenOnPiPStopBehavior.swift +20 -0
- package/ios/Enums/PlayerStatus.swift +10 -0
- package/ios/Enums/VideoContentFit.swift +39 -0
- package/ios/Enums/VideoRange.swift +21 -0
- package/ios/ExpoVideo.podspec +27 -0
- package/ios/NowPlayingManager.swift +304 -0
- package/ios/OrientationAVPlayerViewController.swift +263 -0
- package/ios/Records/BufferOptions.swift +12 -0
- package/ios/Records/DRMOptions.swift +24 -0
- package/ios/Records/FullscreenOptions.swift +14 -0
- package/ios/Records/PlaybackError.swift +10 -0
- package/ios/Records/ScrubbingModeOptions.swift +7 -0
- package/ios/Records/SeekTolerance.swift +20 -0
- package/ios/Records/SubtitleSource.swift +32 -0
- package/ios/Records/SubtitleStyle.swift +118 -0
- package/ios/Records/Tracks.swift +229 -0
- package/ios/Records/VideoEventPayloads.swift +81 -0
- package/ios/Records/VideoMetadata.swift +16 -0
- package/ios/Records/VideoSize.swift +15 -0
- package/ios/Records/VideoSource.swift +28 -0
- package/ios/Thumbnails/VideoThumbnail.swift +27 -0
- package/ios/Thumbnails/VideoThumbnailGenerator.swift +68 -0
- package/ios/Thumbnails/VideoThumbnailOptions.swift +15 -0
- package/ios/Utils/AVAssetVariant+VideoTracks.swift +20 -0
- package/ios/Utils/AvAssetTrack+VideoTracks.swift +79 -0
- package/ios/Utils/FourCharCode+toString.swift +25 -0
- package/ios/Utils/HlsUriUtils.swift +27 -0
- package/ios/Utils/URL+MediaLibraryAssets.swift +68 -0
- package/ios/VideoAirPlayButtonView.swift +70 -0
- package/ios/VideoAsset.swift +98 -0
- package/ios/VideoAssetTransport/VideoAssetLoadPlan.swift +69 -0
- package/ios/VideoAssetTransport/VideoAssetSourceDescriptor.swift +29 -0
- package/ios/VideoAssetTransport/VideoAssetTransportProvider.swift +24 -0
- package/ios/VideoAssetTransport/VideoAssetTransportRegistry.swift +103 -0
- package/ios/VideoExceptions.swift +53 -0
- package/ios/VideoItem.swift +11 -0
- package/ios/VideoManager.swift +182 -0
- package/ios/VideoModule.swift +413 -0
- package/ios/VideoPlayer/DangerousPropertiesStore.swift +19 -0
- package/ios/VideoPlayer.swift +485 -0
- package/ios/VideoPlayerAudioTracks.swift +109 -0
- package/ios/VideoPlayerItem.swift +169 -0
- package/ios/VideoPlayerObserver.swift +612 -0
- package/ios/VideoPlayerSeeker.swift +77 -0
- package/ios/VideoPlayerSubtitleSideload.swift +203 -0
- package/ios/VideoPlayerSubtitles.swift +88 -0
- package/ios/VideoSourceLoader.swift +90 -0
- package/ios/VideoSourceLoaderListener.swift +34 -0
- package/ios/VideoView.swift +201 -0
- package/package.json +51 -0
- package/plugin/build/index.d.ts +3 -0
- package/plugin/build/index.js +3 -0
- package/plugin/build/withExpoVideo.d.ts +9 -0
- package/plugin/build/withExpoVideo.js +73 -0
- package/plugin/index.d.ts +1 -0
- package/plugin/index.js +1 -0
- package/plugin/jest.config.js +1 -0
- package/plugin/src/index.ts +3 -0
- package/plugin/src/withExpoVideo.ts +103 -0
- package/plugin/tsconfig.json +10 -0
- package/src/NativeVideoAirPlayButtonView.ts +3 -0
- package/src/NativeVideoModule.ts +16 -0
- package/src/NativeVideoModule.web.ts +4 -0
- package/src/NativeVideoView.ts +8 -0
- package/src/VideoAirPlayButton.ios.tsx +8 -0
- package/src/VideoAirPlayButton.tsx +14 -0
- package/src/VideoAirPlayButton.types.ts +39 -0
- package/src/VideoModule.ts +47 -0
- package/src/VideoPlayer.tsx +74 -0
- package/src/VideoPlayer.types.ts +893 -0
- package/src/VideoPlayer.web.tsx +466 -0
- package/src/VideoPlayerEvents.types.ts +332 -0
- package/src/VideoThumbnail.ts +31 -0
- package/src/VideoView.tsx +111 -0
- package/src/VideoView.types.ts +405 -0
- package/src/VideoView.web.tsx +293 -0
- package/src/index.ts +26 -0
- package/src/resolveAssetSource.ts +2 -0
- package/src/resolveAssetSource.web.ts +17 -0
- package/src/ts-declarations/react-native-assets.d.ts +22 -0
- package/tsconfig.all.json +11 -0
- package/tsconfig.json +10 -0
- 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
|
+
}
|