Skip to content

feat: Migrate to Androidx.Media3 Exoplayer.#502

Merged
kingargyle merged 1 commit intodevelopfrom
dac_androidx_media3
Dec 22, 2025
Merged

feat: Migrate to Androidx.Media3 Exoplayer.#502
kingargyle merged 1 commit intodevelopfrom
dac_androidx_media3

Conversation

@kingargyle
Copy link
Contributor

@kingargyle kingargyle commented Dec 22, 2025

This migrates to the Androidx.Media3 from Exoplayer v2. Several minor chnages in setup and playback were done to reduce stuttering and dynamically determine of Tunnelling is supported by the device to help improve playback on lower end devices.

Also increased the buffer size for the playback and network optimizations.

Summary by CodeRabbit

Release Notes

  • Chores
    • Upgraded video playback engine to AndroidX Media3, improving stability and performance.
    • Enhanced audio and video codec tunneling support for compatible devices.
    • Optimized internal logging for improved system efficiency.
    • Updated media playback library dependencies for long-term compatibility.

✏️ Tip: You can customize this high-level summary in your review settings.

This migrates to the Androidx.Media3 from Exoplayer v2.
Several minor chnages in setup and playback were done to reduce
stuttering and dynamically determine of Tunnelling is supported
by the device to help improve playback on lower end devices.

Also increased the buffer size for the playback and network
optimizations.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 22, 2025

Walkthrough

This PR migrates the video player infrastructure from ExoPlayer 2 to AndroidX Media3 across the entire codebase, updating imports, dependencies, and API usage patterns. Additional changes include adjusting HTTP logging levels, enhancing Jellyfin API methods for play session tracking, and implementing tunneling support detection in AndroidHelper.

Changes

Cohort / File(s) Change Summary
Gradle Dependency Migration
gradle/libs.versions.toml, serenity-app/build.gradle.kts
Replaced ExoPlayer 2 dependencies with androidx.media3 equivalents and introduced numerous new library declarations (Glide, Moxy, Retrofit, OkHttp, Kotlin coroutines, etc.). Added media3Version and removed exoplayerVersion from version catalog.
Core Application Initialization
serenity-app/src/main/java/us/nineworlds/serenity/SerenityApplication.kt
Replaced ExoPlayer cache classes (ExoDatabaseProvider, LeastRecentlyUsedCacheEvictor, SimpleCache) with androidx.media3 equivalents. Added @UnstableApi annotation to onCreate() method.
Data Source & Injection Providers
serenity-app/src/main/java/us/nineworlds/serenity/injection/modules/ExoplayerVideoModule.java, DataSourceFactoryProvider.kt, DefaultMappingTrackSelectorProvider.java, EventLoggerProvider.java, HttpDataSourceFactoryProvider.java
Updated imports from ExoPlayer 2 to androidx.media3. Added @OptIn(UnstableApi::class) annotations to various provider methods and classes. Refactored HttpDataSourceFactoryProvider to use OkHttpDataSource.Factory(okHttpClient).setUserAgent(userAgent) pattern.
Video Player Event Handling & Logging
serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerPresenter.kt, EventLogger.java
Updated player callback signatures: onPositionDiscontinuity(oldPosition, newPosition, reason), onPlaybackStateChanged(playbackState), onIsLoadingChanged(isLoading). Updated EventLogger method signatures to include DecoderReuseEvaluation parameter. Added @UnstableApi annotation to EventLogger class.
Video Player Initialization & Configuration
serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivity.kt, VideoKeyCodeHandlerDelegate.kt
Migrated player construction to ExoPlayer.Builder() pattern with separate renderers factory. Introduced audio offload preferences via TrackSelectionParameters.AudioOffloadPreferences. Increased buffer durations and added 40 MB target buffer. Updated all imports to androidx.media3.
Layout Resources
serenity-app/src/main/res/layout-large/activity_exoplayer_video.xml, serenity-app/src/main/res/layout/exo_playback_control_view.xml, serenity-app/src/main/res/layout/exo_player_view.xml
Replaced ExoPlayer UI component class references from com.google.android.exoplayer2.ui.* to androidx.media3.ui.* (PlayerView, AspectRatioFrameLayout, SubtitleView, DefaultTimeBar).
Test Files
serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivityTest.kt, VideoKeyCodeHandlerDelegateTest.kt, ShadowSubtitleView.java
Updated mock types from SimpleExoPlayer to ExoPlayer. Renamed test function bindsSimpleExoPlayerView() to bindsExoPlayerView(). Updated all ExoPlayer imports to androidx.media3 equivalents.
Color Resources
serenity-app/src/main/res/values/legacy_colors.xml
Added new color resource exo_error_message_background_color with value #AA000000.
HTTP Logging Configuration
emby-lib/src/main/kotlin/us/nineworlds/serenity/emby/server/api/EmbyAPIClient.kt
Changed HttpLoggingInterceptor level from BODY to BASIC to reduce logged detail.
Jellyfin API Enhancements
jellyfin-lib/src/main/kotlin/us/nineworlds/serenity/jellyfin/server/api/JellyfinAPIClient.kt
Updated startPlaying(itemId: String) to return String? (UUID). Added new overload progress(key: String, offset: String, playSessionId: String?): Boolean for play session tracking.
Tunneling Support Detection
serenity-app/src/main/java/us/nineworlds/serenity/core/util/AndroidHelper.java
Implemented enableTunneling() with API level check (>= API 24) for H.264/H.265 support. Added isTunnelingSupported(String mimeType) method with device capability checks and exception handling. Removed isBeyondTV() method. Added @OptIn(UnstableApi.class) annotation to isAudioPassthroughSupported().

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Specific areas requiring extra attention:
    • ExoplayerVideoActivity.kt — significant player construction refactoring with new builder pattern, audio offload preferences, and buffering configuration changes
    • HttpDataSourceFactoryProvider.java — API pattern change from constructor to factory method chain
    • AndroidHelper.java — new tunneling detection logic with MediaCodec utility integration and exception handling
    • JellyfinAPIClient.kt — API signature changes (return type change for startPlaying) and new method overload; verify compatibility with callers
    • Test updates — ensure mock/assertion changes align with new Media3 types and behavior

