From 8ce38cab1bc7de226b5152fca082d1de98d02665 Mon Sep 17 00:00:00 2001 From: Krzysztof Moch Date: Wed, 15 Oct 2025 15:50:47 +0200 Subject: [PATCH] fix(android): call start foreground service if needed (#4733) --- bun.lock | 6 +- example/ios/Podfile.lock | 8 +-- .../VideoPlaybackService+ServiceManagment.kt | 11 +++- .../services/playback/VideoPlaybackService.kt | 64 ++++++++++++++++++- .../main/res/layout/player_view_surface.xml | 21 ++++-- .../main/res/layout/player_view_texture.xml | 10 ++- 6 files changed, 102 insertions(+), 18 deletions(-) diff --git a/bun.lock b/bun.lock index 053bf4bc..009ab8c5 100644 --- a/bun.lock +++ b/bun.lock @@ -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", diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 03440cc2..65bb1846 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -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 diff --git a/packages/react-native-video/android/src/main/java/com/twg/video/core/extensions/VideoPlaybackService+ServiceManagment.kt b/packages/react-native-video/android/src/main/java/com/twg/video/core/extensions/VideoPlaybackService+ServiceManagment.kt index 52e29ac7..4abe4a93 100644 --- a/packages/react-native-video/android/src/main/java/com/twg/video/core/extensions/VideoPlaybackService+ServiceManagment.kt +++ b/packages/react-native-video/android/src/main/java/com/twg/video/core/extensions/VideoPlaybackService+ServiceManagment.kt @@ -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) { diff --git a/packages/react-native-video/android/src/main/java/com/twg/video/core/services/playback/VideoPlaybackService.kt b/packages/react-native-video/android/src/main/java/com/twg/video/core/services/playback/VideoPlaybackService.kt index bd10a6f2..a373ff94 100644 --- a/packages/react-native-video/android/src/main/java/com/twg/video/core/services/playback/VideoPlaybackService.kt +++ b/packages/react-native-video/android/src/main/java/com/twg/video/core/services/playback/VideoPlaybackService.kt @@ -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() private var binder = VideoPlaybackServiceBinder(this) private var sourceActivity: Class? = 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) { 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() } } diff --git a/packages/react-native-video/android/src/main/res/layout/player_view_surface.xml b/packages/react-native-video/android/src/main/res/layout/player_view_surface.xml index abc3f647..b8158a3b 100644 --- a/packages/react-native-video/android/src/main/res/layout/player_view_surface.xml +++ b/packages/react-native-video/android/src/main/res/layout/player_view_surface.xml @@ -1,7 +1,14 @@ - \ No newline at end of file + diff --git a/packages/react-native-video/android/src/main/res/layout/player_view_texture.xml b/packages/react-native-video/android/src/main/res/layout/player_view_texture.xml index 210a01b7..c0d2588c 100644 --- a/packages/react-native-video/android/src/main/res/layout/player_view_texture.xml +++ b/packages/react-native-video/android/src/main/res/layout/player_view_texture.xml @@ -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" /> \ No newline at end of file + 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" + /> \ No newline at end of file