Dynamically query device H264 max level for WebView device profile

Instead of hardcoding H264 level "41" in nativeshell.js, query the
actual device max AVC level at runtime via
NativeInterface.getCodecCapabilities(). This lets devices that
support H264 level 4.2, 5.0, etc. direct-play content that was
previously forced to transcode.

The profile list (high/main/baseline/constrained baseline) is left
hardcoded deliberately: Chromium's <video> pipeline in the WebView
does not decode the full set of H264 profiles MediaCodec advertises
(notably 10-bit), so advertising them would risk regressions.

Fixes #1921

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Trenton H
2026-06-12 21:01:59 +02:00
committed by Niels van Velzen
co-authored by Claude Sonnet 4.6
parent c3f3a80581
commit cf4d754890
3 changed files with 26 additions and 2 deletions
+2 -1
View File
@@ -32,6 +32,7 @@ for (const plugin of plugins) {
}
const { deviceId, deviceName, appName, appVersion } = JSON.parse(window.NativeInterface.getDeviceInformation());
const codecCaps = JSON.parse(window.NativeInterface.getCodecCapabilities());
window.NativeShell = {
enableFullscreen() {
@@ -131,7 +132,7 @@ function getDeviceProfile(profileBuilder, item) {
{
Condition: "LessThanEqual",
Property: "VideoLevel",
Value: "41"
Value: codecCaps.h264MaxLevel
}]
});
@@ -8,6 +8,7 @@ import android.webkit.JavascriptInterface
import androidx.core.content.ContextCompat
import org.jellyfin.mobile.events.ActivityEvent
import org.jellyfin.mobile.events.ActivityEventHandler
import org.jellyfin.mobile.player.deviceprofile.DeviceProfileBuilder
import org.jellyfin.mobile.utils.Constants
import org.jellyfin.mobile.utils.Constants.EXTRA_ALBUM
import org.jellyfin.mobile.utils.Constants.EXTRA_ARTIST
@@ -38,6 +39,7 @@ import java.util.UUID
class NativeInterface(private val context: Context) : KoinComponent {
private val activityEventHandler: ActivityEventHandler = get()
private val remoteVolumeProvider: RemoteVolumeProvider by inject()
private val deviceProfileBuilder: DeviceProfileBuilder by inject()
@SuppressLint("HardwareIds")
@JavascriptInterface
@@ -60,6 +62,9 @@ class NativeInterface(private val context: Context) : KoinComponent {
null
}
@JavascriptInterface
fun getCodecCapabilities(): String = deviceProfileBuilder.getWebCodecCapabilitiesJson()
@JavascriptInterface
fun enableFullscreen(): Boolean {
emitEvent(ActivityEvent.ChangeFullscreen(true))
@@ -1,8 +1,10 @@
package org.jellyfin.mobile.player.deviceprofile
import android.media.MediaCodecList
import android.media.MediaFormat
import org.jellyfin.mobile.app.AppPreferences
import org.jellyfin.mobile.utils.Constants
import org.json.JSONObject
import org.jellyfin.sdk.model.api.CodecProfile
import org.jellyfin.sdk.model.api.CodecType
import org.jellyfin.sdk.model.api.ContainerProfile
@@ -23,6 +25,7 @@ class DeviceProfileBuilder(
private val supportedVideoCodecs: Array<Array<String>>
private val supportedAudioCodecs: Array<Array<String>>
private val videoCodecsProfiles: Map<String, Set<String>>
private val maxAvcRawLevel: Int
private val transcodingProfiles: List<TranscodingProfile>
@@ -34,12 +37,21 @@ class DeviceProfileBuilder(
// Load Android-supported codecs
val videoCodecs: MutableMap<String, DeviceCodec.Video> = HashMap()
val audioCodecs: MutableMap<String, DeviceCodec.Audio> = HashMap()
var maxAvcLevel = 0
val androidCodecs = MediaCodecList(MediaCodecList.REGULAR_CODECS)
for (codecInfo in androidCodecs.codecInfos) {
if (codecInfo.isEncoder) continue
for (mimeType in codecInfo.supportedTypes) {
val codec = DeviceCodec.from(codecInfo.getCapabilitiesForType(mimeType)) ?: continue
val capabilities = codecInfo.getCapabilitiesForType(mimeType)
if (mimeType == MediaFormat.MIMETYPE_VIDEO_AVC) {
for (pl in capabilities.profileLevels) {
if (pl.level > maxAvcLevel) maxAvcLevel = pl.level
}
}
val codec = DeviceCodec.from(capabilities) ?: continue
val name = codec.name
when (codec) {
is DeviceCodec.Video -> {
@@ -59,6 +71,7 @@ class DeviceProfileBuilder(
}
}
}
maxAvcRawLevel = maxAvcLevel
// Build map of supported codecs from device support and hardcoded data
supportedVideoCodecs = Array(AVAILABLE_VIDEO_CODECS.size) { i ->
@@ -214,8 +227,13 @@ class DeviceProfileBuilder(
musicStreamingTranscodingBitrate = Int.MAX_VALUE,
)
fun getWebCodecCapabilitiesJson(): String = JSONObject().apply {
put("h264MaxLevel", CodecHelpers.getVideoLevel("h264", maxAvcRawLevel)?.toString() ?: DEFAULT_H264_MAX_LEVEL)
}.toString()
companion object {
private const val EXTERNAL_PLAYER_PROFILE_NAME = Constants.APP_INFO_NAME + " External Player"
private const val DEFAULT_H264_MAX_LEVEL = "41"
/**
* List of container formats supported by ExoPlayer