|
|
|
@ -3,9 +3,7 @@ package com.utun.chat.data |
|
|
|
import android.annotation.SuppressLint |
|
|
|
import android.annotation.SuppressLint |
|
|
|
import android.content.Context |
|
|
|
import android.content.Context |
|
|
|
import android.graphics.ImageFormat |
|
|
|
import android.graphics.ImageFormat |
|
|
|
import android.graphics.Bitmap |
|
|
|
import android.graphics.SurfaceTexture |
|
|
|
import android.graphics.Matrix |
|
|
|
|
|
|
|
import android.media.Image |
|
|
|
|
|
|
|
import android.hardware.camera2.CameraCaptureSession |
|
|
|
import android.hardware.camera2.CameraCaptureSession |
|
|
|
import android.hardware.camera2.CameraCharacteristics |
|
|
|
import android.hardware.camera2.CameraCharacteristics |
|
|
|
import android.hardware.camera2.CameraDevice |
|
|
|
import android.hardware.camera2.CameraDevice |
|
|
|
@ -38,9 +36,10 @@ 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) } } |
|
|
|
internal val display: StateFlow<CallVideoFormat?> = player.display |
|
|
|
internal val display: StateFlow<CallVideoFormat?> = player.display |
|
|
|
private val previewFrames = MutableStateFlow<Bitmap?>(null) |
|
|
|
val preview = StreamPreview() |
|
|
|
val preview: StateFlow<Bitmap?> = previewFrames |
|
|
|
private var previewTarget: StreamPreviewTarget? = null |
|
|
|
private var previewAt = 0L |
|
|
|
private var sessionSerial = 0L |
|
|
|
|
|
|
|
private val sessions = mutableMapOf<Long, Pair<CameraDevice, () -> Unit>>() |
|
|
|
@Volatile private var stopped = false |
|
|
|
@Volatile private var stopped = false |
|
|
|
private var encoders = emptyMap<Int, CallVideoCodec.Companion.Encoder>() |
|
|
|
private var encoders = emptyMap<Int, CallVideoCodec.Companion.Encoder>() |
|
|
|
private var caps = ByteArray(64) |
|
|
|
private var caps = ByteArray(64) |
|
|
|
@ -76,6 +75,15 @@ class CallVideoEngine(private val context: Context, private val id: Long) { |
|
|
|
private var excluded = emptySet<Int>() |
|
|
|
private var excluded = emptySet<Int>() |
|
|
|
|
|
|
|
|
|
|
|
init { |
|
|
|
init { |
|
|
|
|
|
|
|
preview.bind { target -> |
|
|
|
|
|
|
|
if (!handler.post { |
|
|
|
|
|
|
|
if (stopped) { target?.release(); return@post } |
|
|
|
|
|
|
|
if (target === previewTarget) { target?.release(); return@post } |
|
|
|
|
|
|
|
val previous = previewTarget; previewTarget = target; previous?.release() |
|
|
|
|
|
|
|
val device = camera; val source = reader |
|
|
|
|
|
|
|
if (device != null && source != null && !closing) createSession(device, source, cameraToken) |
|
|
|
|
|
|
|
}) { target?.release(); LogManager.addLog("WARN", "call", "preview command after worker stop id=$id") } |
|
|
|
|
|
|
|
} |
|
|
|
handler.post { |
|
|
|
handler.post { |
|
|
|
try { |
|
|
|
try { |
|
|
|
val decoders = CallVideoCodec.probeDecoders() |
|
|
|
val decoders = CallVideoCodec.probeDecoders() |
|
|
|
@ -204,7 +212,6 @@ class CallVideoEngine(private val context: Context, private val id: Long) { |
|
|
|
if (stopped || !wanted.enabled || opening || closing) return |
|
|
|
if (stopped || !wanted.enabled || opening || closing) return |
|
|
|
val settings = active ?: return; val f = format ?: return; val token = ++cameraToken |
|
|
|
val settings = active ?: return; val f = format ?: return; val token = ++cameraToken |
|
|
|
val c = manager.getCameraCharacteristics(settings.camera) |
|
|
|
val c = manager.getCameraCharacteristics(settings.camera) |
|
|
|
val mirror = c.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT |
|
|
|
|
|
|
|
realtime = c.get(CameraCharacteristics.SENSOR_INFO_TIMESTAMP_SOURCE) == CameraCharacteristics.SENSOR_INFO_TIMESTAMP_SOURCE_REALTIME |
|
|
|
realtime = c.get(CameraCharacteristics.SENSOR_INFO_TIMESTAMP_SOURCE) == CameraCharacteristics.SENSOR_INFO_TIMESTAMP_SOURCE_REALTIME |
|
|
|
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 |
|
|
|
@ -216,16 +223,6 @@ class CallVideoEngine(private val context: Context, private val id: Long) { |
|
|
|
if (stopped || token != cameraToken || !wanted.enabled) return@setOnImageAvailableListener |
|
|
|
if (stopped || token != cameraToken || !wanted.enabled) return@setOnImageAvailableListener |
|
|
|
val now = System.nanoTime() |
|
|
|
val now = System.nanoTime() |
|
|
|
lastCaptureAt = now |
|
|
|
lastCaptureAt = now |
|
|
|
if (previewFrames.subscriptionCount.value > 0 && now - previewAt >= 200_000_000) { |
|
|
|
|
|
|
|
previewAt = now |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
val preview = cameraPreview(image, f.rotation, mirror) |
|
|
|
|
|
|
|
if (previewFrames.value == null) LogManager.addLog("INFO", "call", |
|
|
|
|
|
|
|
"camera preview visible id=$id size=${preview.width}x${preview.height} rotation=${f.rotation} mirror=$mirror") |
|
|
|
|
|
|
|
previewFrames.value = preview |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
catch (e: Exception) { LogManager.addLog("WARN", "call", "camera preview failed: ${e.message}") } |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
val offset = if (realtime) now - SystemClock.elapsedRealtimeNanos() else sensorOffset ?: (now - image.timestamp).also { |
|
|
|
val offset = if (realtime) now - SystemClock.elapsedRealtimeNanos() else sensorOffset ?: (now - image.timestamp).also { |
|
|
|
sensorOffset = it; LogManager.addLog("WARN", "call", "camera timestamp calibrated from arrival id=$id offset=$it") |
|
|
|
sensorOffset = it; LogManager.addLog("WARN", "call", "camera timestamp calibrated from arrival id=$id offset=$it") |
|
|
|
} |
|
|
|
} |
|
|
|
@ -265,28 +262,12 @@ class CallVideoEngine(private val context: Context, private val id: Long) { |
|
|
|
override fun onOpened(device: CameraDevice) { |
|
|
|
override fun onOpened(device: CameraDevice) { |
|
|
|
opening = false; camera = device |
|
|
|
opening = false; camera = device |
|
|
|
if (stopped || token != cameraToken) { device.close(); return } |
|
|
|
if (stopped || token != cameraToken) { device.close(); return } |
|
|
|
try { |
|
|
|
createSession(device, nextReader, token) |
|
|
|
device.createCaptureSession(listOf(nextReader.surface), object : CameraCaptureSession.StateCallback() { |
|
|
|
|
|
|
|
override fun onConfigured(s: CameraCaptureSession) { |
|
|
|
|
|
|
|
if (stopped || token != cameraToken) { s.close(); return } |
|
|
|
|
|
|
|
session = s |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
val rates = c.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES).orEmpty() |
|
|
|
|
|
|
|
val range = rates.filter { it.upper == 30 }.maxByOrNull { it.lower } ?: error("Camera lacks 30 fps") |
|
|
|
|
|
|
|
val capture = device.createCaptureRequest(CameraDevice.TEMPLATE_RECORD).apply { |
|
|
|
|
|
|
|
addTarget(nextReader.surface); set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, range) |
|
|
|
|
|
|
|
}.build() |
|
|
|
|
|
|
|
s.setRepeatingRequest(capture, null, handler) |
|
|
|
|
|
|
|
report("${if (f.codec == 2) "H.265" else "H.264"} · ${f.width}×${f.height}") |
|
|
|
|
|
|
|
} catch (e: Exception) { fail("Camera request: ${e.message}") } |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
override fun onConfigureFailed(s: CameraCaptureSession) { s.close(); fail("Camera session configuration failed") } |
|
|
|
|
|
|
|
}, handler) |
|
|
|
|
|
|
|
} catch (e: Exception) { fail("Camera session: ${e.message}") } |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
override fun onDisconnected(device: CameraDevice) { opening = false; camera = device; fail("Camera disconnected") } |
|
|
|
override fun onDisconnected(device: CameraDevice) { opening = false; camera = device; fail("Camera disconnected") } |
|
|
|
override fun onError(device: CameraDevice, error: Int) { opening = false; camera = device; fail("Camera error $error") } |
|
|
|
override fun onError(device: CameraDevice, error: Int) { opening = false; camera = device; fail("Camera error $error") } |
|
|
|
override fun onClosed(device: CameraDevice) { |
|
|
|
override fun onClosed(device: CameraDevice) { |
|
|
|
|
|
|
|
sessions.values.filter { it.first === device }.map { it.second }.forEach { it() } |
|
|
|
if (camera === device) camera = null |
|
|
|
if (camera === device) camera = null |
|
|
|
closing = false |
|
|
|
closing = false |
|
|
|
if (stopped) thread.quitSafely() |
|
|
|
if (stopped) thread.quitSafely() |
|
|
|
@ -294,6 +275,62 @@ class CallVideoEngine(private val context: Context, private val id: Long) { |
|
|
|
} |
|
|
|
} |
|
|
|
}, handler) |
|
|
|
}, handler) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Прямое превью использует второй output той же камеры; encoder и его PTS не меняются. */ |
|
|
|
|
|
|
|
private fun createSession(device: CameraDevice, source: ImageReader, token: Long, withPreview: Boolean = true) { |
|
|
|
|
|
|
|
val f = format ?: return |
|
|
|
|
|
|
|
val serial = ++sessionSerial |
|
|
|
|
|
|
|
val output = if (withPreview) previewTarget?.retain() else null |
|
|
|
|
|
|
|
var completed = false |
|
|
|
|
|
|
|
fun complete() { |
|
|
|
|
|
|
|
if (completed) return |
|
|
|
|
|
|
|
completed = true; sessions.remove(serial); output?.release() |
|
|
|
|
|
|
|
LogManager.addLog("DEBUG", "call", "camera session released id=$id session=$serial remaining=${sessions.size}") |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
fun valid() = !stopped && token == cameraToken && serial == sessionSerial |
|
|
|
|
|
|
|
fun rejected(message: String) { |
|
|
|
|
|
|
|
if (!valid()) { LogManager.addLog("DEBUG", "call", "stale camera session id=$id session=$serial: $message"); return } |
|
|
|
|
|
|
|
if (output == null) fail(message) else { |
|
|
|
|
|
|
|
LogManager.addLog("ERROR", "call", "direct preview failed id=$id: $message; retaining transmission") |
|
|
|
|
|
|
|
preview.update(preview.state.value.format, failed = true) |
|
|
|
|
|
|
|
createSession(device, source, token, withPreview = false) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
sessions[serial] = device to ::complete |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
val c = manager.getCameraCharacteristics(device.id) |
|
|
|
|
|
|
|
var previewFormat: StreamPreviewFormat? = null |
|
|
|
|
|
|
|
if (output != null) { |
|
|
|
|
|
|
|
val sizes = c.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)?.getOutputSizes(SurfaceTexture::class.java).orEmpty() |
|
|
|
|
|
|
|
val size = sizes.filter { it.width <= f.width && it.height <= f.height && it.width * f.height == it.height * f.width } |
|
|
|
|
|
|
|
.maxByOrNull { it.width * it.height } ?: error("No compatible preview size") |
|
|
|
|
|
|
|
output.texture.setDefaultBufferSize(size.width, size.height) |
|
|
|
|
|
|
|
previewFormat = StreamPreviewFormat(size.width, size.height, |
|
|
|
|
|
|
|
StreamPreviewCamera(f.rotation, c.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT), realtime) |
|
|
|
|
|
|
|
preview.update(previewFormat) |
|
|
|
|
|
|
|
} else if (withPreview) preview.update(null) |
|
|
|
|
|
|
|
val targets = listOfNotNull(source.surface, output?.surface) |
|
|
|
|
|
|
|
device.createCaptureSession(targets, object : CameraCaptureSession.StateCallback() { |
|
|
|
|
|
|
|
override fun onConfigured(s: CameraCaptureSession) { |
|
|
|
|
|
|
|
if (!valid()) { s.close(); return } |
|
|
|
|
|
|
|
session = s |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
val range = c.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES).orEmpty() |
|
|
|
|
|
|
|
.filter { it.upper == 30 }.maxByOrNull { it.lower } ?: error("Camera lacks 30 fps") |
|
|
|
|
|
|
|
val request = device.createCaptureRequest(CameraDevice.TEMPLATE_RECORD).apply { |
|
|
|
|
|
|
|
targets.forEach(::addTarget); set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, range) |
|
|
|
|
|
|
|
}.build() |
|
|
|
|
|
|
|
s.setRepeatingRequest(request, null, handler) |
|
|
|
|
|
|
|
if (output != null) preview.update(previewFormat, active = true) |
|
|
|
|
|
|
|
LogManager.addLog("INFO", "call", "camera capture id=$id session=$serial range=$range preview=$previewFormat") |
|
|
|
|
|
|
|
report("${if (f.codec == 2) "H.265" else "H.264"} · ${f.width}×${f.height}") |
|
|
|
|
|
|
|
} catch (e: Exception) { s.close(); rejected("Camera request: ${e.message}") } |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
override fun onConfigureFailed(s: CameraCaptureSession) { s.close(); complete(); rejected("Camera session configuration failed") } |
|
|
|
|
|
|
|
override fun onClosed(s: CameraCaptureSession) { if (session === s) session = null; complete() } |
|
|
|
|
|
|
|
}, handler) |
|
|
|
|
|
|
|
} catch (e: Exception) { complete(); rejected("Camera session: ${e.message}") } |
|
|
|
|
|
|
|
} |
|
|
|
private fun drain() { |
|
|
|
private fun drain() { |
|
|
|
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()) { |
|
|
|
@ -316,42 +353,19 @@ class CallVideoEngine(private val context: Context, private val id: Long) { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
private fun closeCamera() { |
|
|
|
private fun closeCamera() { |
|
|
|
cameraToken++ |
|
|
|
cameraToken++; sessionSerial++ |
|
|
|
previewFrames.value = null; previewAt = 0 |
|
|
|
preview.update(null) |
|
|
|
try { session?.close(); session = null; reader?.close(); reader = null |
|
|
|
try { session?.close(); session = null; reader?.close(); reader = null |
|
|
|
if (camera != null) { closing = true; camera!!.close() } |
|
|
|
if (camera != null) { closing = true; camera!!.close() } |
|
|
|
} catch (e: Exception) { LogManager.addLog("WARN", "call", "camera close: ${e.message}") } |
|
|
|
} catch (e: Exception) { LogManager.addLog("WARN", "call", "camera close: ${e.message}") } |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** Миниатюра из уже захваченного YUV: второй output Camera2 и отдельная камера не нужны. */ |
|
|
|
|
|
|
|
private fun cameraPreview(image: Image, rotation: Int, mirror: Boolean): Bitmap { |
|
|
|
|
|
|
|
val crop = image.cropRect; val width = 192; val height = (width * crop.height() / crop.width()).coerceAtLeast(1) |
|
|
|
|
|
|
|
val pixels = IntArray(width * height) |
|
|
|
|
|
|
|
val planes = image.planes; val buffers = planes.map { it.buffer.duplicate() } |
|
|
|
|
|
|
|
fun sample(plane: Int, x: Int, y: Int): Int { |
|
|
|
|
|
|
|
val p = planes[plane]; val b = buffers[plane] |
|
|
|
|
|
|
|
return b.get(b.position() + y * p.rowStride + x * p.pixelStride).toInt() and 255 |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
for (y in 0 until height) for (x in 0 until width) { |
|
|
|
|
|
|
|
val sx = crop.left + x * crop.width() / width; val sy = crop.top + y * crop.height() / height |
|
|
|
|
|
|
|
val luma = (sample(0, sx, sy) - 16).coerceAtLeast(0) |
|
|
|
|
|
|
|
val u = sample(1, sx / 2, sy / 2) - 128; val v = sample(2, sx / 2, sy / 2) - 128 |
|
|
|
|
|
|
|
val r = ((298 * luma + 409 * v + 128) shr 8).coerceIn(0, 255) |
|
|
|
|
|
|
|
val g = ((298 * luma - 100 * u - 208 * v + 128) shr 8).coerceIn(0, 255) |
|
|
|
|
|
|
|
val b = ((298 * luma + 516 * u + 128) shr 8).coerceIn(0, 255) |
|
|
|
|
|
|
|
pixels[y * width + x] = (255 shl 24) or (r shl 16) or (g shl 8) or b |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
val bitmap = Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888) |
|
|
|
|
|
|
|
val transform = Matrix().apply { postRotate(rotation.toFloat()); if (mirror) postScale(-1f, 1f) } |
|
|
|
|
|
|
|
val result = Bitmap.createBitmap(bitmap, 0, 0, width, height, transform, true) |
|
|
|
|
|
|
|
if (result !== bitmap) bitmap.recycle() |
|
|
|
|
|
|
|
return result |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
fun stop() { |
|
|
|
fun stop() { |
|
|
|
if (stopped) return |
|
|
|
if (stopped) return |
|
|
|
stopped = true; player.stop(); CallVideoNative.close(id) |
|
|
|
stopped = true; player.stop(); CallVideoNative.close(id) |
|
|
|
handler.post { |
|
|
|
handler.post { |
|
|
|
closeCamera(); encoder?.close(); encoder = null |
|
|
|
closeCamera(); encoder?.close(); encoder = null |
|
|
|
|
|
|
|
preview.close(); previewTarget?.release(); previewTarget = null |
|
|
|
if (camera == null && !opening) thread.quitSafely() |
|
|
|
if (camera == null && !opening) thread.quitSafely() |
|
|
|
else handler.postDelayed({ thread.quitSafely() }, 3000) |
|
|
|
else handler.postDelayed({ thread.quitSafely() }, 3000) |
|
|
|
LogManager.addLog("INFO", "call", "video workers stopped id=$id") |
|
|
|
LogManager.addLog("INFO", "call", "video workers stopped id=$id") |
|
|
|
|