fix(android): call start foreground service if needed (#4733)

This commit is contained in:
Krzysztof Moch
2025-10-15 15:50:47 +02:00
committed by GitHub
parent 52499e5af7
commit 8ce38cab1b
6 changed files with 102 additions and 18 deletions

View File

@@ -51,7 +51,7 @@
},
"example": {
"name": "react-native-video-example",
"version": "7.0.0-alpha.5",
"version": "7.0.0-alpha.6",
"dependencies": {
"@react-native-community/slider": "^4.5.6",
"@react-native-video/drm": "*",
@@ -79,7 +79,7 @@
},
"packages/drm-plugin": {
"name": "@react-native-video/drm",
"version": "7.0.0-alpha.5",
"version": "7.0.0-alpha.6",
"devDependencies": {
"@react-native/babel-preset": "0.79.2",
"@release-it/conventional-changelog": "^9.0.2",
@@ -107,7 +107,7 @@
},
"packages/react-native-video": {
"name": "react-native-video",
"version": "7.0.0-alpha.5",
"version": "7.0.0-alpha.6",
"devDependencies": {
"@expo/config-plugins": "^10.0.2",
"@react-native/eslint-config": "^0.77.0",

View File

@@ -1565,7 +1565,7 @@ PODS:
- React-logger (= 0.77.2)
- React-perflogger (= 0.77.2)
- React-utils (= 0.77.2)
- ReactNativeVideo (7.0.0-alpha.5):
- ReactNativeVideo (7.0.0-alpha.6):
- DoubleConversion
- glog
- hermes-engine
@@ -1587,7 +1587,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- ReactNativeVideoDrm (7.0.0-alpha.5):
- ReactNativeVideoDrm (7.0.0-alpha.6):
- DoubleConversion
- glog
- hermes-engine
@@ -1904,8 +1904,8 @@ SPEC CHECKSUMS:
ReactAppDependencyProvider: f334cebc0beed0a72490492e978007082c03d533
ReactCodegen: 474fbb3e4bb0f1ee6c255d1955db76e13d509269
ReactCommon: 7763e59534d58e15f8f22121cdfe319040e08888
ReactNativeVideo: 705a2a90d9f04afff9afd90d4ef194e1bc1135d5
ReactNativeVideoDrm: 2e0844e18cd8024078da2762a749420c5268cf18
ReactNativeVideo: 6290dbf881cdeb58c09b5aef1af1245aebf5a207
ReactNativeVideoDrm: 0664dcc3ccac781f6fd00329cb890b6d1f15c392
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: 31a098f74c16780569aebd614a0f37a907de0189

View File

@@ -21,10 +21,17 @@ fun VideoPlaybackService.Companion.startService(
val intent = Intent(context, VideoPlaybackService::class.java)
intent.action = VIDEO_PLAYBACK_SERVICE_INTERFACE
// Use startForegroundService on O+ so the service has the opportunity to call
// startForeground(...) quickly and avoid ForegroundServiceDidNotStartInTimeException.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
reactContext.startForegroundService(intent);
try {
reactContext.startForegroundService(intent)
} catch (_: Exception) {
// Fall back to startService if anything goes wrong
try { reactContext.startService(intent) } catch (_: Exception) {}
}
} else {
reactContext.startService(intent);
reactContext.startService(intent)
}
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {

View File

@@ -13,6 +13,11 @@ import androidx.media3.session.DefaultMediaNotificationProvider
import androidx.media3.session.SimpleBitmapLoader
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.os.Build
import androidx.core.app.NotificationCompat
import com.margelo.nitro.NitroModules
import com.margelo.nitro.video.HybridVideoPlayer
import java.util.concurrent.ExecutorService
@@ -25,12 +30,28 @@ class VideoPlaybackService : MediaSessionService() {
private var mediaSessionsList = mutableMapOf<HybridVideoPlayer, MediaSession>()
private var binder = VideoPlaybackServiceBinder(this)
private var sourceActivity: Class<Activity>? = null // retained for future deep-links; currently unused
private var isForeground = false
override fun onCreate() {
super.onCreate()
setMediaNotificationProvider(CustomMediaNotificationProvider(this))
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// Ensure we call startForeground quickly on newer Android versions to avoid
// ForegroundServiceDidNotStartInTimeException when startForegroundService(...) was used.
try {
if (!isForeground && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForeground(PLACEHOLDER_NOTIFICATION_ID, createPlaceholderNotification())
isForeground = true
}
} catch (_: Exception) {
Log.e(TAG, "Failed to start foreground service!")
}
return super.onStartCommand(intent, flags, startId)
}
// Player Registry
fun registerPlayer(player: HybridVideoPlayer, from: Class<Activity>) {
if (mediaSessionsList.containsKey(player)) {
@@ -113,7 +134,9 @@ class VideoPlaybackService : MediaSessionService() {
private fun stopForegroundSafely() {
try {
stopForeground(STOP_FOREGROUND_REMOVE)
} catch (_: Exception) {}
} catch (_: Exception) {
Log.e(TAG, "Failed to stop foreground service!")
}
}
private fun cleanup() {
@@ -128,11 +151,50 @@ class VideoPlaybackService : MediaSessionService() {
// Stop the service if there are no active media sessions (no players need it)
fun stopIfNoPlayers() {
if (mediaSessionsList.isEmpty()) {
// Remove placeholder notification and stop the service when no active players exist
try {
if (isForeground) {
stopForegroundSafely()
isForeground = false
}
} catch (_: Exception) {
Log.e(TAG, "Failed to stop foreground service!")
}
cleanup()
}
}
companion object {
const val TAG = "VideoPlaybackService"
const val VIDEO_PLAYBACK_SERVICE_INTERFACE = SERVICE_INTERFACE
private const val PLACEHOLDER_NOTIFICATION_ID = 1729
private const val NOTIFICATION_CHANNEL_ID = "twg_video_playback"
}
private fun createPlaceholderNotification(): Notification {
val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
"Media playback",
NotificationManager.IMPORTANCE_LOW
)
channel.setShowBadge(false)
nm.createNotificationChannel(channel)
} catch (_: Exception) {
Log.e(TAG, "Failed to create notification channel!")
}
}
val appName = try { applicationInfo.loadLabel(packageManager).toString() } catch (_: Exception) { "Media Playback" }
return NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_media_play)
.setContentTitle(appName)
.setContentText("")
.setOngoing(true)
.setCategory(Notification.CATEGORY_SERVICE)
.build()
}
}

View File

@@ -1,7 +1,14 @@
<androidx.media3.ui.PlayerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/player_view_surface"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:surface_type="surface_view" />
android:clipChildren="false"
android:clipToPadding="false"
app:surface_type="surface_view"
app:scrubber_enabled_size="0dp"
app:scrubber_disabled_size="0dp"
app:scrubber_dragged_size="0dp"
app:touch_target_height="12dp"
app:bar_height="5dp" />

View File

@@ -2,6 +2,14 @@
android:id="@+id/player_view_texture"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:surface_type="texture_view"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:surface_type="texture_view" />
android:clipChildren="false"
android:clipToPadding="false"
app:scrubber_enabled_size="0dp"
app:scrubber_disabled_size="0dp"
app:scrubber_dragged_size="0dp"
app:touch_target_height="12dp"
app:bar_height="5dp"
/>