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

View File

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

View File

@@ -21,10 +21,17 @@ fun VideoPlaybackService.Companion.startService(
val intent = Intent(context, VideoPlaybackService::class.java) val intent = Intent(context, VideoPlaybackService::class.java)
intent.action = VIDEO_PLAYBACK_SERVICE_INTERFACE 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) { 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 { } else {
reactContext.startService(intent); reactContext.startService(intent)
} }
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 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.SimpleBitmapLoader
import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService 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.NitroModules
import com.margelo.nitro.video.HybridVideoPlayer import com.margelo.nitro.video.HybridVideoPlayer
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
@@ -25,12 +30,28 @@ class VideoPlaybackService : MediaSessionService() {
private var mediaSessionsList = mutableMapOf<HybridVideoPlayer, MediaSession>() private var mediaSessionsList = mutableMapOf<HybridVideoPlayer, MediaSession>()
private var binder = VideoPlaybackServiceBinder(this) private var binder = VideoPlaybackServiceBinder(this)
private var sourceActivity: Class<Activity>? = null // retained for future deep-links; currently unused private var sourceActivity: Class<Activity>? = null // retained for future deep-links; currently unused
private var isForeground = false
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
setMediaNotificationProvider(CustomMediaNotificationProvider(this)) 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 // Player Registry
fun registerPlayer(player: HybridVideoPlayer, from: Class<Activity>) { fun registerPlayer(player: HybridVideoPlayer, from: Class<Activity>) {
if (mediaSessionsList.containsKey(player)) { if (mediaSessionsList.containsKey(player)) {
@@ -113,7 +134,9 @@ class VideoPlaybackService : MediaSessionService() {
private fun stopForegroundSafely() { private fun stopForegroundSafely() {
try { try {
stopForeground(STOP_FOREGROUND_REMOVE) stopForeground(STOP_FOREGROUND_REMOVE)
} catch (_: Exception) {} } catch (_: Exception) {
Log.e(TAG, "Failed to stop foreground service!")
}
} }
private fun cleanup() { private fun cleanup() {
@@ -128,11 +151,50 @@ class VideoPlaybackService : MediaSessionService() {
// Stop the service if there are no active media sessions (no players need it) // Stop the service if there are no active media sessions (no players need it)
fun stopIfNoPlayers() { fun stopIfNoPlayers() {
if (mediaSessionsList.isEmpty()) { 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() cleanup()
} }
} }
companion object { companion object {
const val TAG = "VideoPlaybackService"
const val VIDEO_PLAYBACK_SERVICE_INTERFACE = SERVICE_INTERFACE 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 <androidx.media3.ui.PlayerView
android:id="@+id/player_view_surface" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="match_parent" android:id="@+id/player_view_surface"
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_height="match_parent"
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:id="@+id/player_view_texture"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:surface_type="texture_view"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" 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"
/>