Use explicit HEVC Main tier and diagnose fixture rejection
This commit is contained in:
1 parent
bf8dfeec02
commit
902e7bb921
10 files changed
+82
-52
No files matched your search
@@ -195,14 +195,29 @@ internal class CallVideoCodec(requested: CallVideoFormat, bitrate: Int, val name
|
||||
fun platformLevel(codec: Int, level: Int): Int = if (codec == 1) when (level) {
|
||||
30 -> MediaCodecInfo.CodecProfileLevel.AVCLevel3
|
||||
31 -> MediaCodecInfo.CodecProfileLevel.AVCLevel31
|
||||
40 -> MediaCodecInfo.CodecProfileLevel.AVCLevel4
|
||||
else -> error("Unsupported AVC level $level")
|
||||
} else when (level) {
|
||||
90 -> MediaCodecInfo.CodecProfileLevel.HEVCMainTierLevel3
|
||||
93 -> MediaCodecInfo.CodecProfileLevel.HEVCMainTierLevel31
|
||||
120 -> MediaCodecInfo.CodecProfileLevel.HEVCMainTierLevel4
|
||||
else -> error("Unsupported HEVC level $level")
|
||||
}
|
||||
data class Encoder(val name: String, val profile: Int, val level: Int, val sizes: List<Size>)
|
||||
data class Decoder(val name: String, val profiles: Int, val pipeline: Int = 0)
|
||||
data class EncoderMode(val size: Size, val name: String, val profile: Int, val level: Int)
|
||||
data class Encoder(val modes: List<EncoderMode>) {
|
||||
val sizes get() = modes.map { it.size }
|
||||
val profile get() = modes.fold(0) { bits, mode -> bits or mode.profile }
|
||||
val level get() = modes.maxOf { it.level }
|
||||
}
|
||||
data class DecoderMode(val size: Size, val name: String, val profiles: Int, val level: Int, val pipeline: Int) {
|
||||
fun supports(f: CallVideoFormat) = profiles and f.profile != 0 && f.level <= level &&
|
||||
maxOf(f.width, f.height) <= maxOf(size.width, size.height) && f.width.toLong() * f.height <= size.width.toLong() * size.height
|
||||
}
|
||||
data class Decoder(val modes: List<DecoderMode>) {
|
||||
val size get() = modes.maxBy { it.size.width * it.size.height }.size
|
||||
val profiles get() = modes.fold(0) { bits, mode -> bits or mode.profiles }
|
||||
val level get() = modes.maxOf { it.level }
|
||||
}
|
||||
/** Аппаратные реализации проверяются первыми; software остаётся резервом после реальной пробы. */
|
||||
private fun candidates(encoder: Boolean) = MediaCodecList(MediaCodecList.REGULAR_CODECS).codecInfos
|
||||
.filter { it.isEncoder == encoder }.sortedBy {
|
||||
@@ -223,18 +238,19 @@ internal class CallVideoCodec(requested: CallVideoFormat, bitrate: Int, val name
|
||||
fun probe(): Map<Int, Encoder> {
|
||||
val result = mutableMapOf<Int, Encoder>()
|
||||
for (type in listOf(StreamVideoCodec.H265, StreamVideoCodec.H264)) {
|
||||
val modes = mutableListOf<EncoderMode>()
|
||||
for (size in listOf(Size(640, 480), Size(1280, 720), Size(1920, 1080))) {
|
||||
for (entry in candidates(true)) {
|
||||
if (entry.supportedTypes.none { it == type.mime }) continue
|
||||
try {
|
||||
val caps = entry.getCapabilitiesForType(type.mime)
|
||||
if (caps.encoderCapabilities?.isBitrateModeSupported(MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR) != true) continue
|
||||
val sizes = mutableListOf<Size>(); var profile = 0; var level = 0
|
||||
for (size in listOf(Size(640, 480), Size(1280, 720))) {
|
||||
try {
|
||||
var foundProfile = profile; var foundLevel = level
|
||||
var foundProfile = 0; var foundLevel = 0
|
||||
for ((w, h) in listOf(size.width to size.height, size.height to size.width)) {
|
||||
check(caps.videoCapabilities?.areSizeAndRateSupported(w, h, 30.0) == true) { "Geometry unsupported: ${w}x$h" }
|
||||
val f = CallVideoFormat(1, w, h, codec = type.id, level = if (type.id == 1) 31 else 93,
|
||||
val fullhd = size.width > 1280
|
||||
val f = CallVideoFormat(1, w, h, codec = type.id,
|
||||
level = if (type.id == 1) { if (fullhd) 40 else 31 } else { if (fullhd) 120 else 93 },
|
||||
profile = if (type.id == 1) 2 else 1)
|
||||
CallVideoCodec(f, 1_000_000, entry.name).use { encoder ->
|
||||
encoder.prepare()
|
||||
@@ -242,45 +258,46 @@ internal class CallVideoCodec(requested: CallVideoFormat, bitrate: Int, val name
|
||||
foundProfile = encoder.format.profile; foundLevel = maxOf(foundLevel, encoder.format.level)
|
||||
}
|
||||
}
|
||||
profile = foundProfile; level = foundLevel; sizes += size
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("WARN", "call", "video orientation probe rejected ${entry.name} size=$size: ${e.message}")
|
||||
}
|
||||
}
|
||||
if (sizes.isEmpty()) continue
|
||||
result[type.id] = Encoder(entry.name, profile, level, sizes)
|
||||
LogManager.addLog("INFO", "call", "video codec probe passed type=${type.label} encoder=${entry.name} sizes=$sizes"); break
|
||||
} catch (e: Exception) { LogManager.addLog("WARN", "call", "video codec probe rejected ${entry.name}: ${e.message}") }
|
||||
modes += EncoderMode(size, entry.name, foundProfile, foundLevel)
|
||||
LogManager.addLog("INFO", "call", "video codec probe passed type=${type.label} encoder=${entry.name} size=$size"); break
|
||||
} catch (e: Exception) { LogManager.addLog("WARN", "call", "video codec probe rejected ${entry.name} size=$size: ${e.message}") }
|
||||
}
|
||||
}
|
||||
if (modes.isNotEmpty()) result[type.id] = Encoder(modes)
|
||||
}
|
||||
return result
|
||||
}
|
||||
fun probeDecoders(): Map<Int, Decoder> {
|
||||
val result = mutableMapOf<Int, Decoder>()
|
||||
for (type in listOf(StreamVideoCodec.H265, StreamVideoCodec.H264)) {
|
||||
val modes = mutableListOf<DecoderMode>()
|
||||
for (size in listOf(Size(1280, 720), Size(1920, 1080))) {
|
||||
for (entry in candidates(false)) {
|
||||
if (entry.supportedTypes.none { it == type.mime }) continue
|
||||
try {
|
||||
val caps = entry.getCapabilitiesForType(type.mime)
|
||||
if (caps.videoCapabilities?.areSizeAndRateSupported(1280, 720, 30.0) != true) continue
|
||||
if (caps.videoCapabilities?.areSizeAndRateSupported(size.width, size.height, 30.0) != true) continue
|
||||
var profiles = 0; var pipeline = 0
|
||||
val level = if (type.id == 1) { if (size.width > 1280) 40 else 31 } else { if (size.width > 1280) 120 else 93 }
|
||||
for (profile in if (type.id == 1) listOf(1, 2) else listOf(1)) {
|
||||
for (portrait in listOf(false, true)) {
|
||||
val w = if (portrait) 720 else 1280; val h = if (portrait) 1280 else 720
|
||||
val w = if (portrait) size.height else size.width; val h = if (portrait) size.width else size.height
|
||||
check(caps.videoCapabilities?.areSizeAndRateSupported(w, h, 30.0) == true) { "Decoder geometry unsupported: ${w}x$h" }
|
||||
val f = CallVideoFormat(1, w, h, codec = type.id, level = if (type.id == 1) 31 else 93, profile = profile)
|
||||
val key = checkNotNull(CallVideoNative.probeKey(type.id, profile, portrait))
|
||||
val f = CallVideoFormat(1, w, h, codec = type.id, level = level, profile = profile)
|
||||
val key = checkNotNull(CallVideoNative.probeKey(type.id, profile, w, h))
|
||||
validateKey(key, f)
|
||||
pipeline = maxOf(pipeline, verifyDecode(f, List(8) { Packet(1_000_000L + it * 33333, true, key) },
|
||||
entry.name, true))
|
||||
}
|
||||
profiles = profiles or profile
|
||||
}
|
||||
result[type.id] = Decoder(entry.name, profiles, pipeline)
|
||||
LogManager.addLog("INFO", "call", "video decoder verified codec=${type.id} name=${entry.name} profiles=$profiles pipeline=$pipeline")
|
||||
modes += DecoderMode(size, entry.name, profiles, level, pipeline)
|
||||
LogManager.addLog("INFO", "call", "video decoder verified codec=${type.id} name=${entry.name} size=$size profiles=$profiles pipeline=$pipeline")
|
||||
break
|
||||
} catch (e: Exception) { LogManager.addLog("WARN", "call", "video decoder probe rejected ${entry.name}: ${e.message}") }
|
||||
} catch (e: Exception) { LogManager.addLog("WARN", "call", "video decoder probe rejected ${entry.name} size=$size: ${e.message}") }
|
||||
}
|
||||
}
|
||||
if (modes.isNotEmpty()) result[type.id] = Decoder(modes)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -137,8 +137,13 @@ class CallVideoEngine(private val context: Context, private val id: Long) {
|
||||
if (stopped) return@post
|
||||
val cameras = CameraLensManager.enumerate(manager).mapNotNull { lens ->
|
||||
val c = manager.getCameraCharacteristics(lens.cameraId)
|
||||
val sizes = c.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)?.getOutputSizes(ImageFormat.YUV_420_888)
|
||||
.orEmpty().filter { it == Size(640, 480) || it == Size(1280, 720) }
|
||||
val map = c.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
|
||||
val rates = c.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES).orEmpty()
|
||||
val sizes = map?.getOutputSizes(ImageFormat.YUV_420_888).orEmpty().filter {
|
||||
(it == Size(640, 480) || it == Size(1280, 720) || it == Size(1920, 1080)) &&
|
||||
rates.any { range -> range.upper == 30 } &&
|
||||
map!!.getOutputMinFrameDuration(ImageFormat.YUV_420_888, it) <= 33_366_700L
|
||||
}
|
||||
val supported = sizes.filter { size -> encoders.values.any { size in it.sizes } }
|
||||
if (supported.isEmpty()) null else CallVideoCamera(lens.cameraId, lens.label, supported,
|
||||
c.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0,
|
||||
@@ -147,9 +152,9 @@ class CallVideoEngine(private val context: Context, private val id: Long) {
|
||||
val bytes = ByteBuffer.wrap(caps).order(ByteOrder.BIG_ENDIAN)
|
||||
repeat(2) { direction -> for (codec in 1..2) {
|
||||
val profile = if (direction == 0) encoders[codec]?.profile else decoders[codec]?.profiles
|
||||
val level = if (direction == 0) encoders[codec]?.level else if (codec == 1) 31 else 93
|
||||
val level = if (direction == 0) encoders[codec]?.level else decoders[codec]?.level
|
||||
if (profile != null && level != null) {
|
||||
val size = if (direction == 0) encoders.getValue(codec).sizes.maxBy { it.width * it.height } else Size(1280, 720)
|
||||
val size = if (direction == 0) encoders.getValue(codec).sizes.maxBy { it.width * it.height } else decoders.getValue(codec).size
|
||||
bytes.putShort(size.width.toShort()).putShort(size.height.toShort()).putShort(30).putInt(size.width * size.height * 30)
|
||||
.putInt(4_000_000).put(level.toByte()).put(profile.toByte())
|
||||
}
|
||||
@@ -257,8 +262,7 @@ class CallVideoEngine(private val context: Context, private val id: Long) {
|
||||
val height = if (turn % 180 == 0) settings.height else settings.width
|
||||
for (codec in if (settings.codec == 0) listOf(2, 1) else listOf(settings.codec)) {
|
||||
if (codec in excluded) continue
|
||||
val implementation = encoders[codec] ?: continue
|
||||
if (Size(settings.width, settings.height) !in implementation.sizes) continue
|
||||
val implementation = encoders[codec]?.modes?.firstOrNull { it.size == Size(settings.width, settings.height) } ?: continue
|
||||
val desired = CallVideoFormat(++generation, width, height, codec = codec)
|
||||
val selected = CallVideoNative.choose(caps, peer, desired.array()) ?: continue
|
||||
var candidate: CallVideoCodec? = null
|
||||
|
||||
@@ -26,7 +26,7 @@ internal object CallVideoNative {
|
||||
external fun requestKey(id: Long)
|
||||
external fun range(id: Long, minimum: Int, maximum: Int): Boolean
|
||||
external fun inspect(codec: Int, bytes: ByteArray): LongArray?
|
||||
external fun probeKey(codec: Int, profile: Int, portrait: Boolean): ByteArray?
|
||||
external fun probeKey(codec: Int, profile: Int, width: Int, height: Int): ByteArray?
|
||||
external fun supported(local: ByteArray, peer: ByteArray, format: LongArray): Boolean
|
||||
external fun decoded(id: Long, generation: Long, accepted: Boolean)
|
||||
}
|
||||
|
||||
@@ -101,7 +101,8 @@ internal class CallVideoPlayer(private val id: Long, private val failure: (Strin
|
||||
val first = if (awaitingPts < 0) pictures.peekFirst() else null
|
||||
val localQueued = inputs.size + pictures.size + (if (pending != null) 1 else 0) + (if (awaitingPts >= 0) 1 else 0)
|
||||
val decision = CallVideoNative.playoutStep(playout, first?.timing?.pts ?: -1, first?.timing?.received ?: 0,
|
||||
now / 1000, first?.timing?.interval ?: 0, localQueued, 1 + (verified[format?.codec]?.pipeline ?: 0))
|
||||
now / 1000, first?.timing?.interval ?: 0, localQueued,
|
||||
1 + (format?.let { f -> verified[f.codec]?.modes?.firstOrNull { it.supports(f) }?.pipeline } ?: 0))
|
||||
if (decision > 0) wakeUs = now / 1000 + decision
|
||||
else if (first != null) {
|
||||
val render = decision == 0L
|
||||
@@ -140,8 +141,9 @@ internal class CallVideoPlayer(private val id: Long, private val failure: (Strin
|
||||
if (!key) { pending = null; CallVideoNative.requestKey(id); continue }
|
||||
CallVideoCodec.validateKey(data, next)
|
||||
val type = StreamVideoCodec.fromId(next.codec); val config = type.unpackKey(data).first
|
||||
val implementation = checkNotNull(verified[next.codec]) { "No verified decoder" }
|
||||
check(implementation.profiles and next.profile != 0) { "Unsupported decoder profile" }
|
||||
val implementation = checkNotNull(verified[next.codec]?.modes?.firstOrNull { it.supports(next) }) {
|
||||
"No verified decoder for $next"
|
||||
}
|
||||
decoder = MediaCodec.createByCodecName(implementation.name)
|
||||
decoder!!.configure(CallVideoCodec.decoderFormat(next, decoder!!, config), surface, null, 0)
|
||||
decoder!!.start(); format = next; lastProgress = System.nanoTime()
|
||||
|
||||
Reference in new issue
Block a user