重命名为 isClientAlive 以避免与 Thread.isAlive() 冲突 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
152 lines
4.4 KiB
Kotlin
152 lines
4.4 KiB
Kotlin
package com.usbwebcam
|
|
|
|
import java.io.OutputStream
|
|
import java.net.ServerSocket
|
|
import java.net.Socket
|
|
import java.util.concurrent.CopyOnWriteArrayList
|
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
|
|
class MjpegServer(private val port: Int) {
|
|
private var serverSocket: ServerSocket? = null
|
|
private var serverThread: Thread? = null
|
|
private val clients = CopyOnWriteArrayList<ClientHandler>()
|
|
private val running = AtomicBoolean(false)
|
|
private var currentFrame: ByteArray? = null
|
|
private val frameLock = Any()
|
|
|
|
fun start(onStarted: () -> Unit) {
|
|
serverThread = Thread {
|
|
try {
|
|
serverSocket = ServerSocket(port)
|
|
running.set(true)
|
|
onStarted()
|
|
|
|
while (running.get()) {
|
|
val client = try {
|
|
serverSocket?.accept()
|
|
} catch (e: Exception) {
|
|
null
|
|
} ?: continue
|
|
|
|
val handler = ClientHandler(client)
|
|
clients.add(handler)
|
|
handler.start()
|
|
|
|
// 发送当前帧
|
|
val frameToSend = synchronized(frameLock) {
|
|
currentFrame
|
|
}
|
|
if (frameToSend != null) {
|
|
handler.sendFrame(frameToSend)
|
|
}
|
|
}
|
|
} catch (e: Exception) {
|
|
if (running.get()) {
|
|
e.printStackTrace()
|
|
}
|
|
}
|
|
}.apply {
|
|
name = "MjpegServerThread"
|
|
start()
|
|
}
|
|
}
|
|
|
|
fun updateFrame(frame: ByteArray) {
|
|
synchronized(frameLock) {
|
|
currentFrame = frame
|
|
}
|
|
|
|
// 广播给所有客户端
|
|
val iterator = clients.iterator()
|
|
while (iterator.hasNext()) {
|
|
val handler = iterator.next()
|
|
if (handler.isClientAlive()) {
|
|
handler.sendFrame(frame)
|
|
} else {
|
|
iterator.remove()
|
|
}
|
|
}
|
|
}
|
|
|
|
fun stop() {
|
|
if (!running.getAndSet(false)) return
|
|
|
|
try {
|
|
serverSocket?.close()
|
|
} catch (e: Exception) {
|
|
// ignore
|
|
}
|
|
|
|
clients.forEach { it.close() }
|
|
clients.clear()
|
|
serverSocket = null
|
|
serverThread = null
|
|
}
|
|
|
|
private class ClientHandler(private val socket: Socket) : Thread() {
|
|
private var clientStream: OutputStream? = null
|
|
@Volatile
|
|
private var initialized = false
|
|
|
|
override fun run() {
|
|
try {
|
|
val os = socket.getOutputStream()
|
|
clientStream = os
|
|
socket.setSoTimeout(5000)
|
|
|
|
// 读取HTTP请求
|
|
val buffer = ByteArray(1024)
|
|
socket.getInputStream().read(buffer)
|
|
|
|
// 发送HTTP响应头
|
|
val header = byteArrayOf(
|
|
*("HTTP/1.1 200 OK\r\n").toByteArray(),
|
|
*("Content-Type: multipart/x-mixed-replace; boundary=boundary\r\n").toByteArray(),
|
|
*("Cache-Control: no-cache\r\n").toByteArray(),
|
|
*("Connection: close\r\n").toByteArray(),
|
|
*("\r\n").toByteArray()
|
|
)
|
|
|
|
os.write(header)
|
|
os.flush()
|
|
initialized = true
|
|
} catch (e: Exception) {
|
|
close()
|
|
}
|
|
}
|
|
|
|
fun sendFrame(frame: ByteArray) {
|
|
if (!initialized) return
|
|
|
|
try {
|
|
// MJPEG frame header
|
|
val header = byteArrayOf(
|
|
*("--boundary\r\n").toByteArray(),
|
|
*("Content-Type: image/jpeg\r\n").toByteArray(),
|
|
*("Content-Length: ${frame.size}\r\n").toByteArray(),
|
|
*("\r\n").toByteArray()
|
|
)
|
|
|
|
clientStream?.write(header)
|
|
clientStream?.write(frame)
|
|
clientStream?.write("\r\n".toByteArray())
|
|
clientStream?.flush()
|
|
} catch (e: Exception) {
|
|
close()
|
|
}
|
|
}
|
|
|
|
fun close() {
|
|
try {
|
|
socket.close()
|
|
} catch (e: Exception) {
|
|
e.printStackTrace()
|
|
}
|
|
}
|
|
|
|
fun isClientAlive(): Boolean {
|
|
return !socket.isClosed && socket.isConnected
|
|
}
|
|
}
|
|
}
|