-
Notifications
You must be signed in to change notification settings - Fork 65
feat: Migrate to Androidx.Media3 Exoplayer. #502
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,9 +32,10 @@ import android.preference.PreferenceManager | |
| import androidx.lifecycle.ProcessLifecycleOwner | ||
| import androidx.lifecycle.lifecycleScope | ||
| import androidx.localbroadcastmanager.content.LocalBroadcastManager | ||
| import com.google.android.exoplayer2.database.ExoDatabaseProvider | ||
| import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor | ||
| import com.google.android.exoplayer2.upstream.cache.SimpleCache | ||
| import androidx.media3.common.util.UnstableApi | ||
| import androidx.media3.database.ExoDatabaseProvider | ||
| import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor | ||
| import androidx.media3.datasource.cache.SimpleCache | ||
| import com.google.firebase.analytics.FirebaseAnalytics | ||
| import kotlinx.coroutines.Dispatchers | ||
| import kotlinx.coroutines.launch | ||
|
|
@@ -99,14 +100,16 @@ open class SerenityApplication : Application() { | |
| Toothpick.inject(this, scope) | ||
| } | ||
|
|
||
| @UnstableApi | ||
| override fun onCreate() { | ||
| super.onCreate() | ||
| init() | ||
| setDefaultPreferences() | ||
| discoverServers() | ||
| MediaCodecInfoUtil.logAvailableCodecs() | ||
|
|
||
| val leastRecentlyUsedCacheEvictor = LeastRecentlyUsedCacheEvictor((200 * 1024 * 1024).toLong()) | ||
| val leastRecentlyUsedCacheEvictor = | ||
| LeastRecentlyUsedCacheEvictor((200 * 1024 * 1024).toLong()) | ||
| val exoDatabaseProvider = ExoDatabaseProvider(this) | ||
| simpleCache = SimpleCache(cacheDir, leastRecentlyUsedCacheEvictor, exoDatabaseProvider) | ||
|
Comment on lines
+111
to
114
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Call SimpleCache requires calling 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -27,8 +27,15 @@ | |||||||||||||||||||||||||||||||||||||||||||
| import android.content.pm.PackageManager; | ||||||||||||||||||||||||||||||||||||||||||||
| import android.os.Build; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| import com.google.android.exoplayer2.C; | ||||||||||||||||||||||||||||||||||||||||||||
| import com.google.android.exoplayer2.audio.AudioCapabilities; | ||||||||||||||||||||||||||||||||||||||||||||
| import androidx.annotation.OptIn; | ||||||||||||||||||||||||||||||||||||||||||||
| import androidx.media3.common.C; | ||||||||||||||||||||||||||||||||||||||||||||
| import androidx.media3.common.MimeTypes; | ||||||||||||||||||||||||||||||||||||||||||||
| import androidx.media3.common.util.UnstableApi; | ||||||||||||||||||||||||||||||||||||||||||||
| import androidx.media3.exoplayer.audio.AudioCapabilities; | ||||||||||||||||||||||||||||||||||||||||||||
| import androidx.media3.exoplayer.mediacodec.MediaCodecInfo; | ||||||||||||||||||||||||||||||||||||||||||||
| import androidx.media3.exoplayer.mediacodec.MediaCodecUtil; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| import java.util.List; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| public class AndroidHelper { | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -65,20 +72,47 @@ public boolean isBravia() { | |||||||||||||||||||||||||||||||||||||||||||
| return Build.MODEL.toLowerCase().contains(ANDROID_BRAVIA_MODEL.toLowerCase()); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| public boolean enableTunneling() { | ||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||
| * Checks if the device supports tunneling for common video formats (HEVC or AVC). | ||||||||||||||||||||||||||||||||||||||||||||
| * @return true if tunneling is supported for either H.265 or H.264. | ||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||
| @OptIn(markerClass = UnstableApi.class) | ||||||||||||||||||||||||||||||||||||||||||||
| public boolean enableTunneling() { | ||||||||||||||||||||||||||||||||||||||||||||
| // Tunneling is practically only useful/stable on API 24+ (Android 7.0) | ||||||||||||||||||||||||||||||||||||||||||||
| if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { | ||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| return isTunnelingSupported(MimeTypes.VIDEO_H265) | ||||||||||||||||||||||||||||||||||||||||||||
| || isTunnelingSupported(MimeTypes.VIDEO_H264); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| public boolean isBeyondTV() { | ||||||||||||||||||||||||||||||||||||||||||||
| return Build.MODEL.toLowerCase().contains(ANDROID_TCL_BEYONDTV5.toLowerCase()) || Build.MODEL.toLowerCase().contains("smart tv"); | ||||||||||||||||||||||||||||||||||||||||||||
| @OptIn(markerClass = UnstableApi.class) | ||||||||||||||||||||||||||||||||||||||||||||
| public boolean isTunnelingSupported(String mimeType) { | ||||||||||||||||||||||||||||||||||||||||||||
| if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { | ||||||||||||||||||||||||||||||||||||||||||||
| 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) { | ||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+96
to
+105
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Silent exception handling hinders debugging. The broad 🔎 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 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| public boolean isLeanbackSupported() { | ||||||||||||||||||||||||||||||||||||||||||||
| final PackageManager pm = context.getPackageManager(); | ||||||||||||||||||||||||||||||||||||||||||||
| return pm.hasSystemFeature(ANDROID_SOFTWARE_LEANBACK); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| public boolean isAudioPassthroughSupported(String codec) { | ||||||||||||||||||||||||||||||||||||||||||||
| @OptIn(markerClass = UnstableApi.class) | ||||||||||||||||||||||||||||||||||||||||||||
| public boolean isAudioPassthroughSupported(String codec) { | ||||||||||||||||||||||||||||||||||||||||||||
| AudioCapabilities audioCapabilities = AudioCapabilities.getCapabilities(context); | ||||||||||||||||||||||||||||||||||||||||||||
| int encoding = 0; | ||||||||||||||||||||||||||||||||||||||||||||
| switch (codec.toLowerCase()) { | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,9 +2,11 @@ | |
|
|
||
| import android.content.Context; | ||
|
|
||
| import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; | ||
| import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; | ||
| import com.google.android.exoplayer2.trackselection.MappingTrackSelector; | ||
|
|
||
| import androidx.media3.common.util.UnstableApi; | ||
| import androidx.media3.exoplayer.trackselection.AdaptiveTrackSelection; | ||
| import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; | ||
| import androidx.media3.exoplayer.trackselection.MappingTrackSelector; | ||
|
Comment on lines
+5
to
+9
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
|
||
| import javax.inject.Inject; | ||
| import javax.inject.Provider; | ||
|
|
@@ -17,7 +19,8 @@ public class DefaultMappingTrackSelectorProvider implements Provider<MappingTrac | |
| @ApplicationContext | ||
| Context context; | ||
|
|
||
| @Override public MappingTrackSelector get() { | ||
| @UnstableApi | ||
| @Override public MappingTrackSelector get() { | ||
| return new DefaultTrackSelector(context, new AdaptiveTrackSelection.Factory()); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
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 100Repository: 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.ktRepository: 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 5Repository: 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 2Repository: 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 2Repository: 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.ktRepository: 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.ktRepository: 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 8Repository: 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.ktRepository: NineWorlds/serenity-android
Length of output: 1392
🏁 Script executed:
rg -n "PlaySessionId" ./jellyfin-lib/src/main/kotlin/ -B 3 -A 3Repository: NineWorlds/serenity-android
Length of output: 1072
🏁 Script executed:
Repository: NineWorlds/serenity-android
Length of output: 460
🏁 Script executed:
rg -n "startPlaying" ./jellyfin-lib/ -A 1 -B 1Repository: NineWorlds/serenity-android
Length of output: 1258
Generate UUID before the API call and add error handling.
The current implementation has two critical issues:
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.Missing error handling: Unlike similar methods (
watched,unwatched), theexecute()result is not checked. The method should verify success before returning the session ID.Note: The
usersService.startPlaying()endpoint does not accept aplaySessionIdquery parameter. Theprogress()overload that acceptsplaySessionIdis currently markedTODO("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