diff --git a/android/src/main/java/com/brentvatne/exoplayer/PictureInPictureUtil.kt b/android/src/main/java/com/brentvatne/exoplayer/PictureInPictureUtil.kt index 5e36ef2582..3eba51dd75 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/PictureInPictureUtil.kt +++ b/android/src/main/java/com/brentvatne/exoplayer/PictureInPictureUtil.kt @@ -24,13 +24,18 @@ import com.brentvatne.common.toolbox.DebugLog import com.brentvatne.receiver.PictureInPictureReceiver import com.facebook.react.uimanager.ThemedReactContext -internal fun Context.findActivity(): ComponentActivity { +internal fun Context.findActivity(): ComponentActivity? { var context = this while (context is ContextWrapper) { if (context is ComponentActivity) return context context = context.baseContext } - throw IllegalStateException("Picture in picture should be called in the context of an Activity") + // Fallback: ThemedReactContext stores the activity separately from the ContextWrapper chain + if (this is ThemedReactContext) { + val activity = this.getCurrentActivity() + if (activity is ComponentActivity) return activity + } + return null } object PictureInPictureUtil { @@ -39,7 +44,7 @@ object PictureInPictureUtil { @JvmStatic fun addLifecycleEventListener(context: ThemedReactContext, view: ReactExoplayerView): Runnable { - val activity = context.findActivity() + val activity = context.findActivity() ?: return Runnable {} val onPictureInPictureModeChanged = Consumer { info -> view.setIsInPictureInPicture(info.isInPictureInPictureMode) @@ -73,16 +78,17 @@ object PictureInPictureUtil { @JvmStatic fun enterPictureInPictureMode(context: ThemedReactContext, pictureInPictureParams: PictureInPictureParams?) { if (!isSupportPictureInPicture(context)) return + val activity = context.findActivity() ?: return if (isSupportPictureInPictureAction() && pictureInPictureParams != null) { try { - context.findActivity().enterPictureInPictureMode(pictureInPictureParams) + activity.enterPictureInPictureMode(pictureInPictureParams) } catch (e: IllegalStateException) { DebugLog.e(TAG, e.toString()) } } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { try { @Suppress("DEPRECATION") - context.findActivity().enterPictureInPictureMode() + activity.enterPictureInPictureMode() } catch (e: IllegalStateException) { DebugLog.e(TAG, e.toString()) } @@ -119,8 +125,9 @@ object PictureInPictureUtil { private fun updatePictureInPictureActions(context: ThemedReactContext, pipParams: PictureInPictureParams) { if (!isSupportPictureInPictureAction()) return if (!isSupportPictureInPicture(context)) return + val activity = context.findActivity() ?: return try { - context.findActivity().setPictureInPictureParams(pipParams) + activity.setPictureInPictureParams(pipParams) } catch (e: IllegalStateException) { DebugLog.e(TAG, e.toString()) } @@ -196,7 +203,7 @@ object PictureInPictureUtil { } private fun checkIsUserAllowPIP(context: ThemedReactContext): Boolean { - val activity = context.currentActivity ?: return false + val activity = context.getCurrentActivity() ?: return false return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @SuppressLint("InlinedApi") val result = AppOpsManagerCompat.noteOpNoThrow( diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index e16ac96d8a..3fe22bc69e 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -88,6 +88,9 @@ import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.source.MergingMediaSource; +import androidx.media3.extractor.Extractor; +import androidx.media3.extractor.text.DefaultSubtitleParserFactory; +import androidx.media3.extractor.text.SubtitleExtractor; import androidx.media3.exoplayer.source.ProgressiveMediaSource; import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.source.ads.AdsMediaSource; @@ -1144,12 +1147,48 @@ private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessi ? overridenMediaItemBuilder.build() : mediaItemBuilder.build(); + // Strip subtitle configurations before handing the MediaItem to the factory so that + // no factory variant (including overridden DefaultMediaSourceFactory) creates its own + // subtitle sources via legacy paths. We handle all subtitle merging below. + MediaItem mediaItemForSource = mediaItem.buildUpon() + .setSubtitleConfigurations(ImmutableList.of()) + .build(); MediaSource mediaSource = mediaSourceFactory .setDrmSessionManagerProvider(drmProvider) .setLoadErrorHandlingPolicy( config.buildLoadErrorHandlingPolicy(source.getMinLoadRetryCount()) ) - .createMediaSource(mediaItem); + .createMediaSource(mediaItemForSource); + + // ProgressiveMediaSource.Factory (and HLS/DASH variants) silently ignore + // MediaItem.SubtitleConfiguration entries. Manually merge sideloaded subtitle + // sources so they appear in MappedTrackInfo and can be selected via setSelectedTextTrack. + if (mediaItem.localConfiguration != null && !mediaItem.localConfiguration.subtitleConfigurations.isEmpty()) { + DataSource.Factory subtitleDataSourceFactory = buildDataSourceFactory(false); + DefaultSubtitleParserFactory subtitleParserFactory = new DefaultSubtitleParserFactory(); + List mergedSources = new ArrayList<>(); + mergedSources.add(mediaSource); + for (MediaItem.SubtitleConfiguration subtitleConfig : mediaItem.localConfiguration.subtitleConfigurations) { + Format subtitleFormat = new Format.Builder() + .setSampleMimeType(subtitleConfig.mimeType) + .setLanguage(subtitleConfig.language) + .setLabel(subtitleConfig.label) + .setSelectionFlags(subtitleConfig.selectionFlags) + .setRoleFlags(subtitleConfig.roleFlags) + .build(); + if (!subtitleParserFactory.supportsFormat(subtitleFormat)) { + DebugLog.w(TAG, "Skipping sideloaded subtitle track with unsupported format: " + subtitleConfig.mimeType); + continue; + } + mergedSources.add(new ProgressiveMediaSource.Factory( + subtitleDataSourceFactory, + () -> new Extractor[]{new SubtitleExtractor(subtitleParserFactory.create(subtitleFormat), subtitleFormat)} + ).createMediaSource(MediaItem.fromUri(subtitleConfig.uri))); + } + if (mergedSources.size() > 1) { + mediaSource = new MergingMediaSource(mergedSources.toArray(new MediaSource[0])); + } + } if (cropStartMs >= 0 && cropEndMs >= 0) { return new ClippingMediaSource(mediaSource, cropStartMs * 1000, cropEndMs * 1000);