app更新
This commit is contained in:
@@ -213,28 +213,30 @@ data class FrameBuffer(
|
||||
* 检查是否所有分片都已接收
|
||||
*/
|
||||
fun isComplete(): Boolean {
|
||||
if (totalChunks <= 0) return false
|
||||
for (i in 0 until totalChunks) {
|
||||
if (chunks[i.toInt()] == null) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
// 使用计数器而不是遍历,O(1) 复杂度
|
||||
return chunks.size == totalChunks.toInt()
|
||||
}
|
||||
|
||||
/**
|
||||
* 将所有分片合并为完整的 JPEG 数据
|
||||
*/
|
||||
fun merge(): ByteArray {
|
||||
val result = mutableListOf<Byte>()
|
||||
var totalLen = 0
|
||||
for (i in 0 until totalChunks) {
|
||||
chunks[i.toInt()]?.let {
|
||||
for (byte in it) {
|
||||
result.add(byte)
|
||||
}
|
||||
val chunkLen = chunks[i.toInt()]?.size ?: 0
|
||||
totalLen += chunkLen
|
||||
}
|
||||
|
||||
val result = ByteArray(totalLen)
|
||||
var offset = 0
|
||||
for (i in 0 until totalChunks) {
|
||||
val chunk = chunks[i.toInt()]
|
||||
if (chunk != null) {
|
||||
System.arraycopy(chunk, 0, result, offset, chunk.size)
|
||||
offset += chunk.size
|
||||
}
|
||||
}
|
||||
return result.toByteArray()
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -45,6 +45,9 @@ class UdpReceiver(
|
||||
private var receiverScope: CoroutineScope? = null
|
||||
private var isRunning = false
|
||||
|
||||
// UDP 接收缓冲区大小 (2MB) - 增加缓冲区可以有效防止 UDP 丢包
|
||||
private val SO_RCVBUF_SIZE = 2 * 1024 * 1024
|
||||
|
||||
// ========================================================================
|
||||
// 帧缓冲区管理
|
||||
// ========================================================================
|
||||
@@ -79,6 +82,12 @@ class UdpReceiver(
|
||||
try {
|
||||
socket = DatagramSocket(listenPort)
|
||||
socket!!.soTimeout = 5000
|
||||
|
||||
// 关键优化:增加 Socket 接收缓冲区
|
||||
if (socket!!.receiveBufferSize < SO_RCVBUF_SIZE) {
|
||||
socket!!.receiveBufferSize = SO_RCVBUF_SIZE
|
||||
Log.i(TAG, "UDP 接收缓冲区大小已设置为: ${socket!!.receiveBufferSize}")
|
||||
}
|
||||
|
||||
Log.i(TAG, "✓ UDP 接收器启动,监听端口 $listenPort")
|
||||
|
||||
@@ -106,19 +115,22 @@ class UdpReceiver(
|
||||
*/
|
||||
private suspend fun startReceiving() {
|
||||
val buffer = ByteArray(MAX_UDP_PACKET_SIZE)
|
||||
val packet = DatagramPacket(buffer, buffer.size)
|
||||
|
||||
while (isRunning) {
|
||||
try {
|
||||
val packet = DatagramPacket(buffer, buffer.size)
|
||||
// 重置 packet 长度,以便接收下一个包
|
||||
packet.length = buffer.size
|
||||
socket?.receive(packet)
|
||||
|
||||
val data = packet.data.slice(0 until packet.length).toByteArray()
|
||||
processPacket(data)
|
||||
// 直接处理,避免创建新的 ByteArray 副本
|
||||
processPacket(buffer, packet.length)
|
||||
|
||||
// 定期清理和报告统计
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (currentTime - lastStatsTime >= 1000) {
|
||||
reportStats()
|
||||
cleanupOldFrames() // 移到定时任务中执行,避免每包检查
|
||||
lastStatsTime = currentTime
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -132,30 +144,36 @@ class UdpReceiver(
|
||||
/**
|
||||
* 处理接收到的 UDP 数据包
|
||||
*/
|
||||
private fun processPacket(data: ByteArray) {
|
||||
if (data.size < PACKET_HEADER_SIZE) {
|
||||
private fun processPacket(buffer: ByteArray, length: Int) {
|
||||
if (length < PACKET_HEADER_SIZE) {
|
||||
return
|
||||
}
|
||||
|
||||
// 解析包头
|
||||
val header = SkyLinkPacketHeader.fromByteArray(data) ?: return
|
||||
// 注意:这里传入的是复用的 buffer,fromByteArray 只读取前 50 字节
|
||||
val header = SkyLinkPacketHeader.fromByteArray(buffer) ?: return
|
||||
|
||||
// 验证
|
||||
if (!header.isValid()) {
|
||||
return
|
||||
}
|
||||
|
||||
// 计算载荷大小
|
||||
val payloadSize = length - PACKET_HEADER_SIZE
|
||||
// 简单校验:实际载荷大小应与包头声明一致(或至少不小于)
|
||||
if (payloadSize != header.dataLen.toInt()) {
|
||||
return
|
||||
}
|
||||
|
||||
// 提取载荷
|
||||
val payload = if (data.size > PACKET_HEADER_SIZE) {
|
||||
data.slice(PACKET_HEADER_SIZE until data.size).toByteArray()
|
||||
} else {
|
||||
ByteArray(0)
|
||||
}
|
||||
// 必须拷贝,因为 buffer 会在下一次接收时被覆盖
|
||||
val payload = ByteArray(payloadSize)
|
||||
System.arraycopy(buffer, PACKET_HEADER_SIZE, payload, 0, payloadSize)
|
||||
|
||||
// 更新统计
|
||||
totalPackets++
|
||||
totalBytes += data.size
|
||||
bytesThisSecond += data.size
|
||||
totalBytes += length
|
||||
bytesThisSecond += length
|
||||
packetsThisSecond++
|
||||
|
||||
// 处理分片
|
||||
@@ -179,8 +197,14 @@ class UdpReceiver(
|
||||
handleCompleteFrame(buffer)
|
||||
frameBuffers.remove(header.frameId.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清理超时的帧
|
||||
/**
|
||||
* 清理超时的帧
|
||||
*/
|
||||
private fun cleanupOldFrames() {
|
||||
synchronized(frameBuffers) {
|
||||
frameBuffers.entries.removeIf { (_, fb) ->
|
||||
if (fb.isTimeout()) {
|
||||
droppedFrames++
|
||||
@@ -231,7 +255,7 @@ class UdpReceiver(
|
||||
0.0
|
||||
}
|
||||
|
||||
Log.d(TAG, "📊 FPS: $fps, KB/s: $kbps, 总帧: $totalFrames, 丢弃: $droppedFrames")
|
||||
Log.d(TAG, "FPS: $fps, KB/s: $kbps, 总帧: $totalFrames, 丢弃: $droppedFrames")
|
||||
|
||||
MainScope().launch {
|
||||
listener.onStatsUpdated(fps, kbps, packetLoss)
|
||||
|
||||
@@ -20,11 +20,15 @@ import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.skylink.app.R
|
||||
import com.skylink.app.network.DecodedFrame
|
||||
import com.skylink.app.network.UdpReceiver
|
||||
import com.skylink.app.network.UdpReceiverListener
|
||||
import com.skylink.app.utils.BitmapUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@@ -123,13 +127,17 @@ class HomeFragment : Fragment(), UdpReceiverListener {
|
||||
override fun onFrameDecoded(frame: DecodedFrame) {
|
||||
currentFrame = frame
|
||||
|
||||
// 解码图像
|
||||
val bitmap = BitmapUtils.decodeBitmap(frame.imageData)
|
||||
if (bitmap != null) {
|
||||
view?.let {
|
||||
videoImageView?.setImageBitmap(bitmap)
|
||||
updateGpsInfo(frame)
|
||||
}
|
||||
// 移到后台线程执行解码,避免阻塞 UI 线程,进而影响 UDP 接收协程(如果是单核或高负载)
|
||||
lifecycleScope.launch(Dispatchers.Default) {
|
||||
val bitmap = BitmapUtils.decodeBitmap(frame.imageData)
|
||||
if (bitmap != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
view?.let {
|
||||
videoImageView?.setImageBitmap(bitmap)
|
||||
updateGpsInfo(frame)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ object BitmapUtils {
|
||||
|
||||
// 实际解码
|
||||
val bitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.size, options)
|
||||
Log.i(TAG, "✓ Bitmap 解码成功: ${bitmap?.width}x${bitmap?.height}")
|
||||
// Log.i(TAG, "✓ Bitmap 解码成功: ${bitmap?.width}x${bitmap?.height}") // 移除高频日志
|
||||
bitmap
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Bitmap 解码失败", e)
|
||||
|
||||
Reference in New Issue
Block a user