mirror of
https://github.com/jellyfin/jellyfin-android.git
synced 2026-09-02 21:04:10 +03:00
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:
committed by
Niels van Velzen
co-authored by
Claude Sonnet 4.6
parent
c3f3a80581
commit
cf4d754890
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user