Possibly related PRs

Poem

🐰 From Exo to Media3, we hoppy-fy the way,
With builders, buffers, offload—a modern player's day!
No more SimpleExoPlayer, let UnstableApi reign,
The Android video migration flows like spring's sweet rain! 🎬✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.13% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: migration from ExoPlayer to AndroidX Media3, which is the primary focus across all modified files.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dac_androidx_media3

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sonarqubecloud
Copy link

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (12)
emby-lib/src/main/kotlin/us/nineworlds/serenity/emby/server/api/EmbyAPIClient.kt (1)

58-58: Logging level change improves security.

Reducing the logging level from BODY to BASIC prevents sensitive data (authentication tokens, user credentials, personal information) from being logged, which is a good security practice.

Optional: Consider build-variant-specific logging

For better debugging during development while maintaining security in production, you could make the logging level conditional:

-logger.level = HttpLoggingInterceptor.Level.BASIC
+logger.level = if (BuildConfig.DEBUG) {
+    HttpLoggingInterceptor.Level.BODY
+} else {
+    HttpLoggingInterceptor.Level.BASIC
+}

This allows full request/response logging during debug builds while keeping production logs minimal.

serenity-app/src/test/kotlin/us/nineworlds/serenity/test/ShadowSubtitleView.java (1)

1-30: Consider converting this Java file to Kotlin.

This Java file resides in the kotlin/ source directory. As per coding guidelines, Kotlin should be the primary language with Java code converted where feasible. This straightforward Robolectric shadow class is a good candidate for conversion.

Based on coding guidelines: "Use Kotlin as the primary language and convert Java code to Kotlin where feasible".

🔎 Example Kotlin conversion
package us.nineworlds.serenity.test

import androidx.media3.ui.CaptionStyleCompat
import androidx.media3.ui.SubtitleView
import org.robolectric.annotation.Implementation
import org.robolectric.annotation.Implements
import org.robolectric.annotation.RealObject
import org.robolectric.shadows.ShadowViewGroup

@Implements(SubtitleView::class)
class ShadowSubtitleView : ShadowViewGroup() {

  @RealObject
  private lateinit var subtitleView: SubtitleView

  var setUserDefaultTextSize = false
    private set

  @Implementation
  fun setUserDefaultTextSize() {
    setUserDefaultTextSize = true
  }

  fun isSetUserDefaultTextSizeCalled(): Boolean = setUserDefaultTextSize

  @Implementation
  fun setUserDefaultStyle() {
    subtitleView.setStyle(CaptionStyleCompat.DEFAULT)
  }
}
serenity-app/src/main/java/us/nineworlds/serenity/injection/modules/providers/HttpDataSourceFactoryProvider.java (1)

1-21: Consider converting to Kotlin per coding guidelines.

As per coding guidelines, this simple provider class should be converted from Java to Kotlin. The straightforward structure makes it highly feasible for migration.

🔎 Proposed Kotlin conversion

Create a new file HttpDataSourceFactoryProvider.kt:

package us.nineworlds.serenity.injection.modules.providers

import androidx.media3.datasource.HttpDataSource
import androidx.media3.datasource.okhttp.OkHttpDataSource
import okhttp3.OkHttpClient
import javax.inject.Inject
import javax.inject.Provider

class HttpDataSourceFactoryProvider @Inject constructor(
  private val okHttpClient: OkHttpClient
) : Provider<HttpDataSource.Factory> {

  override fun get(): HttpDataSource.Factory {
    return OkHttpDataSource.Factory(okHttpClient).setUserAgent(USER_AGENT)
  }

  companion object {
    private const val USER_AGENT = "SerenityAndroid"
  }
}

Then delete this Java file.

Based on coding guidelines: "Use Kotlin as the primary language and convert Java code to Kotlin where feasible."

serenity-app/src/main/java/us/nineworlds/serenity/core/util/AndroidHelper.java (1)

75-87: Tunneling detection logic is sound.

The API level guard and MIME type checks for H.265/H.264 are appropriate. The logic correctly determines tunneling support for common video codecs.

However, per coding guidelines, this file should be converted to Kotlin. Since this is new functionality being added as part of the Media3 migration, consider converting AndroidHelper.java to AndroidHelper.kt.

As per coding guidelines: "Use Kotlin as the primary language and convert Java code to Kotlin where feasible."

serenity-app/src/main/java/us/nineworlds/serenity/injection/modules/providers/EventLoggerProvider.java (1)

1-21: Consider converting to Kotlin per coding guidelines.

This Provider class is written in Java, but the project's coding guidelines specify using Kotlin as the primary language and converting Java code to Kotlin where feasible. This straightforward Provider class with minimal boilerplate would be a good candidate for Kotlin conversion.

As per coding guidelines, Kotlin is preferred for new and refactored code.

🔎 Example Kotlin conversion
package us.nineworlds.serenity.injection.modules.providers

import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.trackselection.MappingTrackSelector
import us.nineworlds.serenity.ui.video.player.EventLogger
import javax.inject.Inject
import javax.inject.Provider

@UnstableApi
class EventLoggerProvider @Inject constructor(
  private val mappingTrackSelector: MappingTrackSelector
) : Provider<EventLogger> {
  override fun get(): EventLogger = EventLogger(mappingTrackSelector)
}
serenity-app/src/main/java/us/nineworlds/serenity/ui/video/player/EventLogger.java (3)

100-102: Method signature correctly updated for Media3.

The onAudioInputFormatChanged signature now matches the Media3 AudioRendererEventListener interface by including the @Nullable DecoderReuseEvaluation parameter.

Note: The decoderReuseEvaluation parameter is currently unused in the logging output. If enhanced debugging information would be helpful, you could optionally log decoder reuse details (e.g., whether a decoder was reused or reinitialized).


119-121: Method signature correctly updated for Media3.

The onVideoInputFormatChanged signature now matches the Media3 VideoRendererEventListener interface by including the @Nullable DecoderReuseEvaluation parameter.

Note: The decoderReuseEvaluation parameter is currently unused in the logging output. If enhanced debugging information would be helpful, you could optionally log decoder reuse details (e.g., whether a decoder was reused or reinitialized).


1-204: Consider converting to Kotlin per coding guidelines.

