diff --git a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.kt b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.kt index 2d3f8cae..ec131ade 100644 --- a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.kt +++ b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.kt @@ -110,21 +110,23 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute } fun setPlayer(player: ExoPlayer?) { - val currentPlayer = playerView.player - - if (currentPlayer != null) { - currentPlayer.removeListener(playerListener) - } - playerView.player = player + player?.addListener(playerListener) + } - if (player != null) { - player.addListener(playerListener) - - // Apply pending resize mode if we have one - pendingResizeMode?.let { resizeMode -> - playerView.resizeMode = resizeMode - } + fun setResizeMode(@ResizeMode.Mode mode: Int) { + val resizeMode = when (mode) { + ResizeMode.RESIZE_MODE_FIT -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT + ResizeMode.RESIZE_MODE_FIXED_WIDTH -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH + ResizeMode.RESIZE_MODE_FIXED_HEIGHT -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT + ResizeMode.RESIZE_MODE_FILL -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FILL + ResizeMode.RESIZE_MODE_ZOOM -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM + else -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT + } + if (playerView.width > 0 && playerView.height > 0) { + playerView.resizeMode = resizeMode + } else { + pendingResizeMode = resizeMode } // Re-assert subtitle rendering mode for the current style. @@ -134,27 +136,34 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute fun getPlayerView(): PlayerView = playerView - fun setResizeMode(@ResizeMode.Mode resizeMode: Int) { - val targetResizeMode = when (resizeMode) { - ResizeMode.RESIZE_MODE_FILL -> AspectRatioFrameLayout.RESIZE_MODE_FILL - ResizeMode.RESIZE_MODE_CENTER_CROP -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM - ResizeMode.RESIZE_MODE_FIT -> AspectRatioFrameLayout.RESIZE_MODE_FIT - ResizeMode.RESIZE_MODE_FIXED_WIDTH -> AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH - ResizeMode.RESIZE_MODE_FIXED_HEIGHT -> AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT - else -> AspectRatioFrameLayout.RESIZE_MODE_FIT - } + fun setShowSubtitleButton(show: Boolean) { + playerView.setShowSubtitleButton(show) + } - // Apply the resize mode to PlayerView immediately - playerView.resizeMode = targetResizeMode + fun setUseController(useController: Boolean) { + playerView.useController = useController + } - // Store it for reapplication if needed - pendingResizeMode = targetResizeMode + fun setControllerHideOnTouch(hideOnTouch: Boolean) { + playerView.setControllerHideOnTouch(hideOnTouch) + } - // Force PlayerView to recalculate its layout - playerView.requestLayout() + fun setControllerAutoShow(autoShow: Boolean) { + playerView.setControllerAutoShow(autoShow) + } - // Also request layout on the parent to ensure proper sizing - requestLayout() + fun setControllerShowTimeoutMs(timeoutMs: Int) { + playerView.controllerShowTimeoutMs = timeoutMs + } + + fun isControllerVisible(): Boolean = playerView.isControllerFullyVisible + + fun hideController() { + playerView.hideController() + } + + fun showController() { + playerView.showController() } fun setSubtitleStyle(style: SubtitleStyle) { @@ -281,89 +290,16 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute playerView.setShutterBackgroundColor(color) } - fun updateSurfaceView(viewType: Int) { - // TODO: Implement proper surface type switching if needed + fun setShowLiveBadge(show: Boolean) { + liveBadge.visibility = if (show) View.VISIBLE else View.GONE } - val isPlaying: Boolean - get() = playerView.player?.isPlaying ?: false - fun invalidateAspectRatio() { - // PlayerView handles aspect ratio automatically through its internal AspectRatioFrameLayout - playerView.requestLayout() - - // Reapply the current resize mode to ensure it's properly set - pendingResizeMode?.let { resizeMode -> - playerView.resizeMode = resizeMode + playerView.post { + playerView.requestLayout() } } - fun setUseController(useController: Boolean) { - playerView.useController = useController - if (useController) { - // Ensure proper touch handling when controls are enabled - playerView.controllerAutoShow = true - playerView.controllerHideOnTouch = true - // Show controls immediately when enabled - playerView.showController() - } - } - - fun showController() { - playerView.showController() - } - - fun hideController() { - playerView.hideController() - } - - fun setControllerShowTimeoutMs(showTimeoutMs: Int) { - playerView.controllerShowTimeoutMs = showTimeoutMs - } - - fun setControllerAutoShow(autoShow: Boolean) { - playerView.controllerAutoShow = autoShow - } - - fun setControllerHideOnTouch(hideOnTouch: Boolean) { - playerView.controllerHideOnTouch = hideOnTouch - } - - fun setFullscreenButtonClickListener(listener: PlayerView.FullscreenButtonClickListener?) { - playerView.setFullscreenButtonClickListener(listener) - } - - fun setShowSubtitleButton(show: Boolean) { - playerView.setShowSubtitleButton(show) - } - - fun isControllerVisible(): Boolean = playerView.isControllerFullyVisible - - fun setControllerVisibilityListener(listener: PlayerView.ControllerVisibilityListener?) { - playerView.setControllerVisibilityListener(listener) - } - - override fun addOnLayoutChangeListener(listener: View.OnLayoutChangeListener) { - playerView.addOnLayoutChangeListener(listener) - } - - override fun setFocusable(focusable: Boolean) { - playerView.isFocusable = focusable - } - - private fun updateLiveUi() { - val player = playerView.player ?: return - val isLive = player.isCurrentMediaItemLive - val seekable = player.isCurrentMediaItemSeekable - - // Show/hide badge - liveBadge.visibility = if (isLive) View.VISIBLE else View.GONE - - // Disable/enable scrubbing based on seekable - val timeBar = playerView.findViewById(androidx.media3.ui.R.id.exo_progress) - timeBar?.isEnabled = !isLive || seekable - } - private val playerListener = object : Player.Listener { override fun onCues(cueGroup: CueGroup) { // Keep overlay subtitles in sync. This does NOT interfere with PlayerView's own subtitle rendering. @@ -375,61 +311,15 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute override fun onTimelineChanged(timeline: Timeline, reason: Int) { playerView.post { playerView.requestLayout() - // Reapply resize mode to ensure it's properly set after timeline changes - pendingResizeMode?.let { resizeMode -> - playerView.resizeMode = resizeMode - } - } - updateLiveUi() - } - - override fun onEvents(player: Player, events: Player.Events) { - if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION) || - events.contains(Player.EVENT_IS_PLAYING_CHANGED) - ) { - updateLiveUi() - } - - // Handle video size changes which affect aspect ratio - if (events.contains(Player.EVENT_VIDEO_SIZE_CHANGED)) { - pendingResizeMode?.let { resizeMode -> - playerView.resizeMode = resizeMode - } - playerView.requestLayout() - requestLayout() } } } - companion object { - private const val TAG = "ExoPlayerView" - } - - /** - * React Native (Yoga) can sometimes defer layout passes that are required by - * PlayerView for its child views (controller overlay, surface view, subtitle view, …). - * This helper forces a second measure / layout after RN finishes, ensuring the - * internal views receive the final size. The same approach is used in the v7 - * implementation (see VideoView.kt) and in React Native core (Toolbar example [link]). - */ - private val layoutRunnable = Runnable { - measure( - MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY) - ) - layout(left, top, right, bottom) - } - - override fun requestLayout() { - super.requestLayout() - // Post a second layout pass so the ExoPlayer internal views get correct bounds. - post(layoutRunnable) - } - - override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { - super.onLayout(changed, left, top, right, bottom) - - if (changed) { + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + val width = MeasureSpec.getSize(widthMeasureSpec) + val height = MeasureSpec.getSize(heightMeasureSpec) + if (width > 0 && height > 0) { pendingResizeMode?.let { resizeMode -> playerView.resizeMode = resizeMode } diff --git a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 7db96bd3..3bd58165 100644 --- a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -228,8 +228,7 @@ public class ReactExoplayerView extends FrameLayout implements private ArrayList rootViewChildrenOriginalVisibility = new ArrayList(); /* - * When user is seeking first called is on onPositionDiscontinuity -> - * DISCONTINUITY_REASON_SEEK + * When user is seeking first called is on onPositionDiscontinuity -> DISCONTINUITY_REASON_SEEK * Then we set if to false when playback is back in onIsPlayingChanged -> true */ private boolean isSeeking = false; @@ -299,8 +298,7 @@ public class ReactExoplayerView extends FrameLayout implements lastPos = pos; lastBufferDuration = bufferedDuration; lastDuration = duration; - eventEmitter.onVideoProgress.invoke(pos, bufferedDuration, player.getDuration(), - getPositionInFirstPeriodMsForCurrentWindow(pos)); + eventEmitter.onVideoProgress.invoke(pos, bufferedDuration, player.getDuration(), getPositionInFirstPeriodMsForCurrentWindow(pos)); } } } @@ -318,7 +316,7 @@ public class ReactExoplayerView extends FrameLayout implements public double getPositionInFirstPeriodMsForCurrentWindow(long currentPosition) { Timeline.Window window = new Timeline.Window(); - if (!player.getCurrentTimeline().isEmpty()) { + if(!player.getCurrentTimeline().isEmpty()) { player.getCurrentTimeline().getWindow(player.getCurrentMediaItemIndex(), window); } return window.windowStartTimeMs + currentPosition; @@ -357,9 +355,9 @@ public class ReactExoplayerView extends FrameLayout implements LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); exoPlayerView = new ExoPlayerView(getContext()); - exoPlayerView.addOnLayoutChangeListener( - (View v, int l, int t, int r, int b, int ol, int ot, int or, int ob) -> PictureInPictureUtil - .applySourceRectHint(themedReactContext, pictureInPictureParamsBuilder, exoPlayerView)); + exoPlayerView.addOnLayoutChangeListener( (View v, int l, int t, int r, int b, int ol, int ot, int or, int ob) -> + PictureInPictureUtil.applySourceRectHint(themedReactContext, pictureInPictureParamsBuilder, exoPlayerView) + ); exoPlayerView.setLayoutParams(layoutParams); addView(exoPlayerView, 0, layoutParams); @@ -385,11 +383,9 @@ public class ReactExoplayerView extends FrameLayout implements public void onHostPause() { isInBackground = true; Activity activity = themedReactContext.getCurrentActivity(); - boolean isInPictureInPicture = Util.SDK_INT >= Build.VERSION_CODES.N && activity != null - && activity.isInPictureInPictureMode(); - boolean isInMultiWindowMode = Util.SDK_INT >= Build.VERSION_CODES.N && activity != null - && activity.isInMultiWindowMode(); - if (playInBackground || isInPictureInPicture || isInMultiWindowMode) { + boolean isInPictureInPicture = Util.SDK_INT >= Build.VERSION_CODES.N && activity != null && activity.isInPictureInPictureMode(); + boolean isInMultiWindowMode = Util.SDK_INT >= Build.VERSION_CODES.N && activity != null && activity.isInMultiWindowMode(); + if (playInBackground || isInPictureInPicture || isInMultiWindowMode || enterPictureInPictureOnLeave) { return; } setPlayWhenReady(false); @@ -407,7 +403,7 @@ public class ReactExoplayerView extends FrameLayout implements viewHasDropped = true; } - // BandwidthMeter.EventListener implementation + //BandwidthMeter.EventListener implementation @Override public void onBandwidthSample(int elapsedMs, long bytes, long bitrate) { if (mReportBandwidth) { @@ -415,8 +411,7 @@ public class ReactExoplayerView extends FrameLayout implements eventEmitter.onVideoBandwidthUpdate.invoke(bitrate, 0, 0, null); } else { Format videoFormat = player.getVideoFormat(); - boolean isRotatedContent = videoFormat != null - && (videoFormat.rotationDegrees == 90 || videoFormat.rotationDegrees == 270); + boolean isRotatedContent = videoFormat != null && (videoFormat.rotationDegrees == 90 || videoFormat.rotationDegrees == 270); int width = videoFormat != null ? (isRotatedContent ? videoFormat.height : videoFormat.width) : 0; int height = videoFormat != null ? (isRotatedContent ? videoFormat.width : videoFormat.height) : 0; String trackId = videoFormat != null ? videoFormat.id : null; @@ -431,8 +426,7 @@ public class ReactExoplayerView extends FrameLayout implements * Toggling the visibility of the player control view */ private void togglePlayerControlVisibility() { - if (player == null) - return; + if (player == null) return; if (exoPlayerView.isControllerVisible()) { exoPlayerView.hideController(); } else { @@ -456,8 +450,7 @@ public class ReactExoplayerView extends FrameLayout implements } private void updateControllerConfig() { - if (exoPlayerView == null) - return; + if (exoPlayerView == null) return; exoPlayerView.setControllerShowTimeoutMs(5000); @@ -468,8 +461,7 @@ public class ReactExoplayerView extends FrameLayout implements } private void updateControllerVisibility() { - if (exoPlayerView == null) - return; + if (exoPlayerView == null) return; exoPlayerView.setUseController(controls && !controlsConfig.getHideFullscreen()); } @@ -477,7 +469,7 @@ public class ReactExoplayerView extends FrameLayout implements private void openSettings() { AlertDialog.Builder builder = new AlertDialog.Builder(themedReactContext); builder.setTitle(R.string.settings); - String[] settingsOptions = { themedReactContext.getString(R.string.playback_speed) }; + String[] settingsOptions = {themedReactContext.getString(R.string.playback_speed)}; builder.setItems(settingsOptions, (dialog, which) -> { if (which == 0) { showPlaybackSpeedOptions(); @@ -487,7 +479,7 @@ public class ReactExoplayerView extends FrameLayout implements } private void showPlaybackSpeedOptions() { - String[] speedOptions = { "0.5x", "1.0x", "1.5x", "2.0x" }; + String[] speedOptions = {"0.5x", "1.0x", "1.5x", "2.0x"}; AlertDialog.Builder builder = new AlertDialog.Builder(themedReactContext); builder.setTitle(R.string.select_playback_speed); @@ -505,10 +497,8 @@ public class ReactExoplayerView extends FrameLayout implements speed = 2.0f; break; default: - speed = 1.0f; - ; - } - ; + speed = 1.0f;; + }; setRateModifier(speed); }); builder.show(); @@ -520,30 +510,24 @@ public class ReactExoplayerView extends FrameLayout implements /** * Update the layout + * @param view view needs to update layout * - * @param view view needs to update layout - * - * This is a workaround for the open bug in react-native: ... + * This is a workaround for the open bug in react-native: ... */ private void reLayout(View view) { - if (view == null) - return; + if (view == null) return; view.measure(MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY)); view.layout(view.getLeft(), view.getTop(), view.getMeasuredWidth(), view.getMeasuredHeight()); } private void refreshControlsStyles() { - if (exoPlayerView == null || player == null || !controls) - return; + if (exoPlayerView == null || player == null || !controls) return; updateControllerVisibility(); } - // Note: The following methods for live content and button visibility are no - // longer needed - // as PlayerView handles controls automatically. Some functionality may need to - // be + // Note: The following methods for live content and button visibility are no longer needed + // as PlayerView handles controls automatically. Some functionality may need to be // reimplemented using PlayerView's APIs if custom behavior is required. private void reLayoutControls() { @@ -580,7 +564,6 @@ public class ReactExoplayerView extends FrameLayout implements private class RNVLoadControl extends DefaultLoadControl { private final int availableHeapInBytes; private final Runtime runtime; - public RNVLoadControl(DefaultAllocator allocator, BufferConfig config) { super(allocator, config.getMinBufferMs() != BufferConfig.Companion.getBufferConfigPropUnsetInt() @@ -591,7 +574,7 @@ public class ReactExoplayerView extends FrameLayout implements : DefaultLoadControl.DEFAULT_MAX_BUFFER_MS, config.getBufferForPlaybackMs() != BufferConfig.Companion.getBufferConfigPropUnsetInt() ? config.getBufferForPlaybackMs() - : DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS, + : DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS , config.getBufferForPlaybackAfterRebufferMs() != BufferConfig.Companion.getBufferConfigPropUnsetInt() ? config.getBufferForPlaybackAfterRebufferMs() : DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, @@ -602,12 +585,10 @@ public class ReactExoplayerView extends FrameLayout implements : DefaultLoadControl.DEFAULT_BACK_BUFFER_DURATION_MS, DefaultLoadControl.DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME); runtime = Runtime.getRuntime(); - ActivityManager activityManager = (ActivityManager) themedReactContext - .getSystemService(ThemedReactContext.ACTIVITY_SERVICE); - double maxHeap = config.getMaxHeapAllocationPercent() != BufferConfig.Companion - .getBufferConfigPropUnsetDouble() - ? config.getMaxHeapAllocationPercent() - : DEFAULT_MAX_HEAP_ALLOCATION_PERCENT; + ActivityManager activityManager = (ActivityManager) themedReactContext.getSystemService(ThemedReactContext.ACTIVITY_SERVICE); + double maxHeap = config.getMaxHeapAllocationPercent() != BufferConfig.Companion.getBufferConfigPropUnsetDouble() + ? config.getMaxHeapAllocationPercent() + : DEFAULT_MAX_HEAP_ALLOCATION_PERCENT; availableHeapInBytes = (int) Math.floor(activityManager.getMemoryClass() * maxHeap * 1024 * 1024); } @@ -625,15 +606,13 @@ public class ReactExoplayerView extends FrameLayout implements } long usedMemory = runtime.totalMemory() - runtime.freeMemory(); long freeMemory = runtime.maxMemory() - usedMemory; - double minBufferMemoryReservePercent = source.getBufferConfig() - .getMinBufferMemoryReservePercent() != BufferConfig.Companion.getBufferConfigPropUnsetDouble() - ? source.getBufferConfig().getMinBufferMemoryReservePercent() - : ReactExoplayerView.DEFAULT_MIN_BUFFER_MEMORY_RESERVE; + double minBufferMemoryReservePercent = source.getBufferConfig().getMinBufferMemoryReservePercent() != BufferConfig.Companion.getBufferConfigPropUnsetDouble() + ? source.getBufferConfig().getMinBufferMemoryReservePercent() + : ReactExoplayerView.DEFAULT_MIN_BUFFER_MEMORY_RESERVE; long reserveMemory = (long) minBufferMemoryReservePercent * runtime.maxMemory(); long bufferedMs = bufferedDurationUs / (long) 1000; if (reserveMemory > freeMemory && bufferedMs > 2000) { - // We don't have enough memory in reserve so we stop buffering to allow other - // components to use it instead + // We don't have enough memory in reserve so we stop buffering to allow other components to use it instead return false; } if (runtime.freeMemory() == 0) { @@ -666,13 +645,13 @@ public class ReactExoplayerView extends FrameLayout implements // Initialize core configuration and listeners initializePlayerCore(self); pipListenerUnsubscribe = PictureInPictureUtil.addLifecycleEventListener(themedReactContext, this); - PictureInPictureUtil.applyAutoEnterEnabled(themedReactContext, pictureInPictureParamsBuilder, - this.enterPictureInPictureOnLeave); + PictureInPictureUtil.applyAutoEnterEnabled(themedReactContext, pictureInPictureParamsBuilder, this.enterPictureInPictureOnLeave); } if (!source.isLocalAssetFile() && !source.isAsset() && source.getBufferConfig().getCacheSize() > 0) { RNVSimpleCache.INSTANCE.setSimpleCache( this.getContext(), - source.getBufferConfig().getCacheSize()); + source.getBufferConfig().getCacheSize() + ); useCache = true; } else { useCache = false; @@ -680,8 +659,7 @@ public class ReactExoplayerView extends FrameLayout implements if (playerNeedsSource) { // Will force display of shutter view if needed exoPlayerView.invalidateAspectRatio(); - // DRM session manager creation must be done on a different thread to prevent - // crashes so we start a new thread + // DRM session manager creation must be done on a different thread to prevent crashes so we start a new thread ExecutorService es = Executors.newSingleThreadExecutor(); es.execute(() -> { // DRM initialization must run on a different thread @@ -690,8 +668,7 @@ public class ReactExoplayerView extends FrameLayout implements } if (activity == null) { DebugLog.e(TAG, "Failed to initialize Player!, null activity"); - eventEmitter.onVideoError.invoke("Failed to initialize Player!", - new Exception("Current Activity is null!"), "1001"); + eventEmitter.onVideoError.invoke("Failed to initialize Player!", new Exception("Current Activity is null!"), "1001"); return; } @@ -744,7 +721,8 @@ public class ReactExoplayerView extends FrameLayout implements DefaultAllocator allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); RNVLoadControl loadControl = new RNVLoadControl( allocator, - source.getBufferConfig()); + source.getBufferConfig() + ); long initialBitrate = source.getBufferConfig().getInitialBitrate(); if (initialBitrate > 0) { @@ -752,10 +730,11 @@ public class ReactExoplayerView extends FrameLayout implements this.bandwidthMeter = config.getBandwidthMeter(); } - DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(getContext()) - .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER) - .setEnableDecoderFallback(true) - .forceEnableMediaCodecAsynchronousQueueing(); + DefaultRenderersFactory renderersFactory = + new DefaultRenderersFactory(getContext()) + .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF) + .setEnableDecoderFallback(true) + .forceEnableMediaCodecAsynchronousQueueing(); DefaultMediaSourceFactory mediaSourceFactory; @@ -764,13 +743,11 @@ public class ReactExoplayerView extends FrameLayout implements } else { mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory); - mediaSourceFactory.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, - exoPlayerView.getPlayerView()); + mediaSourceFactory.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView.getPlayerView()); } if (useCache && !disableCache) { - mediaSourceFactory - .setDataSourceFactory(RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true))); + mediaSourceFactory.setDataSourceFactory(RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true))); } player = new ExoPlayer.Builder(getContext(), renderersFactory) @@ -795,7 +772,7 @@ public class ReactExoplayerView extends FrameLayout implements player.setPlaybackParameters(params); changeAudioOutput(this.audioOutput); - if (showNotificationControls) { + if(showNotificationControls) { setupPlaybackService(); } } @@ -807,7 +784,8 @@ public class ReactExoplayerView extends FrameLayout implements Uri adTagUrl = adProps.getAdTagUrl(); if (adTagUrl != null) { // Create an AdsLoader. - ImaAdsLoader.Builder imaLoaderBuilder = new ImaAdsLoader.Builder(themedReactContext) + ImaAdsLoader.Builder imaLoaderBuilder = new ImaAdsLoader + .Builder(themedReactContext) .setAdEventListener(this) .setAdErrorListener(this); @@ -839,8 +817,7 @@ public class ReactExoplayerView extends FrameLayout implements } try { - // First check if there's a custom DRM manager registered through the plugin - // system + // First check if there's a custom DRM manager registered through the plugin system DRMManagerSpec drmManager = ReactNativeVideoManager.Companion.getInstance().getDRMManager(); if (drmManager == null) { // If no custom manager is registered, use the default implementation @@ -849,13 +826,11 @@ public class ReactExoplayerView extends FrameLayout implements DrmSessionManager drmSessionManager = drmManager.buildDrmSessionManager(uuid, drmProps); if (drmSessionManager == null) { - eventEmitter.onVideoError.invoke("Failed to build DRM session manager", - new Exception("DRM session manager is null"), "3007"); + eventEmitter.onVideoError.invoke("Failed to build DRM session manager", new Exception("DRM session manager is null"), "3007"); } // Allow plugins to override the DrmSessionManager - DrmSessionManager overriddenManager = ReactNativeVideoManager.Companion.getInstance() - .overrideDrmSessionManager(source, drmSessionManager); + DrmSessionManager overriddenManager = ReactNativeVideoManager.Companion.getInstance().overrideDrmSessionManager(source, drmSessionManager); return overriddenManager != null ? overriddenManager : drmSessionManager; } catch (UnsupportedDrmException ex) { // Unsupported DRM exceptions are handled by the calling method @@ -878,8 +853,7 @@ public class ReactExoplayerView extends FrameLayout implements } /// init DRM DrmSessionManager drmSessionManager = initializePlayerDrm(); - if (drmSessionManager == null && runningSource.getDrmProps() != null - && runningSource.getDrmProps().getDrmType() != null) { + if (drmSessionManager == null && runningSource.getDrmProps() != null && runningSource.getDrmProps().getDrmType() != null) { // Failed to initialize DRM session manager - cannot continue DebugLog.e(TAG, "Failed to initialize DRM Session Manager Framework!"); return; @@ -936,8 +910,7 @@ public class ReactExoplayerView extends FrameLayout implements } catch (UnsupportedDrmException e) { int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME - ? R.string.error_drm_unsupported_scheme - : R.string.error_drm_unknown); + ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown); eventEmitter.onVideoError.invoke(getResources().getString(errorStringId), e, "3003"); } } @@ -982,8 +955,7 @@ public class ReactExoplayerView extends FrameLayout implements if (playbackServiceBinder != null) { playbackServiceBinder.getService().unregisterPlayer(player); } - } catch (Exception ignored) { - } + } catch (Exception ignored) {} playbackServiceBinder = null; } @@ -1015,22 +987,21 @@ public class ReactExoplayerView extends FrameLayout implements private void cleanupPlaybackService() { try { - if (player != null && playbackServiceBinder != null) { + if(player != null && playbackServiceBinder != null) { playbackServiceBinder.getService().unregisterPlayer(player); } playbackServiceBinder = null; - if (playbackServiceConnection != null) { + if(playbackServiceConnection != null) { themedReactContext.unbindService(playbackServiceConnection); } - } catch (Exception e) { + } catch(Exception e) { DebugLog.w(TAG, "Cloud not cleanup playback service"); } } - private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessionManager drmSessionManager, - long cropStartMs, long cropEndMs) { + private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessionManager drmSessionManager, long cropStartMs, long cropEndMs) { if (uri == null) { throw new IllegalStateException("Invalid video uri"); } @@ -1062,12 +1033,12 @@ public class ReactExoplayerView extends FrameLayout implements Uri adTagUrl = source.getAdsProps().getAdTagUrl(); if (adTagUrl != null) { mediaItemBuilder.setAdsConfiguration( - new MediaItem.AdsConfiguration.Builder(adTagUrl).build()); + new MediaItem.AdsConfiguration.Builder(adTagUrl).build() + ); } } - MediaItem.LiveConfiguration.Builder liveConfiguration = ConfigurationUtils - .getLiveConfiguration(source.getBufferConfig()); + MediaItem.LiveConfiguration.Builder liveConfiguration = ConfigurationUtils.getLiveConfiguration(source.getBufferConfig()); mediaItemBuilder.setLiveConfiguration(liveConfiguration.build()); MediaSource.Factory mediaSourceFactory; @@ -1079,26 +1050,29 @@ public class ReactExoplayerView extends FrameLayout implements drmProvider = new DefaultDrmSessionManagerProvider(); } + switch (type) { case CONTENT_TYPE_SS: - if (!BuildConfig.USE_EXOPLAYER_SMOOTH_STREAMING) { + if(!BuildConfig.USE_EXOPLAYER_SMOOTH_STREAMING) { DebugLog.e("Exo Player Exception", "Smooth Streaming is not enabled!"); throw new IllegalStateException("Smooth Streaming is not enabled!"); } mediaSourceFactory = new SsMediaSource.Factory( new DefaultSsChunkSource.Factory(mediaDataSourceFactory), - buildDataSourceFactory(false)); + buildDataSourceFactory(false) + ); break; case CONTENT_TYPE_DASH: - if (!BuildConfig.USE_EXOPLAYER_DASH) { + if(!BuildConfig.USE_EXOPLAYER_DASH) { DebugLog.e("Exo Player Exception", "DASH is not enabled!"); throw new IllegalStateException("DASH is not enabled!"); } mediaSourceFactory = new DashMediaSource.Factory( new DefaultDashChunkSource.Factory(mediaDataSourceFactory), - buildDataSourceFactory(false)); + buildDataSourceFactory(false) + ); break; case CONTENT_TYPE_HLS: if (!BuildConfig.USE_EXOPLAYER_HLS) { @@ -1113,14 +1087,13 @@ public class ReactExoplayerView extends FrameLayout implements } mediaSourceFactory = new HlsMediaSource.Factory( - dataSourceFactory) - .setAllowChunklessPreparation(source.getTextTracksAllowChunklessPreparation()); + dataSourceFactory + ).setAllowChunklessPreparation(source.getTextTracksAllowChunklessPreparation()); break; case CONTENT_TYPE_OTHER: if ("asset".equals(uri.getScheme())) { try { - DataSource.Factory assetDataSourceFactory = DataSourceUtil - .buildAssetDataSourceFactory(themedReactContext, uri); + DataSource.Factory assetDataSourceFactory = DataSourceUtil.buildAssetDataSourceFactory(themedReactContext, uri); mediaSourceFactory = new ProgressiveMediaSource.Factory(assetDataSourceFactory); } catch (Exception e) { throw new IllegalStateException("cannot open input file:" + uri); @@ -1128,10 +1101,12 @@ public class ReactExoplayerView extends FrameLayout implements } else if ("file".equals(uri.getScheme()) || !useCache) { mediaSourceFactory = new ProgressiveMediaSource.Factory( - mediaDataSourceFactory); + mediaDataSourceFactory + ); } else { mediaSourceFactory = new ProgressiveMediaSource.Factory( - RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true))); + RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true)) + ); } break; @@ -1150,19 +1125,20 @@ public class ReactExoplayerView extends FrameLayout implements if (cmcdConfigurationFactory != null) { mediaSourceFactory = mediaSourceFactory.setCmcdConfigurationFactory( - cmcdConfigurationFactory::createCmcdConfiguration); + cmcdConfigurationFactory::createCmcdConfiguration + ); } mediaSourceFactory = Objects.requireNonNullElse( ReactNativeVideoManager.Companion.getInstance() .overrideMediaSourceFactory(source, mediaSourceFactory, mediaDataSourceFactory), - mediaSourceFactory); + mediaSourceFactory + ); mediaItemBuilder.setStreamKeys(streamKeys); @Nullable - final MediaItem.Builder overridenMediaItemBuilder = ReactNativeVideoManager.Companion.getInstance() - .overrideMediaItemBuilder(source, mediaItemBuilder); + final MediaItem.Builder overridenMediaItemBuilder = ReactNativeVideoManager.Companion.getInstance().overrideMediaItemBuilder(source, mediaItemBuilder); MediaItem mediaItem = overridenMediaItemBuilder != null ? overridenMediaItemBuilder.build() @@ -1171,7 +1147,8 @@ public class ReactExoplayerView extends FrameLayout implements MediaSource mediaSource = mediaSourceFactory .setDrmSessionManagerProvider(drmProvider) .setLoadErrorHandlingPolicy( - config.buildLoadErrorHandlingPolicy(source.getMinLoadRetryCount())) + config.buildLoadErrorHandlingPolicy(source.getMinLoadRetryCount()) + ) .createMediaSource(mediaItem); if (cropStartMs >= 0 && cropEndMs >= 0) { @@ -1206,8 +1183,7 @@ public class ReactExoplayerView extends FrameLayout implements } } - MediaItem.SubtitleConfiguration.Builder configBuilder = new MediaItem.SubtitleConfiguration.Builder( - track.getUri()) + MediaItem.SubtitleConfiguration.Builder configBuilder = new MediaItem.SubtitleConfiguration.Builder(track.getUri()) .setId(trackId) .setMimeType(track.getType()) .setLabel(label) @@ -1218,8 +1194,7 @@ public class ReactExoplayerView extends FrameLayout implements configBuilder.setLanguage(track.getLanguage()); } - // Set selection flags - make first track default if no specific track is - // selected + // Set selection flags - make first track default if no specific track is selected if (trackIndex == 0 && (textTrackType == null || "disabled".equals(textTrackType))) { configBuilder.setSelectionFlags(C.SELECTION_FLAG_DEFAULT); } else { @@ -1229,12 +1204,10 @@ public class ReactExoplayerView extends FrameLayout implements MediaItem.SubtitleConfiguration subtitleConfiguration = configBuilder.build(); subtitleConfigurations.add(subtitleConfiguration); - DebugLog.d(TAG, - "Created subtitle configuration: " + trackId + " - " + label + " (" + track.getType() + ")"); + DebugLog.d(TAG, "Created subtitle configuration: " + trackId + " - " + label + " (" + track.getType() + ")"); trackIndex++; } catch (Exception e) { - DebugLog.e(TAG, - "Error creating SubtitleConfiguration for URI " + track.getUri() + ": " + e.getMessage()); + DebugLog.e(TAG, "Error creating SubtitleConfiguration for URI " + track.getUri() + ": " + e.getMessage()); } } @@ -1247,7 +1220,7 @@ public class ReactExoplayerView extends FrameLayout implements private void releasePlayer() { if (player != null) { - if (playbackServiceBinder != null) { + if(playbackServiceBinder != null) { playbackServiceBinder.getService().unregisterPlayer(player); themedReactContext.unbindService(playbackServiceConnection); } @@ -1303,8 +1276,7 @@ public class ReactExoplayerView extends FrameLayout implements case AudioManager.AUDIOFOCUS_LOSS: view.hasAudioFocus = false; view.eventEmitter.onAudioFocusChanged.invoke(false); - // FIXME this pause can cause issue if content doesn't have pause capability - // (can happen on live channel) + // FIXME this pause can cause issue if content doesn't have pause capability (can happen on live channel) if (activity != null) { activity.runOnUiThread(view::pausePlayback); } @@ -1325,12 +1297,16 @@ public class ReactExoplayerView extends FrameLayout implements if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { // Lower the volume if (!view.muted) { - activity.runOnUiThread(() -> view.player.setVolume(view.audioVolume * 0.8f)); + activity.runOnUiThread(() -> + view.player.setVolume(view.audioVolume * 0.8f) + ); } } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { // Raise it back to normal if (!view.muted) { - activity.runOnUiThread(() -> view.player.setVolume(view.audioVolume * 1)); + activity.runOnUiThread(() -> + view.player.setVolume(view.audioVolume * 1) + ); } } } @@ -1403,8 +1379,7 @@ public class ReactExoplayerView extends FrameLayout implements /** * Returns a new DataSource factory. * - * @param useBandwidthMeter Whether to set {@link #bandwidthMeter} as a listener - * to the new + * @param useBandwidthMeter Whether to set {@link #bandwidthMeter} as a listener to the new * DataSource factory. * @return A new DataSource factory. */ @@ -1416,14 +1391,12 @@ public class ReactExoplayerView extends FrameLayout implements /** * Returns a new HttpDataSource factory. * - * @param useBandwidthMeter Whether to set {@link #bandwidthMeter} as a listener - * to the new - * DataSource factory. + * @param useBandwidthMeter Whether to set {@link #bandwidthMeter} as a listener to the new + * DataSource factory. * @return A new HttpDataSource factory. */ private HttpDataSource.Factory buildHttpDataSourceFactory(boolean useBandwidthMeter) { - return DataSourceUtil.getDefaultHttpDataSourceFactory(this.themedReactContext, - useBandwidthMeter ? bandwidthMeter : null, source.getHeaders()); + return DataSourceUtil.getDefaultHttpDataSourceFactory(this.themedReactContext, useBandwidthMeter ? bandwidthMeter : null, source.getHeaders()); } // AudioBecomingNoisyListener implementation @@ -1440,13 +1413,11 @@ public class ReactExoplayerView extends FrameLayout implements @Override public void onEvents(@NonNull Player player, Player.Events events) { - if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED) - || events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) { + if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED) || events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) { int playbackState = player.getPlaybackState(); boolean playWhenReady = player.getPlayWhenReady(); String text = "onStateChanged: playWhenReady=" + playWhenReady + ", playbackState="; - eventEmitter.onPlaybackRateChange - .invoke(playWhenReady && playbackState == ExoPlayer.STATE_READY ? 1.0f : 0.0f); + eventEmitter.onPlaybackRateChange.invoke(playWhenReady && playbackState == ExoPlayer.STATE_READY ? 1.0f : 0.0f); switch (playbackState) { case Player.STATE_IDLE: text += "idle"; @@ -1503,11 +1474,9 @@ public class ReactExoplayerView extends FrameLayout implements } /** - * The progress message handler will duplicate recursions of the - * onProgressMessage handler - * on change of player state from any state to STATE_READY with playWhenReady is - * true (when - * the video is not paused). This clears all existing messages. + * The progress message handler will duplicate recursions of the onProgressMessage handler + * on change of player state from any state to STATE_READY with playWhenReady is true (when + * the video is not paused). This clears all existing messages. */ private void clearProgressMessageHandler() { progressHandler.removeMessages(SHOW_PROGRESS); @@ -1526,8 +1495,7 @@ public class ReactExoplayerView extends FrameLayout implements setSelectedTextTrack(textTrackType, textTrackValue); } Format videoFormat = player.getVideoFormat(); - boolean isRotatedContent = videoFormat != null - && (videoFormat.rotationDegrees == 90 || videoFormat.rotationDegrees == 270); + boolean isRotatedContent = videoFormat != null && (videoFormat.rotationDegrees == 90 || videoFormat.rotationDegrees == 270); int width = videoFormat != null ? (isRotatedContent ? videoFormat.height : videoFormat.width) : 0; int height = videoFormat != null ? (isRotatedContent ? videoFormat.width : videoFormat.height) : 0; String trackId = videoFormat != null ? videoFormat.id : null; @@ -1536,19 +1504,18 @@ public class ReactExoplayerView extends FrameLayout implements long duration = player.getDuration(); long currentPosition = player.getCurrentPosition(); ArrayList audioTracks = getAudioTrackInfo(); - ArrayList textTracks = getTextTrackInfo(); + ArrayList textTracks = getTextTrackInfo(); if (source.getContentStartTime() != -1) { ExecutorService es = Executors.newSingleThreadExecutor(); es.execute(() -> { - // To prevent ANRs caused by getVideoTrackInfo we run this on a different thread - // and notify the player only when we're done + // To prevent ANRs caused by getVideoTrackInfo we run this on a different thread and notify the player only when we're done ArrayList videoTracks = getVideoTrackInfoFromManifest(); if (videoTracks != null) { isUsingContentResolution = true; } eventEmitter.onVideoLoad.invoke(duration, currentPosition, width, height, - audioTracks, textTracks, videoTracks, trackId); + audioTracks, textTracks, videoTracks, trackId ); updateSubtitleButtonVisibility(); }); @@ -1566,9 +1533,9 @@ public class ReactExoplayerView extends FrameLayout implements } private static boolean isTrackSelected(TrackSelection selection, TrackGroup group, - int trackIndex) { + int trackIndex){ return selection != null && selection.getTrackGroup() == group - && selection.indexOf(trackIndex) != C.INDEX_UNSET; + && selection.indexOf( trackIndex ) != C.INDEX_UNSET; } private ArrayList getAudioTrackInfo() { @@ -1586,6 +1553,7 @@ public class ReactExoplayerView extends FrameLayout implements TrackSelectionArray selectionArray = player.getCurrentTrackSelections(); TrackSelection selection = selectionArray.get(C.TRACK_TYPE_AUDIO); + for (int groupIndex = 0; groupIndex < groups.length; ++groupIndex) { TrackGroup group = groups.get(groupIndex); Format format = group.getFormat(0); @@ -1599,29 +1567,6 @@ public class ReactExoplayerView extends FrameLayout implements Track audioTrack = exoplayerTrackToGenericTrack(format, groupIndex, selection, group); audioTrack.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate); audioTrack.setSelected(isSelected); - // Encode channel count, bitrate and mimeType into title so JS can read them reliably - // e.g. "English|ch:6|br:640000|mt:audio/ac3" - String existing = audioTrack.getTitle() != null ? audioTrack.getTitle() : ""; - if (format.channelCount != Format.NO_VALUE && format.channelCount > 0) { - existing = existing + "|ch:" + format.channelCount; - } - // Use bitrate, fall back to averageBitrate then peakBitrate - int effectiveBitrate = format.bitrate; - if (effectiveBitrate == Format.NO_VALUE || effectiveBitrate <= 0) { - effectiveBitrate = format.averageBitrate; - } - if (effectiveBitrate == Format.NO_VALUE || effectiveBitrate <= 0) { - effectiveBitrate = format.peakBitrate; - } - if (effectiveBitrate != Format.NO_VALUE && effectiveBitrate > 0) { - existing = existing + "|br:" + effectiveBitrate; - } - if (format.sampleMimeType != null && !format.sampleMimeType.isEmpty()) { - existing = existing + "|mt:" + format.sampleMimeType; - } - if (!existing.isEmpty()) { - audioTrack.setTitle(existing); - } audioTracks.add(audioTrack); } @@ -1634,8 +1579,7 @@ public class ReactExoplayerView extends FrameLayout implements videoTrack.setHeight(format.height == Format.NO_VALUE ? 0 : format.height); videoTrack.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate); videoTrack.setRotation(format.rotationDegrees); - if (format.codecs != null) - videoTrack.setCodecs(format.codecs); + if (format.codecs != null) videoTrack.setCodecs(format.codecs); videoTrack.setTrackId(format.id == null ? String.valueOf(trackIndex) : format.id); videoTrack.setIndex(trackIndex); return videoTrack; @@ -1672,8 +1616,7 @@ public class ReactExoplayerView extends FrameLayout implements return this.getVideoTrackInfoFromManifest(0); } - // We need retry count to in case where minefest request fails from poor network - // conditions + // We need retry count to in case where minefest request fails from poor network conditions @WorkerThread private ArrayList getVideoTrackInfoFromManifest(int retryCount) { ExecutorService es = Executors.newSingleThreadExecutor(); @@ -1688,20 +1631,18 @@ public class ReactExoplayerView extends FrameLayout implements public ArrayList call() { ArrayList videoTracks = new ArrayList<>(); - try { + try { DashManifest manifest = DashUtil.loadManifest(this.ds, this.uri); int periodCount = manifest.getPeriodCount(); for (int i = 0; i < periodCount; i++) { Period period = manifest.getPeriod(i); - for (int adaptationIndex = 0; adaptationIndex < period.adaptationSets - .size(); adaptationIndex++) { + for (int adaptationIndex = 0; adaptationIndex < period.adaptationSets.size(); adaptationIndex++) { AdaptationSet adaptation = period.adaptationSets.get(adaptationIndex); if (adaptation.type != C.TRACK_TYPE_VIDEO) { continue; } boolean hasFoundContentPeriod = false; - for (int representationIndex = 0; representationIndex < adaptation.representations - .size(); representationIndex++) { + for (int representationIndex = 0; representationIndex < adaptation.representations.size(); representationIndex++) { Representation representation = adaptation.representations.get(representationIndex); Format format = representation.format; if (isFormatSupported(format)) { @@ -1709,8 +1650,7 @@ public class ReactExoplayerView extends FrameLayout implements break; } hasFoundContentPeriod = true; - VideoTrack videoTrack = exoplayerVideoTrackToGenericVideoTrack(format, - representationIndex); + VideoTrack videoTrack = exoplayerVideoTrackToGenericVideoTrack(format, representationIndex); videoTracks.add(videoTrack); } } @@ -1740,16 +1680,12 @@ public class ReactExoplayerView extends FrameLayout implements return null; } - private Track exoplayerTrackToGenericTrack(Format format, int trackIndex, TrackSelection selection, - TrackGroup group) { + private Track exoplayerTrackToGenericTrack(Format format, int trackIndex, TrackSelection selection, TrackGroup group) { Track track = new Track(); track.setIndex(trackIndex); - if (format.sampleMimeType != null) - track.setMimeType(format.sampleMimeType); - if (format.language != null) - track.setLanguage(format.language); - if (format.label != null) - track.setTitle(format.label); + if (format.sampleMimeType != null) track.setMimeType(format.sampleMimeType); + if (format.language != null) track.setLanguage(format.language); + if (format.label != null) track.setTitle(format.label); track.setSelected(isTrackSelected(selection, group, trackIndex)); return track; } @@ -1817,28 +1753,9 @@ public class ReactExoplayerView extends FrameLayout implements Track track = new Track(); track.setIndex(groupIndex); track.setLanguage(format.language != null ? format.language : "unknown"); - String baseTitle = format.label != null ? format.label : ""; - if (format.channelCount != Format.NO_VALUE && format.channelCount > 0) { - baseTitle = baseTitle + "|ch:" + format.channelCount; - } - // Use bitrate, fall back to averageBitrate then peakBitrate - int effectiveBitrate = format.bitrate; - if (effectiveBitrate == Format.NO_VALUE || effectiveBitrate <= 0) { - effectiveBitrate = format.averageBitrate; - } - if (effectiveBitrate == Format.NO_VALUE || effectiveBitrate <= 0) { - effectiveBitrate = format.peakBitrate; - } - if (effectiveBitrate != Format.NO_VALUE && effectiveBitrate > 0) { - baseTitle = baseTitle + "|br:" + effectiveBitrate; - } - if (format.sampleMimeType != null && !format.sampleMimeType.isEmpty()) { - baseTitle = baseTitle + "|mt:" + format.sampleMimeType; - } - track.setTitle(baseTitle); + track.setTitle(format.label != null ? format.label : "Track " + (groupIndex + 1)); track.setSelected(false); // Don't report selection status - let PlayerView handle it - if (format.sampleMimeType != null) - track.setMimeType(format.sampleMimeType); + if (format.sampleMimeType != null) track.setMimeType(format.sampleMimeType); track.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate); tracks.add(track); @@ -1869,10 +1786,8 @@ public class ReactExoplayerView extends FrameLayout implements Track textTrack = new Track(); textTrack.setIndex(textTracks.size()); - if (format.sampleMimeType != null) - textTrack.setMimeType(format.sampleMimeType); - if (format.language != null) - textTrack.setLanguage(format.language); + if (format.sampleMimeType != null) textTrack.setMimeType(format.sampleMimeType); + if (format.language != null) textTrack.setLanguage(format.language); boolean isExternal = format.id != null && format.id.startsWith("external-subtitle-"); @@ -1906,34 +1821,28 @@ public class ReactExoplayerView extends FrameLayout implements } @Override - public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, - @NonNull Player.PositionInfo newPosition, @Player.DiscontinuityReason int reason) { + public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, @Player.DiscontinuityReason int reason) { if (reason == Player.DISCONTINUITY_REASON_SEEK) { isSeeking = true; seekPosition = newPosition.positionMs; if (isUsingContentResolution) { - // We need to update the selected track to make sure that it still matches user - // selection if track list has changed in this period + // We need to update the selected track to make sure that it still matches user selection if track list has changed in this period setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue); } } if (playerNeedsSource) { - // This will only occur if the user has performed a seek whilst in the error - // state. Update the - // resume position so that if the user then retries, playback will resume from - // the position to + // This will only occur if the user has performed a seek whilst in the error state. Update the + // resume position so that if the user then retries, playback will resume from the position to // which they seeked. updateResumePosition(); } if (isUsingContentResolution) { - // Discontinuity events might have a different track list so we update the - // selected track + // Discontinuity events might have a different track list so we update the selected track setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue); selectTrackWhenReady = true; } - // When repeat is turned on, reaching the end of the video will not cause a - // state change + // When repeat is turned on, reaching the end of the video will not cause a state change // so we need to explicitly detect it. if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION && player.getRepeatMode() == Player.REPEAT_MODE_ONE) { @@ -1981,17 +1890,15 @@ public class ReactExoplayerView extends FrameLayout implements updateSubtitleButtonVisibility(); } + private boolean hasBuiltInTextTracks() { - if (player == null || trackSelector == null) - return false; + if (player == null || trackSelector == null) return false; MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo(); - if (info == null) - return false; + if (info == null) return false; int textRendererIndex = getTrackRendererIndex(C.TRACK_TYPE_TEXT); - if (textRendererIndex == C.INDEX_UNSET) - return false; + if (textRendererIndex == C.INDEX_UNSET) return false; TrackGroupArray groups = info.getTrackGroups(textRendererIndex); @@ -2011,12 +1918,11 @@ public class ReactExoplayerView extends FrameLayout implements } private void updateSubtitleButtonVisibility() { - if (exoPlayerView == null) - return; + if (exoPlayerView == null) return; boolean hasTextTracks = (source.getSideLoadedTextTracks() != null && - !source.getSideLoadedTextTracks().getTracks().isEmpty()) || - hasBuiltInTextTracks(); + !source.getSideLoadedTextTracks().getTracks().isEmpty()) || + hasBuiltInTextTracks(); exoPlayerView.setShowSubtitleButton(hasTextTracks); } @@ -2036,8 +1942,7 @@ public class ReactExoplayerView extends FrameLayout implements if (isPlaying && isSeeking) { eventEmitter.onVideoSeek.invoke(player.getCurrentPosition(), seekPosition); } - PictureInPictureUtil.applyPlayingStatus(themedReactContext, pictureInPictureParamsBuilder, - pictureInPictureReceiver, !isPlaying); + PictureInPictureUtil.applyPlayingStatus(themedReactContext, pictureInPictureParamsBuilder, pictureInPictureReceiver, !isPlaying); eventEmitter.onVideoPlaybackStateChanged.invoke(isPlaying, isSeeking); if (isPlaying) { @@ -2049,15 +1954,14 @@ public class ReactExoplayerView extends FrameLayout implements public void onPlayerError(@NonNull PlaybackException e) { String errorString = "ExoPlaybackException: " + PlaybackException.getErrorCodeName(e.errorCode); String errorCode = "2" + e.errorCode; - switch (e.errorCode) { + switch(e.errorCode) { case PlaybackException.ERROR_CODE_DRM_DEVICE_REVOKED: case PlaybackException.ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED: case PlaybackException.ERROR_CODE_DRM_PROVISIONING_FAILED: case PlaybackException.ERROR_CODE_DRM_SYSTEM_ERROR: case PlaybackException.ERROR_CODE_DRM_UNSPECIFIED: if (!hasDrmFailed) { - // When DRM fails to reach the app level certificate server it will fail with a - // source error so we assume that it is DRM related and try one more time + // When DRM fails to reach the app level certificate server it will fail with a source error so we assume that it is DRM related and try one more time hasDrmFailed = true; playerNeedsSource = true; updateResumePosition(); @@ -2139,16 +2043,14 @@ public class ReactExoplayerView extends FrameLayout implements boolean isSourceEqual = source.isEquals(this.source); hasDrmFailed = false; this.source = source; - final DataSource.Factory tmpMediaDataSourceFactory = DataSourceUtil.getDefaultDataSourceFactory( - this.themedReactContext, bandwidthMeter, - source.getHeaders()); + final DataSource.Factory tmpMediaDataSourceFactory = + DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, bandwidthMeter, + source.getHeaders()); @Nullable - final DataSource.Factory overriddenMediaDataSourceFactory = ReactNativeVideoManager.Companion.getInstance() - .overrideMediaDataSourceFactory(source, tmpMediaDataSourceFactory); + final DataSource.Factory overriddenMediaDataSourceFactory = ReactNativeVideoManager.Companion.getInstance().overrideMediaDataSourceFactory(source, tmpMediaDataSourceFactory); - this.mediaDataSourceFactory = Objects.requireNonNullElse(overriddenMediaDataSourceFactory, - tmpMediaDataSourceFactory); + this.mediaDataSourceFactory = Objects.requireNonNullElse(overriddenMediaDataSourceFactory, tmpMediaDataSourceFactory); if (source.getCmcdProps() != null) { CMCDConfig cmcdConfig = new CMCDConfig(source.getCmcdProps()); @@ -2167,7 +2069,6 @@ public class ReactExoplayerView extends FrameLayout implements clearSrc(); } } - public void clearSrc() { if (source.getUri() != null) { if (player != null) { @@ -2216,8 +2117,7 @@ public class ReactExoplayerView extends FrameLayout implements } public void disableTrack(int rendererIndex) { - if (trackSelector == null) - return; + if (trackSelector == null) return; DefaultTrackSelector.Parameters disableParameters = trackSelector.getParameters() .buildUpon() @@ -2227,8 +2127,7 @@ public class ReactExoplayerView extends FrameLayout implements } private void selectTextTrackInternal(String type, String value) { - if (player == null || trackSelector == null) - return; + if (player == null || trackSelector == null) return; DebugLog.d(TAG, "selectTextTrackInternal: type=" + type + ", value=" + value); @@ -2248,11 +2147,6 @@ public class ReactExoplayerView extends FrameLayout implements TrackGroupArray groups = info.getTrackGroups(textRendererIndex); boolean trackFound = false; - // react-native-video uses a flattened `textTracks` list on the JS side. - // For HLS/DASH, each TrackGroup often contains a single track at index 0, - // so comparing against `trackIndex` alone makes only the first subtitle selectable. - int flattenedIndex = 0; - for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup group = groups.get(groupIndex); for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { @@ -2265,28 +2159,25 @@ public class ReactExoplayerView extends FrameLayout implements isMatch = true; } else if ("index".equals(type)) { int targetIndex = ReactBridgeUtils.safeParseInt(value, -1); - if (targetIndex == flattenedIndex) { + if (targetIndex == trackIndex) { isMatch = true; } } - flattenedIndex++; - if (isMatch) { TrackSelectionOverride override = new TrackSelectionOverride(group, - java.util.Arrays.asList(trackIndex)); + java.util.Arrays.asList(trackIndex)); parametersBuilder.addOverride(override); trackFound = true; break; } } - if (trackFound) - break; + if (trackFound) break; } if (!trackFound) { DebugLog.w(TAG, "Text track not found for type=" + type + ", value=" + value + - ". Keeping current selection."); + ". Keeping current selection."); } } } @@ -2307,8 +2198,7 @@ public class ReactExoplayerView extends FrameLayout implements } public void setSelectedTrack(int trackType, String type, String value) { - if (player == null || trackSelector == null) - return; + if (player == null || trackSelector == null) return; if (controls) { return; @@ -2382,11 +2272,9 @@ public class ReactExoplayerView extends FrameLayout implements usingExactMatch = true; break; } else if (isUsingContentResolution) { - // When using content resolution rather than ads, we need to try and find the - // closest match if there is no exact match + // When using content resolution rather than ads, we need to try and find the closest match if there is no exact match if (closestFormat != null) { - if ((format.bitrate > closestFormat.bitrate || format.height > closestFormat.height) - && format.height < height) { + if ((format.bitrate > closestFormat.bitrate || format.height > closestFormat.height) && format.height < height) { // Higher quality match closestFormat = format; closestTrackIndex = j; @@ -2397,8 +2285,7 @@ public class ReactExoplayerView extends FrameLayout implements } } } - // This is a fallback if the new period contains only higher resolutions than - // the user has selected + // This is a fallback if the new period contains only higher resolutions than the user has selected if (closestFormat == null && isUsingContentResolution && !usingExactMatch) { // No close match found - so we pick the lowest quality int minHeight = Integer.MAX_VALUE; @@ -2421,8 +2308,8 @@ public class ReactExoplayerView extends FrameLayout implements } } else if (trackType == C.TRACK_TYPE_TEXT && Util.SDK_INT > 18) { // Text default // Use system settings if possible - CaptioningManager captioningManager = (CaptioningManager) themedReactContext - .getSystemService(Context.CAPTIONING_SERVICE); + CaptioningManager captioningManager + = (CaptioningManager)themedReactContext.getSystemService(Context.CAPTIONING_SERVICE); if (captioningManager != null && captioningManager.isEnabled()) { groupIndex = getGroupIndexForDefaultLocale(groups); } @@ -2451,7 +2338,7 @@ public class ReactExoplayerView extends FrameLayout implements // With only one tracks we can't remove any tracks so attempt to play it anyway tracks = allTracks; } else { - tracks = new ArrayList<>(supportedFormatLength + 1); + tracks = new ArrayList<>(supportedFormatLength + 1); for (int k = 0; k < allTracks.size(); k++) { Format format = group.getFormat(k); if (isFormatSupported(format)) { @@ -2477,8 +2364,7 @@ public class ReactExoplayerView extends FrameLayout implements .setRendererDisabled(rendererIndex, false); // Clear existing overrides for this track type to avoid conflicts - // But be careful with audio tracks - don't clear unless explicitly selecting a - // different track + // But be careful with audio tracks - don't clear unless explicitly selecting a different track if (trackType != C.TRACK_TYPE_AUDIO || !type.equals("default")) { selectionParameters.clearOverridesOfType(selectionOverride.getType()); } @@ -2494,7 +2380,7 @@ public class ReactExoplayerView extends FrameLayout implements selectionParameters.setForceHighestSupportedBitrate(false); selectionParameters.setForceLowestBitrate(false); DebugLog.d(TAG, "Audio track selection: group=" + groupIndex + ", tracks=" + tracks + - ", override=" + selectionOverride); + ", override=" + selectionOverride); } trackSelector.setParameters(selectionParameters.build()); @@ -2525,7 +2411,7 @@ public class ReactExoplayerView extends FrameLayout implements } private int getGroupIndexForDefaultLocale(TrackGroupArray groups) { - if (groups.length == 0) { + if (groups.length == 0){ return C.INDEX_UNSET; } @@ -2546,8 +2432,7 @@ public class ReactExoplayerView extends FrameLayout implements public void setSelectedVideoTrack(String type, String value) { videoTrackType = type; videoTrackValue = value; - if (!loadVideoStarted) - setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue); + if (!loadVideoStarted) setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue); } public void setSelectedAudioTrack(String type, String value) { @@ -2578,11 +2463,9 @@ public class ReactExoplayerView extends FrameLayout implements } public void setEnterPictureInPictureOnLeave(boolean enterPictureInPictureOnLeave) { - this.enterPictureInPictureOnLeave = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N - && enterPictureInPictureOnLeave; + this.enterPictureInPictureOnLeave = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && enterPictureInPictureOnLeave; if (player != null) { - PictureInPictureUtil.applyAutoEnterEnabled(themedReactContext, pictureInPictureParamsBuilder, - this.enterPictureInPictureOnLeave); + PictureInPictureUtil.applyAutoEnterEnabled(themedReactContext, pictureInPictureParamsBuilder, this.enterPictureInPictureOnLeave); } } @@ -2590,14 +2473,12 @@ public class ReactExoplayerView extends FrameLayout implements eventEmitter.onPictureInPictureStatusChanged.invoke(isInPictureInPicture); if (fullScreenPlayerView != null && fullScreenPlayerView.isShowing()) { - if (isInPictureInPicture) - fullScreenPlayerView.hideWithoutPlayer(); + if (isInPictureInPicture) fullScreenPlayerView.hideWithoutPlayer(); return; } Activity currentActivity = themedReactContext.getCurrentActivity(); - if (currentActivity == null) - return; + if (currentActivity == null) return; View decorView = currentActivity.getWindow().getDecorView(); ViewGroup rootView = decorView.findViewById(android.R.id.content); @@ -2607,7 +2488,7 @@ public class ReactExoplayerView extends FrameLayout implements LayoutParams.MATCH_PARENT); if (isInPictureInPicture) { - ViewGroup parent = (ViewGroup) exoPlayerView.getParent(); + ViewGroup parent = (ViewGroup)exoPlayerView.getParent(); if (parent != null) { parent.removeView(exoPlayerView); } @@ -2633,12 +2514,10 @@ public class ReactExoplayerView extends FrameLayout implements public void enterPictureInPictureMode() { PictureInPictureParams _pipParams = null; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - ArrayList actions = PictureInPictureUtil.getPictureInPictureActions(themedReactContext, - isPaused, pictureInPictureReceiver); + ArrayList actions = PictureInPictureUtil.getPictureInPictureActions(themedReactContext, isPaused, pictureInPictureReceiver); pictureInPictureParamsBuilder.setActions(actions); if (player.getPlaybackState() == Player.STATE_READY) { - pictureInPictureParamsBuilder - .setAspectRatio(PictureInPictureUtil.calcPictureInPictureAspectRatio(player)); + pictureInPictureParamsBuilder.setAspectRatio(PictureInPictureUtil.calcPictureInPictureAspectRatio(player)); } _pipParams = pictureInPictureParamsBuilder.build(); } @@ -2647,15 +2526,13 @@ public class ReactExoplayerView extends FrameLayout implements public void exitPictureInPictureMode() { Activity currentActivity = themedReactContext.getCurrentActivity(); - if (currentActivity == null) - return; + if (currentActivity == null) return; View decorView = currentActivity.getWindow().getDecorView(); ViewGroup rootView = decorView.findViewById(android.R.id.content); if (!rootViewChildrenOriginalVisibility.isEmpty()) { - if (exoPlayerView.getParent().equals(rootView)) - rootView.removeView(exoPlayerView); + if (exoPlayerView.getParent().equals(rootView)) rootView.removeView(exoPlayerView); for (int i = 0; i < rootView.getChildCount(); i++) { rootView.getChildAt(i).setVisibility(rootViewChildrenOriginalVisibility.get(i)); } @@ -2753,7 +2630,7 @@ public class ReactExoplayerView extends FrameLayout implements if (playbackServiceConnection == null && showNotificationControls) { setupPlaybackService(); - } else if (!showNotificationControls && playbackServiceConnection != null) { + } else if(!showNotificationControls && playbackServiceConnection != null) { cleanupPlaybackService(); } } @@ -2782,13 +2659,12 @@ public class ReactExoplayerView extends FrameLayout implements } if (isFullscreen) { - fullScreenPlayerView = new FullScreenPlayerView(getContext(), exoPlayerView, this, null, - new OnBackPressedCallback(true) { - @Override - public void handleOnBackPressed() { - setFullscreen(false); - } - }, controlsConfig); + fullScreenPlayerView = new FullScreenPlayerView(getContext(), exoPlayerView, this, null, new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + setFullscreen(false); + } + }, controlsConfig); eventEmitter.onVideoFullscreenPlayerWillPresent.invoke(); if (fullScreenPlayerView != null) { fullScreenPlayerView.show(); @@ -2825,8 +2701,7 @@ public class ReactExoplayerView extends FrameLayout implements } @Override - public void onDrmSessionManagerError(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, - @NonNull Exception e) { + public void onDrmSessionManagerError(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, @NonNull Exception e) { DebugLog.d("DRM Info", "onDrmSessionManagerError"); eventEmitter.onVideoError.invoke("onDrmSessionManagerError", e, "3002"); } @@ -2844,7 +2719,7 @@ public class ReactExoplayerView extends FrameLayout implements /** * Handling controls prop * - * @param controls Controls prop, if true enable controls, if false disable them + * @param controls Controls prop, if true enable controls, if false disable them */ public void setControls(boolean controls) { this.controls = controls; @@ -2853,7 +2728,7 @@ public class ReactExoplayerView extends FrameLayout implements // Additional configuration for proper touch handling if (controls) { exoPlayerView.setControllerAutoShow(true); - exoPlayerView.setControllerHideOnTouch(true); // Show controls on touch, don't hide + exoPlayerView.setControllerHideOnTouch(true); // Show controls on touch, don't hide exoPlayerView.setControllerShowTimeoutMs(5000); } } @@ -2886,7 +2761,8 @@ public class ReactExoplayerView extends FrameLayout implements Map errMap = Map.of( "message", error.getMessage(), "code", String.valueOf(error.getErrorCode()), - "type", String.valueOf(error.getErrorType())); + "type", String.valueOf(error.getErrorType()) + ); eventEmitter.onReceiveAdEvent.invoke("ERROR", errMap); handleDaiBackupStream(); @@ -2920,10 +2796,10 @@ public class ReactExoplayerView extends FrameLayout implements * @return The configured IMA server-side ad insertion AdsLoader */ private ImaServerSideAdInsertionMediaSource.AdsLoader createAdsLoader() { - ImaServerSideAdInsertionMediaSource.AdsLoader.Builder adsLoaderBuilder = new ImaServerSideAdInsertionMediaSource.AdsLoader.Builder( - getContext(), exoPlayerView.getPlayerView()) - .setAdEventListener(this) - .setAdErrorListener(this); + ImaServerSideAdInsertionMediaSource.AdsLoader.Builder adsLoaderBuilder = + new ImaServerSideAdInsertionMediaSource.AdsLoader.Builder(getContext(), exoPlayerView.getPlayerView()) + .setAdEventListener(this) + .setAdErrorListener(this); return adsLoaderBuilder.build(); } @@ -2939,8 +2815,8 @@ public class ReactExoplayerView extends FrameLayout implements DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(getContext()); DefaultMediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(dataSourceFactory); - ImaServerSideAdInsertionMediaSource.Factory adsMediaSourceFactory = new ImaServerSideAdInsertionMediaSource.Factory( - daiAdsLoader, mediaSourceFactory); + ImaServerSideAdInsertionMediaSource.Factory adsMediaSourceFactory = + new ImaServerSideAdInsertionMediaSource.Factory(daiAdsLoader, mediaSourceFactory); mediaSourceFactory.setServerSideAdInsertionMediaSourceFactory(adsMediaSourceFactory); @@ -2974,8 +2850,7 @@ public class ReactExoplayerView extends FrameLayout implements /** * Requests a DAI stream from Google IMA using the ExoPlayer IMA extension. * - * Builds an SSAI URI based on the provided parameters and sets it on the - * player. + * Builds an SSAI URI based on the provided parameters and sets it on the player. * Supports both VOD (contentSourceId + videoId) and Live (assetKey) streams. * * @param runningSource The source containing DAI properties @@ -3008,8 +2883,7 @@ public class ReactExoplayerView extends FrameLayout implements .build() .buildUpon(); } else { - throw new IllegalArgumentException( - "Either assetKey (for live) or contentSourceId+videoId (for VOD) must be provided"); + throw new IllegalArgumentException("Either assetKey (for live) or contentSourceId+videoId (for VOD) must be provided"); } Map adTagParameters = adsProps.getAdTagParameters(); @@ -3032,8 +2906,7 @@ public class ReactExoplayerView extends FrameLayout implements /** * Handles fallback to backup stream when DAI stream fails. * - * If a backup stream URI is available in the DAI properties, it cleans up DAI - * resources + * If a backup stream URI is available in the DAI properties, it cleans up DAI resources * and switches to the backup stream. * * @return true if backup stream was successfully used, false otherwise diff --git a/src/components/player/hooks/useWatchProgress.ts b/src/components/player/hooks/useWatchProgress.ts index b873756d..f5a531e7 100644 --- a/src/components/player/hooks/useWatchProgress.ts +++ b/src/components/player/hooks/useWatchProgress.ts @@ -189,7 +189,7 @@ export const useWatchProgress = ( currentTmdbId ); } else if (type === 'movie' && currentImdbId) { - watchedService.markMovieAsWatched(currentImdbId, new Date(), currentMalId, currentTmdbId); + watchedService.markMovieAsWatched(currentImdbId, new Date(), currentMalId, currentTmdbId, title); } } } catch (error) { diff --git a/src/services/mal/MalSync.ts b/src/services/mal/MalSync.ts index 6ab08956..09f1d04b 100644 --- a/src/services/mal/MalSync.ts +++ b/src/services/mal/MalSync.ts @@ -375,28 +375,31 @@ export const MalSync = { const currentLibrary = await catalogService.getLibraryItems(); const libraryIds = new Set(currentLibrary.map(l => l.id)); - // Process items in parallel - await Promise.all(response.data.map(async (item) => { - const malId = item.node.id; - const { imdbId } = await MalSync.getIdsFromMalId(malId); - - if (imdbId && !libraryIds.has(imdbId)) { - const type = item.node.media_type === 'movie' ? 'movie' : 'series'; - logger.log(`[MalSync] Auto-adding to library: ${item.node.title} (${imdbId})`); + // Process items in small batches to avoid rate limiting + for (let i = 0; i < response.data.length; i += 5) { + const batch = response.data.slice(i, i + 5); + await Promise.all(batch.map(async (item) => { + const malId = item.node.id; + const { imdbId } = await MalSync.getIdsFromMalId(malId); - await catalogService.addToLibrary({ - id: imdbId, - type: type, - name: item.node.title, - poster: item.node.main_picture?.large || item.node.main_picture?.medium || '', - posterShape: 'poster', - year: item.node.start_season?.year, - description: '', - genres: [], - inLibrary: true, - }); - } - })); + if (imdbId && !libraryIds.has(imdbId)) { + const type = item.node.media_type === 'movie' ? 'movie' : 'series'; + logger.log(`[MalSync] Auto-adding to library: ${item.node.title} (${imdbId})`); + + await catalogService.addToLibrary({ + id: imdbId, + type: type, + name: item.node.title, + poster: item.node.main_picture?.large || item.node.main_picture?.medium || '', + posterShape: 'poster', + year: item.node.start_season?.year, + description: '', + genres: [], + inLibrary: true, + }); + } + })); + } } catch (e) { logger.error('[MalSync] syncMalWatchingToLibrary failed:', e); } diff --git a/src/services/watchedService.ts b/src/services/watchedService.ts index 67e3ed52..a923bf7f 100644 --- a/src/services/watchedService.ts +++ b/src/services/watchedService.ts @@ -203,7 +203,8 @@ class WatchedService { imdbId: string, watchedAt: Date = new Date(), malId?: number, - tmdbId?: number + tmdbId?: number, + title?: string ): Promise<{ success: boolean; syncedToTrakt: boolean }> { try { logger.log(`[WatchedService] Marking movie as watched: ${imdbId}`); @@ -221,7 +222,7 @@ class WatchedService { const malToken = MalAuth.getToken(); if (malToken) { MalSync.scrobbleEpisode( - '', // Don't use IMDb ID as title fallback (unlikely to match) + title || '', // Use real title if provided for search fallback 1, 1, 'movie',