mic works and improved theme

This commit is contained in:
srtk 2025-11-30 14:42:37 +05:30
parent 797c5c48d7
commit 55aa3c6510
4 changed files with 1201 additions and 981 deletions

View file

@ -5,13 +5,18 @@ import android.content.Context
import android.hardware.camera2.*
import android.media.MediaCodec
import android.media.MediaCodecInfo
import android.media.MediaCodecList
import android.media.MediaFormat
import android.os.Bundle
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
import android.util.Log
import android.util.Range
import android.util.Size
import android.view.Surface
import androidx.annotation.RequiresApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.net.DatagramPacket
import java.net.DatagramSocket
import java.net.InetAddress
@ -21,8 +26,20 @@ import kotlin.math.abs
import kotlin.math.min
data class VideoConfig(val width: Int, val height: Int, val fps: Int) {
val aspectRatio: String
get() {
val ratio = width.toDouble() / height.toDouble()
return when {
abs(ratio - 1.77) < 0.05 -> "16:9"
abs(ratio - 1.33) < 0.05 -> "4:3"
abs(ratio - 2.0) < 0.1 -> "18:9"
abs(ratio - 2.16) < 0.1 -> "19.5:9"
abs(ratio - 1.0) < 0.05 -> "1:1"
else -> String.format("%.1f:1", ratio)
}
}
override fun toString() = "${height}p"
fun toDetailedString() = "${height}p (${width}×${height} @ ${fps}fps)"
fun toDetailedString() = "${width}×${height} @ ${fps}fps"
}
data class CameraCapabilities(
@ -44,7 +61,6 @@ enum class WhiteBalance(val value: Int, val label: String) {
class CameraStreamer(private val context: Context) {
private val TAG = "CameraStreamer"
private val manager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
private val cameraThread = HandlerThread("CamThread").apply { start() }
private val camHandler = Handler(cameraThread.looper)
@ -54,10 +70,12 @@ class CameraStreamer(private val context: Context) {
private var mediaCodec: MediaCodec? = null
private var socket: DatagramSocket? = null
private var currentIp: InetAddress? = null
private var currentPort: Int = 0
@Volatile private var isStreaming = false
private val restartLock = Semaphore(1)
private var currentConfig = VideoConfig(960, 720, 30)
private var bitrate = 3000000
private var currentConfig = VideoConfig(1280, 720, 30)
private var zoomRatio = 1f
private var flashMode = false
@ -65,24 +83,42 @@ class CameraStreamer(private val context: Context) {
private var focusMode = CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_VIDEO
private var focusDistance = 0f
private var exposureCompensation = 0
private var onErrorCallback: ((String) -> Unit)? = null
private var onDisconnectedCallback: (() -> Unit)? = null
private var connectionManager: ConnectionManager? = null
private var lastPacketTime = 0L
private val connectionTimeout = 5000L // 5 seconds
fun setConnectionManager(manager: ConnectionManager) { connectionManager = manager }
fun setErrorCallback(callback: (String) -> Unit) { onErrorCallback = callback }
fun setErrorCallback(callback: (String) -> Unit) {
onErrorCallback = callback
private fun sendControl(msg: String) {
val ip = currentIp ?: return
val port = currentPort
if (port == 0) return
Thread {
try {
val s = socket ?: DatagramSocket()
val data = msg.toByteArray()
for(i in 1..3) {
s.send(DatagramPacket(data, data.size, ip, port))
Thread.sleep(5)
}
if (socket == null) s.close()
} catch (e: Exception) {}
}.start()
}
fun setDisconnectedCallback(callback: () -> Unit) {
onDisconnectedCallback = callback
}
/**
* Actually test if encoder can handle this resolution by trying to configure it
*/
private fun canEncodeResolution(width: Int, height: Int, fps: Int): Boolean {
var codec: MediaCodec? = null
return try {
val bitrate = when {
width * height >= 1920 * 1080 -> 4_000_000
width * height >= 1280 * 720 -> 2_500_000
else -> 1_500_000
}
// Test if a resolution actually works with the encoder
private fun testEncoderResolution(width: Int, height: Int): Boolean {
var testCodec: MediaCodec? = null
try {
val format = MediaFormat.createVideoFormat(
MediaFormat.MIMETYPE_VIDEO_AVC,
width,
@ -92,123 +128,142 @@ class CameraStreamer(private val context: Context) {
MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
)
setInteger(MediaFormat.KEY_BIT_RATE, 2000000)
setInteger(MediaFormat.KEY_FRAME_RATE, 30)
setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2)
setInteger(MediaFormat.KEY_BIT_RATE, bitrate)
setInteger(MediaFormat.KEY_FRAME_RATE, fps)
setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
}
testCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
testCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
testCodec.release()
Log.d(TAG, "✅ Resolution ${width}x${height} WORKS")
return true
codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
codec.reset() // Don't actually start, just test config
true
} catch (e: Exception) {
Log.d(TAG, "❌ Resolution ${width}x${height} FAILED: ${e.message}")
return false
Log.d(TAG, "${width}x${height} @ ${fps}fps - Encoder rejected: ${e.message}")
false
} finally {
try {
testCodec?.release()
codec?.release()
} catch (e: Exception) {}
}
}
private fun is16by9(width: Int, height: Int): Boolean {
val ratio = width.toDouble() / height.toDouble()
return abs(ratio - 16.0 / 9.0) < 0.1
}
private fun is4by3(width: Int, height: Int): Boolean {
val ratio = width.toDouble() / height.toDouble()
return abs(ratio - 4.0 / 3.0) < 0.1
}
fun getCameraCapabilities(useFront: Boolean): CameraCapabilities {
suspend fun getCameraCapabilities(useFront: Boolean): CameraCapabilities = withContext(Dispatchers.Default) {
val camId = getCameraId(useFront)
val chars = manager.getCameraCharacteristics(camId)
val map = chars.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
val allSizes = map.getOutputSizes(MediaCodec::class.java)
.filter { is16by9(it.width, it.height) || is4by3(it.width, it.height) }
.sortedByDescending { it.width * it.height }
// Get all sizes supported by camera for MediaCodec
val cameraSizes = map.getOutputSizes(MediaCodec::class.java)
.filter { it.width <= 1920 && it.height <= 1080 }
.toSet()
Log.d(TAG, "Testing encoder capabilities...")
Log.d(TAG, "Testing ${cameraSizes.size} camera resolutions against encoder...")
val configs = mutableListOf<VideoConfig>()
val fpsRanges = chars.get(
CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES
) ?: emptyArray()
val validConfigs = mutableListOf<VideoConfig>()
for (size in allSizes) {
try {
// TEST if encoder actually supports this resolution
if (!testEncoderResolution(size.width, size.height)) {
continue
}
// Define common resolutions to prioritize
// val commonResolutions = listOf(
// Size(1920, 1080), // 1080p 16:9
// Size(1280, 720), // 720p 16:9
// Size(960, 720), // 720p 4:3
// Size(640, 480), // 480p 4:3
// Size(854, 480), // 480p 16:9
// Size(1440, 1080), // 1080p 4:3
// Size(800, 600), // 600p 4:3
// Size(320, 240), // 240p 4:3
// )
val minFrameDuration = map.getOutputMinFrameDuration(
MediaCodec::class.java,
size
)
// Test common resolutions first
// for (size in commonResolutions) {
// if (!cameraSizes.contains(size)) continue
//
// val minFrameDuration = map.getOutputMinFrameDuration(MediaCodec::class.java, size)
// val maxFps = if (minFrameDuration > 0) {
// (1_000_000_000.0 / minFrameDuration).toInt()
// } else 30
//
// val targetFps = min(30, maxFps)
//
// if (targetFps >= 15 && canEncodeResolution(size.width, size.height, targetFps)) {
// validConfigs.add(VideoConfig(size.width, size.height, targetFps))
// Log.d(TAG, "✓ ${size.width}x${size.height} @ ${targetFps}fps")
// }
// }
if (minFrameDuration <= 0) continue
// Test remaining camera sizes that aren't in common list
for (size in cameraSizes) {
// if (commonResolutions.contains(size)) continue
if (validConfigs.any { it.width == size.width && it.height == size.height }) continue
val maxFpsFromDuration = (1_000_000_000.0 / minFrameDuration).toInt()
val validRanges = fpsRanges.filter {
it.upper <= maxFpsFromDuration && it.upper >= 15
}
val ratio = size.width.toDouble() / size.height
val targetFps = min(validRanges.maxOfOrNull { it.upper } ?: 30, 30)
// Only accept common aspect ratios
val isCommonAspectRatio = listOf(1.77, 1.33, 2.0, 2.16, 1.0)
.any { abs(ratio - it) < 0.1 }
if (!isCommonAspectRatio) continue
if (targetFps >= 15) {
configs.add(VideoConfig(size.width, size.height, targetFps))
}
} catch (e: Exception) {
Log.w(TAG, "Skipping ${size.width}x${size.height}: ${e.message}")
val minFrameDuration = map.getOutputMinFrameDuration(MediaCodec::class.java, size)
val maxFps = if (minFrameDuration > 0) {
(1_000_000_000.0 / minFrameDuration).toInt()
} else 30
val targetFps = min(30, maxFps)
if (targetFps >= 15 && canEncodeResolution(size.width, size.height, targetFps)) {
validConfigs.add(VideoConfig(size.width, size.height, targetFps))
Log.d(TAG, "${size.width}x${size.height} @ ${targetFps}fps")
}
}
if (configs.isEmpty()) {
configs.add(VideoConfig(640, 480, 30))
Log.w(TAG, "No configs found, added 640x480 fallback")
// Fallback if nothing found (this should never happen on real devices)
if (validConfigs.isEmpty()) {
Log.w(TAG, "No compatible resolutions found! Adding fallback...")
// Try absolute minimum safe resolution
if (canEncodeResolution(640, 480, 30)) {
validConfigs.add(VideoConfig(640, 480, 30))
} else if (canEncodeResolution(320, 240, 30)) {
validConfigs.add(VideoConfig(320, 240, 30))
}
}
val maxZoom = chars.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM) ?: 1f
val hasFlash = chars.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) == true
val hasFront = manager.cameraIdList.any {
manager.getCameraCharacteristics(it).get(CameraCharacteristics.LENS_FACING) ==
CameraCharacteristics.LENS_FACING_FRONT
val sortedConfigs = validConfigs
.sortedByDescending { it.width * it.height }
.distinctBy { "${it.width}x${it.height}" }
Log.d(TAG, "✓ Final supported resolutions: ${sortedConfigs.size} configs")
sortedConfigs.forEach {
Log.d(TAG, " - ${it.toDetailedString()}")
}
val minFocus = chars.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE)
val exposureRange = chars.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE)
return CameraCapabilities(
configs.sortedByDescending { it.width * it.height },
maxZoom,
hasFlash,
hasFront,
CameraCapabilities(
sortedConfigs,
chars.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM) ?: 1f,
chars.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) == true,
manager.cameraIdList.any {
manager.getCameraCharacteristics(it).get(CameraCharacteristics.LENS_FACING) ==
CameraCharacteristics.LENS_FACING_FRONT
},
listOf(WhiteBalance.AUTO, WhiteBalance.WARM, WhiteBalance.COOL),
if (minFocus != null && minFocus > 0) Range(0f, minFocus) else null,
exposureRange
chars.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE)?.let {
if (it > 0) Range(0f, it) else null
},
chars.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE)
)
}
fun start(
ip: InetAddress,
port: Int,
config: VideoConfig,
useFront: Boolean
) {
@RequiresApi(Build.VERSION_CODES.Q)
fun start(ip: InetAddress, port: Int, config: VideoConfig, useFront: Boolean) {
currentConfig = config
currentIp = ip
currentPort = port
val camId = getCameraId(useFront)
camHandler.post {
try {
if (restartLock.tryAcquire(3, TimeUnit.SECONDS)) {
if (restartLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
try {
stopInternal()
Thread.sleep(500)
stopInternal(sendBye = false)
Thread.sleep(100)
startInternal(ip, port, camId)
} finally {
restartLock.release()
@ -216,335 +271,197 @@ class CameraStreamer(private val context: Context) {
}
} catch (e: Exception) {
Log.e(TAG, "Start error", e)
onErrorCallback?.invoke("Failed to start: ${e.message}")
onErrorCallback?.invoke("Start failed: ${e.message}")
}
}
}
fun updateBitrate(mbps: Int) {
val newBitrate = mbps * 1_000_000
if (bitrate == newBitrate) return
bitrate = newBitrate
camHandler.post {
try {
mediaCodec?.let { codec ->
val bundle = Bundle().apply {
putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, bitrate)
}
codec.setParameters(bundle)
Log.d(TAG, "Bitrate updated to ${mbps} Mbps")
}
} catch (e: Exception) {
Log.e(TAG, "Bitrate update failed", e)
}
}
}
fun setZoom(z: Float) {
zoomRatio = z
updateSession()
}
fun setTorch(on: Boolean) {
flashMode = on
updateSession()
}
fun setWhiteBalance(wb: WhiteBalance) {
wbMode = wb.value
updateSession()
}
@RequiresApi(Build.VERSION_CODES.R)
fun setZoom(z: Float) { zoomRatio = z; updateSession() }
@RequiresApi(Build.VERSION_CODES.R)
fun setTorch(on: Boolean) { flashMode = on; updateSession() }
@RequiresApi(Build.VERSION_CODES.R)
fun setWhiteBalance(wb: WhiteBalance) { wbMode = wb.value; updateSession() }
@RequiresApi(Build.VERSION_CODES.R)
fun setFocusMode(auto: Boolean) {
focusMode = if (auto) CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_VIDEO
else CameraMetadata.CONTROL_AF_MODE_OFF
updateSession()
}
@RequiresApi(Build.VERSION_CODES.R)
fun setFocusDistance(distance: Float) {
focusDistance = distance
focusMode = CameraMetadata.CONTROL_AF_MODE_OFF
updateSession()
}
@RequiresApi(Build.VERSION_CODES.R)
fun setExposure(value: Int) { exposureCompensation = value; updateSession() }
fun setExposure(value: Int) {
exposureCompensation = value
updateSession()
fun stopCapture() {
sendControl("PAUSE")
camHandler.post {
try {
captureSession?.stopRepeating()
} catch(e: Exception) {}
}
}
@RequiresApi(Build.VERSION_CODES.Q)
@SuppressLint("MissingPermission")
private fun startInternal(
ip: InetAddress,
port: Int,
camId: String
) {
private fun startInternal(ip: InetAddress, port: Int, camId: String) {
try {
socket = DatagramSocket()
isStreaming = true
lastPacketTime = System.currentTimeMillis()
val width = currentConfig.width
val height = currentConfig.height
val fps = currentConfig.fps
val pixelCount = width * height
val calculatedBitrate = when {
pixelCount >= 1920 * 1080 -> 8_000_000
pixelCount >= 1280 * 720 -> 5_000_000
pixelCount >= 640 * 480 -> 2_500_000
val bitrate = when {
width * height >= 1920 * 1080 -> 4_000_000
width * height >= 1280 * 720 -> 2_500_000
else -> 1_500_000
}
Log.d(TAG, "Starting: ${width}x${height} @ ${currentConfig.fps}fps")
val format = MediaFormat.createVideoFormat(
MediaFormat.MIMETYPE_VIDEO_AVC,
width,
height
).apply {
setInteger(
MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
)
setInteger(MediaFormat.KEY_BIT_RATE, calculatedBitrate)
setInteger(MediaFormat.KEY_FRAME_RATE, currentConfig.fps)
setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2)
val format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height).apply {
setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
setInteger(MediaFormat.KEY_BIT_RATE, bitrate)
setInteger(MediaFormat.KEY_FRAME_RATE, fps)
setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline)
}
mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
mediaCodec?.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
val surface = mediaCodec?.createInputSurface()
?: throw Exception("Failed to create input surface")
try {
mediaCodec?.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
} catch (e: Exception) {
format.removeKey(MediaFormat.KEY_PROFILE)
mediaCodec?.reset()
mediaCodec?.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
}
val surface = mediaCodec?.createInputSurface() ?: throw Exception("Failed to create surface")
mediaCodec?.start()
Log.d(TAG, "✅ Encoder started with ${width}x${height}")
// Start connection monitor
Thread {
while (isStreaming) {
Thread.sleep(1000)
if (System.currentTimeMillis() - lastPacketTime > connectionTimeout) {
Log.w(TAG, "Server disconnected - no data sent")
stop()
onDisconnectedCallback?.invoke()
break
}
}
}.start()
Thread {
Thread.currentThread().priority = Thread.MAX_PRIORITY
streamLoop(ip, port)
}.start()
manager.openCamera(
camId,
object : CameraDevice.StateCallback() {
override fun onOpened(camera: CameraDevice) {
cameraDevice = camera
createSession(camera, surface)
}
manager.openCamera(camId, object : CameraDevice.StateCallback() {
override fun onOpened(camera: CameraDevice) {
cameraDevice = camera
createSession(camera, surface)
}
override fun onDisconnected(camera: CameraDevice) { stopInternal() }
override fun onError(camera: CameraDevice, error: Int) {
onErrorCallback?.invoke("Camera Error: $error")
stopInternal()
}
}, camHandler)
override fun onDisconnected(camera: CameraDevice) {
Log.w(TAG, "Camera disconnected")
stopInternal()
}
override fun onError(camera: CameraDevice, error: Int) {
Log.e(TAG, "Camera error: $error")
stopInternal()
onErrorCallback?.invoke("Camera error: $error")
}
},
camHandler
)
} catch (e: Exception) {
Log.e(TAG, "Start failed: ${e.message}", e)
onErrorCallback?.invoke("Failed to start: ${e.message}")
Log.e(TAG, "Start Failed", e)
onErrorCallback?.invoke("Camera failed: ${e.message}")
stopInternal()
}
}
private fun createSession(camera: CameraDevice, surface: Surface) {
try {
camera.createCaptureSession(
listOf(surface),
object : CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) {
if (!isStreaming) return
captureSession = session
requestBuilder = camera.createCaptureRequest(
CameraDevice.TEMPLATE_RECORD
).apply {
camera.createCaptureSession(listOf(surface), object : CameraCaptureSession.StateCallback() {
@RequiresApi(Build.VERSION_CODES.R)
override fun onConfigured(session: CameraCaptureSession) {
if (!isStreaming) return
captureSession = session
try {
requestBuilder = camera.createCaptureRequest(CameraDevice.TEMPLATE_RECORD).apply {
addTarget(surface)
set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO)
set(CaptureRequest.CONTROL_AE_MODE, CameraMetadata.CONTROL_AE_MODE_ON)
set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,
Range(currentConfig.fps, currentConfig.fps))
set(CaptureRequest.CONTROL_AF_MODE, focusMode)
set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE,
CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_ON)
if (focusMode == CameraMetadata.CONTROL_AF_MODE_OFF) {
set(CaptureRequest.LENS_FOCUS_DISTANCE, focusDistance)
}
set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureCompensation)
set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(currentConfig.fps, currentConfig.fps))
set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_OFF)
}
updateSession()
Log.d(TAG, "Camera session started")
}
override fun onConfigureFailed(session: CameraCaptureSession) {
Log.e(TAG, "Session config failed")
onErrorCallback?.invoke("Camera session failed")
}
},
camHandler
)
} catch (e: Exception) {
Log.e(TAG, "Create session error", e)
}
} catch (e: Exception) {}
}
override fun onConfigureFailed(session: CameraCaptureSession) {}
}, camHandler)
} catch (e: Exception) {}
}
@RequiresApi(Build.VERSION_CODES.R)
private fun updateSession() {
val builder = requestBuilder ?: return
val session = captureSession ?: return
try {
if (zoomRatio > 1f) {
builder.set(CaptureRequest.CONTROL_ZOOM_RATIO, zoomRatio)
}
builder.set(
CaptureRequest.FLASH_MODE,
if (flashMode) CameraMetadata.FLASH_MODE_TORCH
else CameraMetadata.FLASH_MODE_OFF
)
if (zoomRatio > 1f) builder.set(CaptureRequest.CONTROL_ZOOM_RATIO, zoomRatio)
builder.set(CaptureRequest.FLASH_MODE, if (flashMode) CameraMetadata.FLASH_MODE_TORCH else CameraMetadata.FLASH_MODE_OFF)
builder.set(CaptureRequest.CONTROL_AWB_MODE, wbMode)
builder.set(CaptureRequest.CONTROL_AF_MODE, focusMode)
if (focusMode == CameraMetadata.CONTROL_AF_MODE_OFF) {
builder.set(CaptureRequest.LENS_FOCUS_DISTANCE, focusDistance)
}
if (focusMode == CameraMetadata.CONTROL_AF_MODE_OFF) builder.set(CaptureRequest.LENS_FOCUS_DISTANCE, focusDistance)
builder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureCompensation)
session.setRepeatingRequest(builder.build(), null, camHandler)
} catch (e: Exception) {
Log.e(TAG, "Update session error", e)
}
} catch (e: Exception) {}
}
private fun streamLoop(ip: InetAddress, port: Int) {
val bufferInfo = MediaCodec.BufferInfo()
val maxChunkSize = 1400
Log.d(TAG, "Stream loop started")
var lastUpdate = System.currentTimeMillis()
try {
while (isStreaming) {
val codec = mediaCodec ?: break
val index = try { codec.dequeueOutputBuffer(bufferInfo, 10000) } catch (e: Exception) { -1 }
val index = codec.dequeueOutputBuffer(bufferInfo, 10000)
if (index >= 0) {
val buffer = codec.getOutputBuffer(index)
if (buffer != null) {
buffer.position(bufferInfo.offset)
buffer.limit(bufferInfo.offset + bufferInfo.size)
val data = ByteArray(bufferInfo.size)
buffer.get(data)
when (index) {
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
val format = codec.outputFormat
Log.d(TAG, "Output format: $format")
}
var offset = 0
while (offset < data.size && isStreaming) {
val chunkSize = min(maxChunkSize, data.size - offset)
try { socket?.send(DatagramPacket(data, offset, chunkSize, ip, port)) } catch (e: Exception) {}
offset += chunkSize
}
in 0..Int.MAX_VALUE -> {
try {
if (bufferInfo.size <= 0) {
codec.releaseOutputBuffer(index, false)
continue
}
val buffer = codec.getOutputBuffer(index) ?: continue
buffer.position(bufferInfo.offset)
buffer.limit(bufferInfo.offset + bufferInfo.size)
val frameData = ByteArray(bufferInfo.size)
buffer.get(frameData)
var offset = 0
while (offset < frameData.size && isStreaming) {
val chunkSize = min(maxChunkSize, frameData.size - offset)
socket?.send(
DatagramPacket(frameData, offset, chunkSize, ip, port)
)
offset += chunkSize
}
lastPacketTime = System.currentTimeMillis()
codec.releaseOutputBuffer(index, false)
} catch (e: Exception) {
Log.e(TAG, "Buffer processing error", e)
val now = System.currentTimeMillis()
if (now - lastUpdate > 500) {
connectionManager?.updateActivity()
lastUpdate = now
}
}
codec.releaseOutputBuffer(index, false)
}
}
} catch (e: Exception) {
Log.e(TAG, "Stream loop error", e)
}
Log.d(TAG, "Stream loop ended")
} catch (e: Exception) {}
}
fun stop() {
camHandler.post { stopInternal() }
}
fun stop() { camHandler.post { stopInternal(sendBye = true) } }
fun cleanup() { stop(); cameraThread.quitSafely() }
fun cleanup() {
stop()
cameraThread.quitSafely()
}
private fun stopInternal(sendBye: Boolean = true) {
if (sendBye && isStreaming) sendControl("BYE")
private fun stopInternal() {
isStreaming = false
try {
captureSession?.stopRepeating()
captureSession?.close()
captureSession = null
} catch (e: Exception) {
Log.e(TAG, "Session stop error", e)
}
try {
cameraDevice?.close()
cameraDevice = null
} catch (e: Exception) {
Log.e(TAG, "Camera close error", e)
}
try {
mediaCodec?.stop()
mediaCodec?.release()
mediaCodec = null
} catch (e: Exception) {
Log.e(TAG, "Codec stop error", e)
}
try {
socket?.close()
socket = null
} catch (e: Exception) {
Log.e(TAG, "Socket close error", e)
}
requestBuilder = null
try { captureSession?.stopRepeating(); captureSession?.close() } catch (e: Exception) {}
captureSession = null
try { cameraDevice?.close() } catch (e: Exception) {}
cameraDevice = null
try { mediaCodec?.stop(); mediaCodec?.release() } catch (e: Exception) {}
mediaCodec = null
try { socket?.close() } catch (e: Exception) {}
socket = null
}
private fun getCameraId(front: Boolean): String {
val target = if (front)
CameraCharacteristics.LENS_FACING_FRONT
else
CameraCharacteristics.LENS_FACING_BACK
val target = if (front) CameraCharacteristics.LENS_FACING_FRONT else CameraCharacteristics.LENS_FACING_BACK
return manager.cameraIdList.firstOrNull {
manager.getCameraCharacteristics(it)
.get(CameraCharacteristics.LENS_FACING) == target
manager.getCameraCharacteristics(it).get(CameraCharacteristics.LENS_FACING) == target
} ?: manager.cameraIdList[0]
}
}