Negotiate actual Android modes and replace single encoder on IDR
This commit is contained in:
1 parent
fe16509293
commit
2f1de49e87
2 files changed
+59
-22
No files matched your search
@@ -35,8 +35,8 @@ class CallVideoEngine(private val context: Context, private val id: Long) {
|
|||||||
private val manager = context.getSystemService(CameraManager::class.java)
|
private val manager = context.getSystemService(CameraManager::class.java)
|
||||||
private val player = CallVideoPlayer(id) { message -> handler.post { report(message) } }
|
private val player = CallVideoPlayer(id) { message -> handler.post { report(message) } }
|
||||||
@Volatile private var stopped = false
|
@Volatile private var stopped = false
|
||||||
private var encoders = emptyMap<Int, String>()
|
private var encoders = emptyMap<Int, CallVideoCodec.Companion.Encoder>()
|
||||||
private var caps = ByteArray(80)
|
private var caps = ByteArray(64)
|
||||||
private var wanted = CallVideoSettings()
|
private var wanted = CallVideoSettings()
|
||||||
private var active: CallVideoSettings? = null
|
private var active: CallVideoSettings? = null
|
||||||
private var format: CallVideoFormat? = null
|
private var format: CallVideoFormat? = null
|
||||||
@@ -67,6 +67,8 @@ class CallVideoEngine(private val context: Context, private val id: Long) {
|
|||||||
init {
|
init {
|
||||||
handler.post {
|
handler.post {
|
||||||
try {
|
try {
|
||||||
|
val decoders = CallVideoCodec.probeDecoders()
|
||||||
|
player.decoders(decoders)
|
||||||
encoders = CallVideoCodec.probe()
|
encoders = CallVideoCodec.probe()
|
||||||
if (stopped) return@post
|
if (stopped) return@post
|
||||||
val cameras = CameraLensManager.enumerate(manager).mapNotNull { lens ->
|
val cameras = CameraLensManager.enumerate(manager).mapNotNull { lens ->
|
||||||
@@ -76,15 +78,17 @@ class CallVideoEngine(private val context: Context, private val id: Long) {
|
|||||||
if (sizes.isEmpty()) null else CallVideoCamera(lens.cameraId, lens.label, sizes)
|
if (sizes.isEmpty()) null else CallVideoCamera(lens.cameraId, lens.label, sizes)
|
||||||
}
|
}
|
||||||
val bytes = ByteBuffer.wrap(caps).order(ByteOrder.BIG_ENDIAN)
|
val bytes = ByteBuffer.wrap(caps).order(ByteOrder.BIG_ENDIAN)
|
||||||
repeat(2) { for (codec in 1..2) {
|
repeat(2) { direction -> for (codec in 1..2) {
|
||||||
if (encoders.containsKey(codec)) bytes.putShort(1280).putShort(720).putShort(30).putInt(1280 * 720 * 30)
|
val profile = if (direction == 0) encoders[codec]?.profile else decoders[codec]?.profiles
|
||||||
.putInt(4_000_000).putInt(1_000_000).put(if (codec == 1) 31 else 93).put(0)
|
val level = if (direction == 0) encoders[codec]?.level else if (codec == 1) 31 else 93
|
||||||
else bytes.position(bytes.position() + 20)
|
if (profile != null && level != null) bytes.putShort(1280).putShort(720).putShort(30).putInt(1280 * 720 * 30)
|
||||||
|
.putInt(4_000_000).put(level.toByte()).put(profile.toByte())
|
||||||
|
else bytes.position(bytes.position() + 16)
|
||||||
} }
|
} }
|
||||||
check(CallVideoNative.open(id, caps)) { "Video port unavailable" }
|
check(CallVideoNative.open(id, caps)) { "Video port unavailable" }
|
||||||
wanted = wanted.copy(camera = cameras.firstOrNull()?.id.orEmpty())
|
wanted = wanted.copy(camera = cameras.firstOrNull()?.id.orEmpty())
|
||||||
mutable.value = mutable.value.copy(ready = true, cameras = cameras, codecs = encoders.keys.toList(), settings = wanted)
|
mutable.value = mutable.value.copy(ready = true, cameras = cameras, codecs = encoders.keys.toList(), settings = wanted)
|
||||||
report(if (encoders.isEmpty()) "No verified video codec" else "Video ready")
|
report(if (encoders.isEmpty()) "Receive-only video" else "Video ready")
|
||||||
tick()
|
tick()
|
||||||
} catch (e: Exception) { fail("Video setup: ${e.message}") }
|
} catch (e: Exception) { fail("Video setup: ${e.message}") }
|
||||||
}
|
}
|
||||||
@@ -133,15 +137,35 @@ class CallVideoEngine(private val context: Context, private val id: Long) {
|
|||||||
val cameraInfo = manager.getCameraCharacteristics(wanted.camera)
|
val cameraInfo = manager.getCameraCharacteristics(wanted.camera)
|
||||||
val rotation = cameraInfo.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0
|
val rotation = cameraInfo.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0
|
||||||
val desired = CallVideoFormat(++generation, wanted.width, wanted.height, rotation = rotation, codec = wanted.codec)
|
val desired = CallVideoFormat(++generation, wanted.width, wanted.height, rotation = rotation, codec = wanted.codec)
|
||||||
val selected = CallVideoNative.choose(caps, peer, desired.array()) ?: error("No common codec for selected resolution")
|
val previous = encoder; val previousFormat = format; val previousSettings = active
|
||||||
// MediaCodec получает фиксированный 3.1; ряд backend не умеет понижать маркировку level для меньшего размера.
|
val previousName = previous?.name; val previousRate = previous?.bitrate
|
||||||
val next = CallVideoFormat.from(selected).let { it.copy(level = if (it.codec == 1) 31 else 93) }
|
previous?.close(); encoder = null
|
||||||
val candidate = CallVideoCodec(next, 1_000_000, checkNotNull(encoders[next.codec]))
|
for (codec in if (wanted.codec == 0) listOf(2, 1) else listOf(wanted.codec)) {
|
||||||
closeCamera(); encoder?.close(); encoder = candidate; format = next; active = wanted
|
val implementation = encoders[codec] ?: continue
|
||||||
pendingRequest = 0; confirmingRequest = 0; request = 0; lastKey = -1; needKey = true; lastPts = -1
|
val selected = CallVideoNative.choose(caps, peer, desired.copy(codec = codec).array()) ?: continue
|
||||||
check(CallVideoNative.format(id, next.array(), candidate.bitrate)) { "Video format rejected" }
|
var candidate: CallVideoCodec? = null
|
||||||
mutable.value = mutable.value.copy(bitrate = candidate.bitrate)
|
try {
|
||||||
if (!closing) openCamera()
|
candidate = CallVideoCodec(CallVideoFormat.from(selected), 1_000_000, implementation.name)
|
||||||
|
candidate.prepare()
|
||||||
|
val next = candidate.format
|
||||||
|
check(CallVideoNative.supported(caps, peer, next.array())) { "Actual encoder format exceeds peer capabilities: $next" }
|
||||||
|
check(CallVideoNative.format(id, next.array(), candidate.bitrate)) { "Video format rejected" }
|
||||||
|
closeCamera(); encoder = candidate; format = next; active = wanted
|
||||||
|
pendingRequest = 0; confirmingRequest = 0; request = 0; lastKey = -1; needKey = true
|
||||||
|
mutable.value = mutable.value.copy(bitrate = candidate.bitrate)
|
||||||
|
if (!closing) openCamera()
|
||||||
|
return
|
||||||
|
} catch (e: Exception) {
|
||||||
|
candidate?.close()
|
||||||
|
LogManager.addLog("WARN", "call", "video mode rejected codec=$codec name=${implementation.name}: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (previousFormat != null && previousSettings != null && previousName != null && previousRate != null) {
|
||||||
|
encoder = CallVideoCodec(previousFormat, previousRate, previousName).also { it.prepare(); check(it.format == previousFormat) }
|
||||||
|
wanted = previousSettings; needKey = true
|
||||||
|
mutable.value = mutable.value.copy(settings = wanted)
|
||||||
|
report("Selected mode unavailable; previous video restored")
|
||||||
|
} else fail("No compatible encoder for selected video mode")
|
||||||
}
|
}
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
private fun openCamera() {
|
private fun openCamera() {
|
||||||
@@ -152,7 +176,9 @@ class CallVideoEngine(private val context: Context, private val id: Long) {
|
|||||||
sensorOffset = null
|
sensorOffset = null
|
||||||
val nextReader = ImageReader.newInstance(f.width, f.height, ImageFormat.YUV_420_888, 3); reader = nextReader
|
val nextReader = ImageReader.newInstance(f.width, f.height, ImageFormat.YUV_420_888, 3); reader = nextReader
|
||||||
nextReader.setOnImageAvailableListener({ source ->
|
nextReader.setOnImageAvailableListener({ source ->
|
||||||
val image = source.acquireLatestImage() ?: return@setOnImageAvailableListener
|
val image = try { source.acquireLatestImage() } catch (e: IllegalStateException) {
|
||||||
|
LogManager.addLog("WARN", "call", "closed camera reader token=$token/$cameraToken: ${e.message}"); null
|
||||||
|
} ?: return@setOnImageAvailableListener
|
||||||
try {
|
try {
|
||||||
if (stopped || token != cameraToken || !wanted.enabled) return@setOnImageAvailableListener
|
if (stopped || token != cameraToken || !wanted.enabled) return@setOnImageAvailableListener
|
||||||
val now = System.nanoTime()
|
val now = System.nanoTime()
|
||||||
@@ -164,15 +190,21 @@ class CallVideoEngine(private val context: Context, private val id: Long) {
|
|||||||
val key = needKey || lastKey < 0 || pts - lastKey >= 1_000_000
|
val key = needKey || lastKey < 0 || pts - lastKey >= 1_000_000
|
||||||
if (key && pendingRequest != 0L) {
|
if (key && pendingRequest != 0L) {
|
||||||
val requestId = pendingRequest; pendingRequest = 0
|
val requestId = pendingRequest; pendingRequest = 0
|
||||||
|
val old = checkNotNull(encoder); val oldRate = old.bitrate; val name = old.name
|
||||||
|
old.close(); encoder = null
|
||||||
try {
|
try {
|
||||||
val candidate = CallVideoCodec(f, pendingRate, checkNotNull(encoders[f.codec]))
|
encoder = CallVideoCodec(f, pendingRate, name)
|
||||||
encoder?.close(); encoder = candidate; confirmingRequest = requestId; needKey = true
|
encoder!!.prepare(); check(encoder!!.format == f) { "Bitrate change altered encoder format" }
|
||||||
|
confirmingRequest = requestId; needKey = true
|
||||||
report("Applying ${pendingRate / 1000} kbit/s on IDR")
|
report("Applying ${pendingRate / 1000} kbit/s on IDR")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
CallVideoNative.applied(id, f.generation, requestId, 0); throw e
|
LogManager.addLog("WARN", "call", "video bitrate replacement failed: ${e.message}; restoring $oldRate")
|
||||||
|
encoder?.close(); encoder = CallVideoCodec(f, oldRate, name)
|
||||||
|
encoder!!.prepare(); check(encoder!!.format == f)
|
||||||
|
confirmingRequest = requestId; needKey = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (encoder?.submit(image, pts, key) == true) { lastPts = pts; if (key) lastKey = pts }
|
if (encoder?.submit(image, pts, key) == true) lastPts = pts
|
||||||
drain()
|
drain()
|
||||||
} catch (e: Exception) { fail("Camera encode: ${e.message}") }
|
} catch (e: Exception) { fail("Camera encode: ${e.message}") }
|
||||||
finally { image.close() }
|
finally { image.close() }
|
||||||
@@ -215,6 +247,7 @@ class CallVideoEngine(private val context: Context, private val id: Long) {
|
|||||||
val codec = encoder ?: return; val f = format ?: return
|
val codec = encoder ?: return; val f = format ?: return
|
||||||
for (p in codec.drain()) {
|
for (p in codec.drain()) {
|
||||||
encodedBytes += p.bytes.size; encodedFrames++
|
encodedBytes += p.bytes.size; encodedFrames++
|
||||||
|
if (p.key) lastKey = p.pts
|
||||||
if (confirmingRequest != 0L && p.key) {
|
if (confirmingRequest != 0L && p.key) {
|
||||||
CallVideoNative.applied(id, f.generation, confirmingRequest, codec.bitrate); confirmingRequest = 0
|
CallVideoNative.applied(id, f.generation, confirmingRequest, codec.bitrate); confirmingRequest = 0
|
||||||
mutable.value = mutable.value.copy(bitrate = codec.bitrate)
|
mutable.value = mutable.value.copy(bitrate = codec.bitrate)
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import android.view.Surface
|
|||||||
internal class CallVideoPlayer(private val id: Long, private val failure: (String) -> Unit) {
|
internal class CallVideoPlayer(private val id: Long, private val failure: (String) -> Unit) {
|
||||||
@Volatile private var running = true
|
@Volatile private var running = true
|
||||||
@Volatile private var target: Surface? = null
|
@Volatile private var target: Surface? = null
|
||||||
|
@Volatile private var verified = emptyMap<Int, CallVideoCodec.Companion.Decoder>()
|
||||||
private val worker = Thread(::loop, "call-video-rx").apply { start() }
|
private val worker = Thread(::loop, "call-video-rx").apply { start() }
|
||||||
fun surface(surface: Surface?) { target = surface }
|
fun surface(surface: Surface?) { target = surface }
|
||||||
|
fun decoders(value: Map<Int, CallVideoCodec.Companion.Decoder>) { verified = value }
|
||||||
fun stop() { running = false; worker.join() }
|
fun stop() { running = false; worker.join() }
|
||||||
|
|
||||||
private fun loop() {
|
private fun loop() {
|
||||||
@@ -65,7 +67,9 @@ internal class CallVideoPlayer(private val id: Long, private val failure: (Strin
|
|||||||
if (!key) { pending = null; CallVideoNative.requestKey(id); continue }
|
if (!key) { pending = null; CallVideoNative.requestKey(id); continue }
|
||||||
CallVideoCodec.validateKey(data, next)
|
CallVideoCodec.validateKey(data, next)
|
||||||
val type = StreamVideoCodec.fromId(next.codec); val config = type.unpackKey(data).first
|
val type = StreamVideoCodec.fromId(next.codec); val config = type.unpackKey(data).first
|
||||||
decoder = MediaCodec.createDecoderByType(type.mime)
|
val implementation = checkNotNull(verified[next.codec]) { "No verified decoder" }
|
||||||
|
check(implementation.profiles and next.profile != 0) { "Unsupported decoder profile" }
|
||||||
|
decoder = MediaCodec.createByCodecName(implementation.name)
|
||||||
decoder!!.configure(type.decoderFormat(next.width, next.height, next.rotation, config), surface, null, 0)
|
decoder!!.configure(type.decoderFormat(next.width, next.height, next.rotation, config), surface, null, 0)
|
||||||
decoder!!.start(); format = next; lastProgress = now
|
decoder!!.start(); format = next; lastProgress = now
|
||||||
LogManager.addLog("INFO", "call", "video decoder started call=$id format=$next")
|
LogManager.addLog("INFO", "call", "video decoder started call=$id format=$next")
|
||||||
|
|||||||
Reference in new issue
Block a user