This EventLogger class is written in Java, but the project's coding guidelines specify using Kotlin as the primary language and converting Java code to Kotlin where feasible. While this is a more substantial class than EventLoggerProvider, it would still benefit from Kotlin's concise syntax, null safety, and improved string formatting capabilities.

As per coding guidelines, Kotlin is preferred for new and refactored code.

serenity-app/src/main/java/us/nineworlds/serenity/SerenityApplication.kt (1)

103-103: Consider using @OptIn instead of @UnstableApi.

In Kotlin, the idiomatic way to opt-in to unstable APIs is using @OptIn(UnstableApi::class) rather than annotating the method with @UnstableApi directly.

🔎 Proposed refactor
-    @UnstableApi
+    @OptIn(UnstableApi::class)
     override fun onCreate() {
serenity-app/src/main/java/us/nineworlds/serenity/injection/modules/providers/DefaultMappingTrackSelectorProvider.java (1)

22-24: Consider converting to Kotlin per coding guidelines.

The @UnstableApi annotation is correctly applied to mark usage of unstable Media3 APIs. However, the coding guidelines specify to "Use Kotlin as the primary language and convert Java code to Kotlin where feasible."

This provider class is a good candidate for Kotlin conversion, which would align with similar providers like DataSourceFactoryProvider.kt already using Kotlin.

🔎 Proposed Kotlin conversion

Create a new file DefaultMappingTrackSelectorProvider.kt:

package us.nineworlds.serenity.injection.modules.providers

import android.content.Context
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.trackselection.AdaptiveTrackSelection
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import androidx.media3.exoplayer.trackselection.MappingTrackSelector
import us.nineworlds.serenity.common.android.injection.ApplicationContext
import javax.inject.Inject
import javax.inject.Provider

class DefaultMappingTrackSelectorProvider : Provider<MappingTrackSelector> {

    @Inject
    @ApplicationContext
    lateinit var context: Context

    @OptIn(UnstableApi::class)
    override fun get(): MappingTrackSelector {
        return DefaultTrackSelector(context, AdaptiveTrackSelection.Factory())
    }
}

Then delete the Java file.

serenity-app/src/main/java/us/nineworlds/serenity/injection/modules/ExoplayerVideoModule.java (1)

18-21: Migration looks correct; consider class-level annotation for consistency.

The @OptIn(markerClass = UnstableApi.class) annotation is correctly applied to opt into Media3's unstable APIs. However, the related providers (EventLoggerProvider, EventLogger) use @UnstableApi at the class level. For consistency, consider moving the annotation to the class level:

+@OptIn(markerClass = UnstableApi.class)
 public class ExoplayerVideoModule extends Module {
 
-    @OptIn(markerClass = UnstableApi.class)
     public ExoplayerVideoModule() {

As per coding guidelines, this Java file could be converted to Kotlin in a future cleanup.

jellyfin-lib/src/main/kotlin/us/nineworlds/serenity/jellyfin/server/api/JellyfinAPIClient.kt (1)

428-434: Unimplemented progress overload with playSessionId.

The new progress overload that accepts a playSessionId parameter is currently unimplemented. This method is likely needed for proper play session tracking with the Media3 migration.

Would you like me to generate an implementation based on the existing progress method at lines 414-426? The implementation would need to pass the playSessionId to the usersService.progress() call. Please verify whether this should be implemented now or tracked as a follow-up task.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cf4df5c and 6c5664e.

📒 Files selected for processing (22)
  • emby-lib/src/main/kotlin/us/nineworlds/serenity/emby/server/api/EmbyAPIClient.kt
  • gradle/libs.versions.toml
  • jellyfin-lib/src/main/kotlin/us/nineworlds/serenity/jellyfin/server/api/JellyfinAPIClient.kt
  • serenity-app/build.gradle.kts
  • serenity-app/src/main/java/us/nineworlds/serenity/SerenityApplication.kt
  • serenity-app/src/main/java/us/nineworlds/serenity/core/util/AndroidHelper.java
  • serenity-app/src/main/java/us/nineworlds/serenity/injection/modules/ExoplayerVideoModule.java
  • serenity-app/src/main/java/us/nineworlds/serenity/injection/modules/providers/DataSourceFactoryProvider.kt
  • serenity-app/src/main/java/us/nineworlds/serenity/injection/modules/providers/DefaultMappingTrackSelectorProvider.java
  • serenity-app/src/main/java/us/nineworlds/serenity/injection/modules/providers/EventLoggerProvider.java
  • serenity-app/src/main/java/us/nineworlds/serenity/injection/modules/providers/HttpDataSourceFactoryProvider.java
  • serenity-app/src/main/java/us/nineworlds/serenity/ui/video/player/EventLogger.java
  • serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerPresenter.kt
  • serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivity.kt
  • serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/VideoKeyCodeHandlerDelegate.kt
  • serenity-app/src/main/res/layout-large/activity_exoplayer_video.xml
  • serenity-app/src/main/res/layout/exo_playback_control_view.xml
  • serenity-app/src/main/res/layout/exo_player_view.xml
  • serenity-app/src/main/res/values/legacy_colors.xml
  • serenity-app/src/test/kotlin/us/nineworlds/serenity/test/ShadowSubtitleView.java
  • serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivityTest.kt
  • serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/VideoKeyCodeHandlerDelegateTest.kt
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{java,kt}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{java,kt}: Use Kotlin as the primary language and convert Java code to Kotlin where feasible
Enforce proper formatting using ./gradlew spotlessApply
Use Toothpick for dependency injection

Files:

  • serenity-app/src/test/kotlin/us/nineworlds/serenity/test/ShadowSubtitleView.java
  • serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/VideoKeyCodeHandlerDelegateTest.kt
  • serenity-app/src/main/java/us/nineworlds/serenity/SerenityApplication.kt
  • serenity-app/src/main/java/us/nineworlds/serenity/injection/modules/providers/EventLoggerProvider.java
  • serenity-app/src/main/java/us/nineworlds/serenity/injection/modules/providers/DefaultMappingTrackSelectorProvider.java
  • serenity-app/src/main/java/us/nineworlds/serenity/injection/modules/providers/DataSourceFactoryProvider.kt
  • serenity-app/src/main/java/us/nineworlds/serenity/injection/modules/ExoplayerVideoModule.java
  • serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivity.kt
  • serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/VideoKeyCodeHandlerDelegate.kt
  • serenity-app/src/main/java/us/nineworlds/serenity/ui/video/player/EventLogger.java
  • emby-lib/src/main/kotlin/us/nineworlds/serenity/emby/server/api/EmbyAPIClient.kt
  • serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerPresenter.kt
  • jellyfin-lib/src/main/kotlin/us/nineworlds/serenity/jellyfin/server/api/JellyfinAPIClient.kt
  • serenity-app/src/main/java/us/nineworlds/serenity/core/util/AndroidHelper.java
  • serenity-app/src/main/java/us/nineworlds/serenity/injection/modules/providers/HttpDataSourceFactoryProvider.java
  • serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivityTest.kt
**/*.kt

📄 CodeRabbit inference engine (AGENTS.md)

Follow Kotlin naming conventions and style guidelines

Files:

  • serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/VideoKeyCodeHandlerDelegateTest.kt
  • serenity-app/src/main/java/us/nineworlds/serenity/SerenityApplication.kt
  • serenity-app/src/main/java/us/nineworlds/serenity/injection/modules/providers/DataSourceFactoryProvider.kt
  • serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivity.kt
  • serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/VideoKeyCodeHandlerDelegate.kt
  • emby-lib/src/main/kotlin/us/nineworlds/serenity/emby/server/api/EmbyAPIClient.kt
  • serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerPresenter.kt
  • jellyfin-lib/src/main/kotlin/us/nineworlds/serenity/jellyfin/server/api/JellyfinAPIClient.kt
  • serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivityTest.kt
**/*Test.kt

📄 CodeRabbit inference engine (AGENTS.md)

**/*Test.kt: All mocking operations must use MockK and no other mocking libraries are permitted
DO NOT USE MockK annotations; prefer field level creation of mocks directly
Write unit tests for all new features, bug fixes, or critical code changes
Define mocks as properties, initialized with mockk(relaxed = true), and use every for expectations and verify for assertions
Define setup and cleanup routines explicitly in unit tests, ensuring all mocks are cleared in @After blocks using clearAllMocks() and Toothpick.reset()
Use @BeforeClass and @AfterClass for static mocks in unit tests
Extend us.nineworlds.serenity.test.InjectingTest for Toothpick-based tests and override setUp with a call to super.setUp() as the first action
Bind mocks to instances in installTestModules using the Module pattern: bind(MainPresenter::class.java).toInstance(mockPresenter)

Files:

  • serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/VideoKeyCodeHandlerDelegateTest.kt
  • serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivityTest.kt
**/*Activity.kt

📄 CodeRabbit inference engine (AGENTS.md)

**/*Activity.kt: DO NOT use @InjectPresenter on fields; instead inject Providers and use moxyPresenter delegate when converting Moxy MVP Activities to Kotlin
Inject Providers using @Inject lateinit var presenterProvider: Provider<MainPresenter> and use moxyPresenter delegate: internal val presenter by moxyPresenter { presenterProvider.get() }
Always replace findViewById with View Binding in Activities
Use the moxyPresenter delegate pattern for presenter injection in Activities

Files:

  • serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivity.kt
🧠 Learnings (11)
📚 Learning: 2025-12-16T18:16:58.793Z
Learnt from: CR
Repo: NineWorlds/serenity-android PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-16T18:16:58.793Z
Learning: Applies to **/*Test.kt : DO NOT USE MockK annotations; prefer field level creation of mocks directly

Applied to files:

  • serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/VideoKeyCodeHandlerDelegateTest.kt
  • serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivityTest.kt
📚 Learning: 2025-12-16T18:16:58.793Z
Learnt from: CR
Repo: NineWorlds/serenity-android PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-16T18:16:58.793Z
Learning: Applies to **/*Test.kt : All mocking operations must use MockK and no other mocking libraries are permitted

Applied to files:

  • serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/VideoKeyCodeHandlerDelegateTest.kt
  • serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivityTest.kt
📚 Learning: 2025-12-16T18:16:58.793Z
Learnt from: CR
Repo: NineWorlds/serenity-android PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-16T18:16:58.793Z
Learning: Applies to **/*Test.kt : Define mocks as properties, initialized with `mockk(relaxed = true)`, and use `every` for expectations and `verify` for assertions

Applied to files:

  • serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/VideoKeyCodeHandlerDelegateTest.kt
  • serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivityTest.kt
📚 Learning: 2025-12-16T18:16:58.793Z
Learnt from: CR
Repo: NineWorlds/serenity-android PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-16T18:16:58.793Z
Learning: Applies to **/*Test.kt : Define setup and cleanup routines explicitly in unit tests, ensuring all mocks are cleared in `After` blocks using `clearAllMocks()` and `Toothpick.reset()`

Applied to files:

  • serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/VideoKeyCodeHandlerDelegateTest.kt
📚 Learning: 2025-12-16T18:16:58.793Z
Learnt from: CR
Repo: NineWorlds/serenity-android PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-16T18:16:58.793Z
Learning: Applies to **/*Activity.kt : Inject Providers using `Inject lateinit var presenterProvider: Provider<MainPresenter>` and use moxyPresenter delegate: `internal val presenter by moxyPresenter { presenterProvider.get() }`

Applied to files:

  • serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/VideoKeyCodeHandlerDelegateTest.kt
  • serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivity.kt
  • serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/VideoKeyCodeHandlerDelegate.kt
  • serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerPresenter.kt
📚 Learning: 2025-12-16T18:16:58.793Z
Learnt from: CR
Repo: NineWorlds/serenity-android PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-16T18:16:58.793Z
Learning: Applies to **/*Test.kt : Bind mocks to instances in `installTestModules` using the Module pattern: `bind(MainPresenter::class.java).toInstance(mockPresenter)`

Applied to files:

  • serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/VideoKeyCodeHandlerDelegateTest.kt
  • serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivityTest.kt
📚 Learning: 2025-12-16T18:16:58.793Z
Learnt from: CR
Repo: NineWorlds/serenity-android PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-16T18:16:58.793Z
Learning: Applies to **/*Activity.kt : Use the moxyPresenter delegate pattern for presenter injection in Activities

Applied to files:

  • serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/VideoKeyCodeHandlerDelegateTest.kt
  • serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivity.kt
  • serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/VideoKeyCodeHandlerDelegate.kt
  • serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerPresenter.kt
📚 Learning: 2025-12-16T18:16:58.793Z
Learnt from: CR
Repo: NineWorlds/serenity-android PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-16T18:16:58.793Z
Learning: Applies to **/*Activity.kt : DO NOT use `InjectPresenter` on fields; instead inject Providers and use `moxyPresenter` delegate when converting Moxy MVP Activities to Kotlin

Applied to files:

  • serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/VideoKeyCodeHandlerDelegateTest.kt
  • serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivity.kt
  • serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/VideoKeyCodeHandlerDelegate.kt
  • serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerPresenter.kt
📚 Learning: 2025-12-16T18:16:58.793Z
Learnt from: CR
Repo: NineWorlds/serenity-android PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-16T18:16:58.793Z
Learning: Applies to **/*Test.kt : Use `BeforeClass` and `AfterClass` for static mocks in unit tests

Applied to files:

  • serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/VideoKeyCodeHandlerDelegateTest.kt
  • serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivityTest.kt
📚 Learning: 2025-12-16T18:16:58.793Z
Learnt from: CR
Repo: NineWorlds/serenity-android PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-16T18:16:58.793Z
Learning: Applies to **/*.kt : Follow Kotlin naming conventions and style guidelines

Applied to files:

  • gradle/libs.versions.toml
📚 Learning: 2025-12-16T18:16:58.793Z
Learnt from: CR
Repo: NineWorlds/serenity-android PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-16T18:16:58.793Z
Learning: Applies to **/*Activity.kt : Always replace `findViewById` with View Binding in Activities

Applied to files:

  • serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivityTest.kt
🧬 Code graph analysis (5)
serenity-app/src/main/java/us/nineworlds/serenity/injection/modules/providers/EventLoggerProvider.java (1)
serenity-app/src/main/java/us/nineworlds/serenity/ui/video/player/EventLogger.java (1)
  • UnstableApi (55-204)
serenity-app/src/main/java/us/nineworlds/serenity/injection/modules/providers/DefaultMappingTrackSelectorProvider.java (3)
serenity-app/src/main/java/us/nineworlds/serenity/injection/modules/providers/EventLoggerProvider.java (1)
  • UnstableApi (12-21)
serenity-app/src/main/java/us/nineworlds/serenity/ui/video/player/EventLogger.java (1)
  • UnstableApi (55-204)
serenity-app/src/main/java/us/nineworlds/serenity/injection/modules/providers/DataSourceFactoryProvider.kt (1)
  • get (24-32)
serenity-app/src/main/java/us/nineworlds/serenity/injection/modules/ExoplayerVideoModule.java (1)
serenity-app/src/main/java/us/nineworlds/serenity/ui/video/player/EventLogger.java (1)
  • UnstableApi (55-204)
serenity-app/src/main/java/us/nineworlds/serenity/ui/video/player/EventLogger.java (1)
serenity-app/src/main/java/us/nineworlds/serenity/injection/modules/providers/EventLoggerProvider.java (1)
  • UnstableApi (12-21)
jellyfin-lib/src/main/kotlin/us/nineworlds/serenity/jellyfin/server/api/JellyfinAPIClient.kt (1)
emby-lib/src/main/kotlin/us/nineworlds/serenity/emby/server/api/EmbyAPIClient.kt (2)
  • fetchUserId (547-547)
  • headerMap (160-169)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (32)
serenity-app/src/test/kotlin/us/nineworlds/serenity/test/ShadowSubtitleView.java (1)

4-5: LGTM! Import migration correctly updates to AndroidX Media3.

The imports have been properly migrated from ExoPlayer v2 to AndroidX Media3, aligning with the PR's migration objectives.

serenity-app/src/main/res/values/legacy_colors.xml (1)

31-31: Color resource is correctly defined and actively used.

The semi-transparent black (#AA000000) is appropriate for the error message background overlay. The resource is referenced in serenity-app/src/main/res/layout/exo_player_view.xml at line 45 and used as intended.

serenity-app/src/main/java/us/nineworlds/serenity/injection/modules/providers/DataSourceFactoryProvider.kt (3)

4-10: LGTM! Clean migration to Media3 imports.

The import statements have been correctly updated from ExoPlayer v2 (com.google.android.exoplayer2.*) to AndroidX Media3 (androidx.media3.*) packages. The migration is consistent with the PR objectives.


24-24: LGTM! Correct opt-in for unstable API.

The @OptIn(UnstableApi::class) annotation properly acknowledges the use of unstable Media3 APIs, which is the recommended approach for this migration.


26-28: Use DefaultDataSource.Factory instead of deprecated DefaultDataSourceFactory.

DefaultDataSourceFactory is deprecated in Media3; use DefaultDataSource.Factory instead. DefaultBandwidthMeter.getSingletonInstance(Context) usage is correct and remains the recommended pattern in Media3.

serenity-app/src/main/java/us/nineworlds/serenity/injection/modules/providers/HttpDataSourceFactoryProvider.java (2)

4-5: LGTM! Imports correctly updated for AndroidX Media3.

The imports have been properly migrated from legacy ExoPlayer to androidx.media3.datasource packages.


19-19: The OkHttpDataSource.Factory API usage is correct. The setUserAgent() method properly configures the user agent in OkHttpDataSource.Factory, and this is the recommended approach, as OkHttpDataSourceFactory is deprecated. No changes are needed unless your use case specifically requires additional configuration such as transfer listeners or cache control settings.

serenity-app/src/main/java/us/nineworlds/serenity/core/util/AndroidHelper.java (2)

30-38: LGTM: Media3 imports are appropriate.

The new imports correctly support the migration from ExoPlayer v2 to AndroidX Media3 and provide the necessary classes for tunneling capability detection.


114-136: LGTM: Audio passthrough migration to Media3 is correct.

The @OptIn annotation is properly applied, and the codec encoding mappings correctly use Media3's C.ENCODING_* constants. The logic is sound for checking audio passthrough support.

serenity-app/src/main/java/us/nineworlds/serenity/injection/modules/providers/EventLoggerProvider.java (1)

3-12: LGTM: Media3 migration correctly applied.

The import updates and @UnstableApi annotation are properly implemented for the AndroidX Media3 migration. The annotation is required when working with Media3's unstable APIs like MappingTrackSelector.

serenity-app/src/main/java/us/nineworlds/serenity/ui/video/player/EventLogger.java (1)

21-55: LGTM: Media3 imports and annotation correctly applied.

The comprehensive import migration from ExoPlayer to androidx.media3.* packages is properly implemented. The @UnstableApi annotation correctly marks this class's usage of Media3 unstable APIs.

serenity-app/src/main/java/us/nineworlds/serenity/SerenityApplication.kt (1)

35-38: LGTM! Media3 migration imports are correct.

The imports correctly replace the ExoPlayer v2 equivalents with AndroidX Media3 components for cache management.

serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/VideoKeyCodeHandlerDelegate.kt (1)

7-7: LGTM!

The import migration from com.google.android.exoplayer2.ExoPlayer to androidx.media3.exoplayer.ExoPlayer is correct. The Media3 ExoPlayer interface maintains API compatibility for the methods used in this class (playWhenReady, currentPosition, duration, stop(), seekTo()).

serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/VideoKeyCodeHandlerDelegateTest.kt (1)

5-5: LGTM on the migration changes!

The import and mock type migration from SimpleExoPlayer to ExoPlayer correctly aligns with the production code changes. In Media3, SimpleExoPlayer was removed and ExoPlayer is now the primary interface.

Also applies to: 29-29

serenity-app/src/main/res/layout/exo_playback_control_view.xml (1)

75-79: LGTM!

The DefaultTimeBar widget class is correctly migrated from com.google.android.exoplayer2.ui.DefaultTimeBar to androidx.media3.ui.DefaultTimeBar. The widget attributes and behavior remain unchanged.

gradle/libs.versions.toml (1)

43-43: No action needed. Media3 version 1.9.0 is a valid, stable release.

serenity-app/src/main/res/layout-large/activity_exoplayer_video.xml (1)

12-12: LGTM! Clean migration to Media3 PlayerView.

The namespace update from ExoPlayer v2 to AndroidX Media3 is correct and aligns with the project-wide migration.

serenity-app/src/main/res/layout/exo_player_view.xml (1)

4-4: LGTM! UI component namespace migration is correct.

The update of AspectRatioFrameLayout and SubtitleView to androidx.media3.ui namespace is consistent with the Media3 migration.

Also applies to: 25-25, 48-48

serenity-app/src/test/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivityTest.kt (4)

9-14: LGTM! Import migration to Media3 is correct.

The test imports have been correctly updated to use androidx.media3.* packages, aligning with the ExoPlayer to Media3 migration.


51-51: LGTM! Mock updated to ExoPlayer type.

The mock correctly uses ExoPlayer (Media3) instead of SimpleExoPlayer (ExoPlayer v2). The relaxed mode is appropriate for testing.

Based on learnings, mock definitions follow the project guidelines.


74-74: LGTM! Test name updated to reflect Media3 terminology.

The test name change from bindsSimpleExoPlayerView to bindsExoPlayerView correctly reflects the migration to Media3's ExoPlayer type.


135-135: LGTM! Type assertion updated for Media3.

The assertion correctly checks for ExoPlayer::class.java instead of SimpleExoPlayer::class.java, aligning with the Media3 API.

serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerVideoActivity.kt (5)

15-30: LGTM! Import migration to Media3 is correct.

All ExoPlayer v2 imports have been correctly replaced with their androidx.media3.* equivalents, maintaining the necessary classes for media playback, track selection, and UI components.


50-50: LGTM! UnstableApi annotation added.

The @UnstableApi annotation is correctly applied since Media3 APIs used in this activity are marked as unstable and require opt-in.


220-223: Verify buffer configuration for memory impact.

The buffer configuration has been significantly increased:

  • Max buffer duration: 80 seconds (up from default ~50s)
  • Target buffer size: 40 MB

While this may help reduce stuttering on lower-end devices (as mentioned in PR objectives), it could also:

  • Increase memory pressure on devices with limited RAM
  • Cause OOM issues on very low-end hardware
  • Delay initial playback start

Consider testing on the lowest-end target device to ensure this doesn't cause memory issues.


226-232: LGTM! ExoPlayer construction updated to Media3 Builder pattern.

The player construction correctly uses the new ExoPlayer.Builder API with setRenderersFactory, replacing the old constructor-based approach. This is the proper way to configure ExoPlayer in Media3.


199-216: Audio offload configuration is appropriate but verify device support.

Devices may support offload playback of MP3 and AAC on Android 10+, and Opus on Android 11+. The configuration correctly disables speed change support—offload limits the ability to apply audio effects, like speed changes. Disabling gapless support is an intentional trade-off: offload may result in silence gaps between tracks in order to prioritize battery saving. If your target devices require playback speed adjustment, consider using setIsSpeedChangeSupportRequired(true), though this may cause issues on some devices like Galaxy Z Fold.

serenity-app/src/main/kotlin/us/nineworlds/serenity/ui/video/player/ExoplayerPresenter.kt (4)

4-5: LGTM! Import migration to Media3 is correct.

The imports have been correctly updated to use androidx.media3.* packages for Player and PlayerControlView.


78-78: LGTM! Player.Listener callback signature updated for Media3.

The onPositionDiscontinuity callback signature has been correctly updated to match Media3's Player.Listener interface, which now includes oldPosition and newPosition parameters. The no-op implementation (returning Unit) is appropriate since the functionality isn't needed.


82-89: LGTM! onPlaybackStateChanged correctly implements Media3 callback.

The method signature and implementation correctly handle the Media3 Player.Listener callback for playback state changes. The STATE_ENDED handling logic remains functionally equivalent.


91-91: LGTM! onIsLoadingChanged callback signature updated.

The callback signature has been correctly updated to match Media3's Player.Listener interface. The no-op implementation is appropriate.

serenity-app/build.gradle.kts (1)

125-127: Media3 dependencies are correctly using the latest stable version.

The version catalog specifies Media3 1.9.0, which is the latest stable release. All three Media3 modules (exoplayer, ui, datasource-okhttp) are properly centralized through the version catalog and will benefit from recent bug fixes and performance improvements.

Comment on lines +486 to 495
override fun startPlaying(itemId: String): String? {
if (userId == null) {
userId = fetchUserId()
}

val call = usersService.startPlaying(headerMap(), userId!!, itemId)

call.execute()
return UUID.randomUUID().toString()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "JellyfinAPIClient.kt" -type f

Repository: NineWorlds/serenity-android

Length of output: 165


🏁 Script executed:

head -n 500 ./jellyfin-lib/src/main/kotlin/us/nineworlds/serenity/jellyfin/server/api/JellyfinAPIClient.kt | tail -n 100

Repository: NineWorlds/serenity-android

Length of output: 3551


🏁 Script executed:

rg -nP --type=kt "fun startPlaying\s*\(" --context=3 jellyfin-lib/

Repository: NineWorlds/serenity-android

Length of output: 97


🏁 Script executed:

rg -n "fun startPlaying" --context=3 jellyfin-lib/

Repository: NineWorlds/serenity-android

Length of output: 1886


🏁 Script executed:

sed -n '120,130p' jellyfin-lib/src/main/kotlin/us/nineworlds/serenity/jellyfin/server/api/JellyfinUsersService.kt

Repository: NineWorlds/serenity-android

Length of output: 450


🏁 Script executed:

rg -n "fun progress" jellyfin-lib/src/main/kotlin/us/nineworlds/serenity/jellyfin/server/api/JellyfinUsersService.kt -A 5

Repository: NineWorlds/serenity-android

Length of output: 336


🏁 Script executed:

rg -n "playSessionId\|PlaySessionId" jellyfin-lib/src/main/kotlin/us/nineworlds/serenity/jellyfin/server/api/ -A 2 -B 2

Repository: NineWorlds/serenity-android

Length of output: 53


🏁 Script executed:

rg -rn "playSessionId\|PlaySessionId" jellyfin-lib/

Repository: NineWorlds/serenity-android

Length of output: 53


🏁 Script executed:

rg -n "startPlaying" jellyfin-lib/src/main/kotlin/ -B 2 -A 2

Repository: NineWorlds/serenity-android

Length of output: 1936


🏁 Script executed:

sed -n '425,445p' ./jellyfin-lib/src/main/kotlin/us/nineworlds/serenity/jellyfin/server/api/JellyfinAPIClient.kt

Repository: NineWorlds/serenity-android

Length of output: 845


🏁 Script executed:

sed -n '460,480p' ./jellyfin-lib/src/main/kotlin/us/nineworlds/serenity/jellyfin/server/api/JellyfinAPIClient.kt

Repository: NineWorlds/serenity-android

Length of output: 975


🏁 Script executed:

rg -n "fun progress" jellyfin-lib/src/main/kotlin/us/nineworlds/serenity/jellyfin/server/api/JellyfinUsersService.kt -A 8

Repository: NineWorlds/serenity-android

Length of output: 414


🏁 Script executed:

sed -n '100,140p' ./jellyfin-lib/src/main/kotlin/us/nineworlds/serenity/jellyfin/server/api/JellyfinUsersService.kt

Repository: NineWorlds/serenity-android

Length of output: 1392


🏁 Script executed:

rg -n "PlaySessionId" ./jellyfin-lib/src/main/kotlin/ -B 3 -A 3

Repository: NineWorlds/serenity-android

Length of output: 1072


🏁 Script executed:

rg -n "startPlaying" ./jellyfin-lib/ --type-list -A 1 | head -50

Repository: NineWorlds/serenity-android

Length of output: 460


🏁 Script executed:

rg -n "startPlaying" ./jellyfin-lib/ -A 1 -B 1

Repository: NineWorlds/serenity-android

Length of output: 1258


Generate UUID before the API call and add error handling.

The current implementation has two critical issues:

  1. UUID timing: The UUID is generated on line 494 after call.execute(), which means it's never sent to the server. If this session ID is needed for tracking, it must be generated before the API call.

  2. Missing error handling: Unlike similar methods (watched, unwatched), the execute() result is not checked. The method should verify success before returning the session ID.

Note: The usersService.startPlaying() endpoint does not accept a playSessionId query parameter. The progress() overload that accepts playSessionId is currently marked TODO("Not yet implemented"), so those must be addressed separately. For now, focus on generating the UUID before the call and properly handling the API response:

 override fun startPlaying(itemId: String): String? {
     if (userId == null) {
         userId = fetchUserId()
     }
+    
+    val playSessionId = UUID.randomUUID().toString()
 
-    val call = usersService.startPlaying(headerMap(), userId!!, itemId)
+    val call = usersService.startPlaying(headerMap(), userId!!, itemId)
 
-    call.execute()
-    return UUID.randomUUID().toString()
+    val result = call.execute()
+    return if (result.isSuccessful) playSessionId else null
 }
🤖 Prompt for AI Agents
In
jellyfin-lib/src/main/kotlin/us/nineworlds/serenity/jellyfin/server/api/JellyfinAPIClient.kt
around lines 486–495, generate the play session UUID before making the
usersService.startPlaying call, then pass it along if the request supports it
(or keep it locally for tracking), and call execute() into a val response; check
response.isSuccessful and only return the UUID on success; on failure log or
handle the error and return null. Ensure userId is fetched the same way as
before and avoid generating the UUID after the network call.

Comment on lines +96 to +105
try {
List<MediaCodecInfo> decoderInfos = MediaCodecUtil.getDecoderInfos(mimeType, false, false);
for (MediaCodecInfo info : decoderInfos) {
if (info.capabilities != null && info.capabilities.isFeatureSupported(android.media.MediaCodecInfo.CodecCapabilities.FEATURE_TunneledPlayback)) {
return true;
}
}
} catch (Exception e) {
return false;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Silent exception handling hinders debugging.

The broad catch (Exception e) silently swallows all errors without logging. If MediaCodecUtil throws exceptions due to device issues or unsupported codecs, you'll have no visibility into the failure cause.

🔎 Suggested improvement: Add logging for exceptions
+import android.util.Log;
+
 // ... in method body
     try {
       List<MediaCodecInfo> decoderInfos = MediaCodecUtil.getDecoderInfos(mimeType, false, false);
       for (MediaCodecInfo info : decoderInfos) {
         if (info.capabilities != null && info.capabilities.isFeatureSupported(android.media.MediaCodecInfo.CodecCapabilities.FEATURE_TunneledPlayback)) {
           return true;
         }
       }
     } catch (Exception e) {
+      Log.w("AndroidHelper", "Failed to check tunneling support for " + mimeType, e);
       return false;
     }

Alternatively, narrow the catch to MediaCodecUtil.DecoderQueryException if you only expect that specific exception type.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
List<MediaCodecInfo> decoderInfos = MediaCodecUtil.getDecoderInfos(mimeType, false, false);
for (MediaCodecInfo info : decoderInfos) {
if (info.capabilities != null && info.capabilities.isFeatureSupported(android.media.MediaCodecInfo.CodecCapabilities.FEATURE_TunneledPlayback)) {
return true;
}
}
} catch (Exception e) {
return false;
}
try {
List<MediaCodecInfo> decoderInfos = MediaCodecUtil.getDecoderInfos(mimeType, false, false);
for (MediaCodecInfo info : decoderInfos) {
if (info.capabilities != null && info.capabilities.isFeatureSupported(android.media.MediaCodecInfo.CodecCapabilities.FEATURE_TunneledPlayback)) {
return true;
}
}
} catch (Exception e) {
Log.w("AndroidHelper", "Failed to check tunneling support for " + mimeType, e);
return false;
}
🤖 Prompt for AI Agents
In
serenity-app/src/main/java/us/nineworlds/serenity/core/util/AndroidHelper.java
around lines 96-105, the current catch block swallows all exceptions which hides
failures from MediaCodecUtil; change the catch to either catch the specific
MediaCodecUtil.DecoderQueryException (if available) or keep Exception but log
the exception before returning false (use the class TAG or Android Log.e with a
short message and the exception) so the error is visible for debugging while
preserving the existing false return behavior.

Comment on lines +5 to +9

import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.trackselection.AdaptiveTrackSelection;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
import androidx.media3.exoplayer.trackselection.MappingTrackSelector;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Convert file to Kotlin and verify Media3 API compliance.

The import migration from ExoPlayer v2 to AndroidX.Media3 is correct. However, this file should be converted to Kotlin per the coding guidelines that require Kotlin as the primary language. When converting, use @OptIn annotation for unstable Media3 APIs at the method level in Kotlin, rather than the @UnstableApi annotation used in Java. The DefaultTrackSelector constructor requires Context context, TrackSelectionParameters parameters, and ExoTrackSelection.Factory trackSelectionFactory—verify that the provider passes the correct parameters. Toothpick usage with @Inject is appropriate and can be migrated to Kotlin incrementally while maintaining compatibility.

🤖 Prompt for AI Agents
In
serenity-app/src/main/java/us/nineworlds/serenity/injection/modules/providers/DefaultMappingTrackSelectorProvider.java
around lines 5–9, convert this Java provider to Kotlin and update it to use
Media3 APIs correctly: rewrite the class as a Kotlin provider, annotate the
method that constructs the DefaultTrackSelector with @OptIn(UnstableApi::class)
(at method level) instead of using the Java @UnstableApi, and ensure you call
the DefaultTrackSelector constructor with the correct parameters (Context,
TrackSelectionParameters, and
ExoTrackSelection.Factory/AdaptiveTrackSelection.Factory as required by the
Media3 version you target) passing in the current context, appropriate
TrackSelectionParameters, and a proper trackSelectionFactory; keep the Toothpick
@Inject wiring equivalent in Kotlin to maintain DI compatibility and migrate
incrementally if needed.

Comment on lines +111 to 114
val leastRecentlyUsedCacheEvictor =
LeastRecentlyUsedCacheEvictor((200 * 1024 * 1024).toLong())
val exoDatabaseProvider = ExoDatabaseProvider(this)
simpleCache = SimpleCache(cacheDir, leastRecentlyUsedCacheEvictor, exoDatabaseProvider)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Call simpleCache.release() for proper resource cleanup, but not in onTerminate().

SimpleCache requires calling .release() to properly clean up database resources. However, onTerminate() is only for testing and is not called in production. Consider releasing the cache during app lifecycle events that are reliably called, such as onTrimMemory() or when the last activity is destroyed, to ensure cleanup happens in production scenarios.

🤖 Prompt for AI Agents
In serenity-app/src/main/java/us/nineworlds/serenity/SerenityApplication.kt
around lines 111 to 114, SimpleCache is created but not released; calling
simpleCache.release() in onTerminate() is insufficient because onTerminate() is
not called in production. Add reliable cleanup: implement
Application.ActivityLifecycleCallbacks to track activity count (or override
onTrimMemory) and call simpleCache.release() when the app is
backgrounded/trimmed to a critical level (e.g., TRIM_MEMORY_COMPLETE/UiHidden)
or when the last activity is destroyed; ensure you null out the simpleCache
reference after release, guard against multiple releases, and wrap release in a
try/catch to avoid crashing.

@codecov
Copy link

codecov bot commented Dec 22, 2025

Codecov Report

❌ Patch coverage is 90.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 50.27%. Comparing base (cf4df5c) to head (6c5664e).
⚠️ Report is 2 commits behind head on develop.

Files with missing lines Patch % Lines
...lds/serenity/ui/video/player/ExoplayerPresenter.kt 0.00% 2 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             develop     #502      +/-   ##
=============================================
+ Coverage      50.05%   50.27%   +0.22%     
  Complexity       467      467              
=============================================
  Files             63       63              
  Lines           1996     2005       +9     
  Branches         273      273              
=============================================
+ Hits             999     1008       +9     
  Misses           878      878              
  Partials         119      119              
Flag Coverage Δ
unittests 50.27% <90.00%> (+0.22%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@kingargyle kingargyle merged commit ecfa4e0 into develop Dec 22, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant