6个文件已修改
475 ■■■■ 已修改文件
app/src/main/java/com/anyun/h264/H264EncodeService.java 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/H264Encoder.java 167 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/H264FileTransmitter.java 187 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/JT1076ProtocolHelper.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/JT1076TcpClient.java 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/MainActivity.kt 91 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/H264EncodeService.java
@@ -250,7 +250,7 @@
            int framerate = config != null ? config.framerate : DEFAULT_FRAME_RATE;
            h264Encoder.setEncoderParams(width, height, framerate, DEFAULT_BITRATE);
            long timeFile = System.currentTimeMillis();
            long timeFile = System.currentTimeMillis()/1000*1000;//Date是秒,所以为了跟下发的Date starttime一致,此处除以1000 秒
            SimpleDateFormat bcdFormat = new SimpleDateFormat("yyMMddHHmmss");
            String str = bcdFormat.format(timeFile);
            Timber.i("文件名:%s", str);
@@ -317,9 +317,17 @@
            int height =  DEFAULT_HEIGHT;
            int framerate = DEFAULT_FRAME_RATE;
            h264Encoder.setEncoderParams(width, height, framerate, DEFAULT_BITRATE);
            // 禁用文件输出
            h264Encoder.setEnableFileOutput(false);
            long timeFile = System.currentTimeMillis()/1000*1000;
            SimpleDateFormat bcdFormat = new SimpleDateFormat("yyMMddHHmmss");
            String str = bcdFormat.format(timeFile);
            Timber.i("startNetworkEncode 文件名:%s", str);
            // 设置输出文件
            String fileName = "h264_" + timeFile+ ".h264";
            File outputFile = new File(outputFileDirectory, fileName);
            h264Encoder.setOutputFile(outputFile.getAbsolutePath());
            h264Encoder.setEnableFileOutput(true); // 启用文件输出
            
            // 启用网络传输并设置服务器地址
            h264Encoder.setEnableNetworkTransmission(true);
app/src/main/java/com/anyun/h264/H264Encoder.java
@@ -64,9 +64,9 @@
    // 编码参数
    private int width = 640;
    private int height = 480;
    private int frameRate = 25;
    private int frameRate = 15;
    private int bitrate = 2000000; // 2Mbps
    private int iFrameInterval = 1; // I帧间隔(秒)
    private int iFrameInterval = 2; // I帧间隔(秒)
    // JT/T 1076-2016 协议工具类
    private JT1076ProtocolHelper protocolHelper;
@@ -81,6 +81,10 @@
    // 网络传输控制
    private boolean enableNetworkTransmission = true; // 是否启用TCP/UDP网络传输
    // SPS/PPS 缓存(用于网络传输)
    private byte[] spsBuffer = null; // SPS 缓存
    private byte[] ppsBuffer = null; // PPS 缓存
    // 编码回调
    public interface OnFrameEncodedCallback {
@@ -138,6 +142,7 @@
     * @param enable true表示启用文件输出,false表示禁用
     */
    public void setEnableFileOutput(boolean enable) {
        Timber.d("开启h264文件输出");
        this.enableFileOutput = enable;
    }
@@ -314,6 +319,10 @@
        encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
        encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        encoder.start();
        // 清理 SPS/PPS 缓存
        spsBuffer = null;
        ppsBuffer = null;
        Timber.d( "H264 encoder initialized");
    }
@@ -502,22 +511,21 @@
            while (outputBufferIndex >= 0) {
                ByteBuffer outputBuffer = encoder.getOutputBuffer(outputBufferIndex);
                if (outputBuffer != null && bufferInfo.size > 0) {
                    // 检查是否为关键帧
                    boolean isKeyFrame = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0;
                    // 复制编码数据
                    byte[] encodedData = new byte[bufferInfo.size];
                    byte[] nalUnit = new byte[bufferInfo.size];
                    outputBuffer.position(bufferInfo.offset);
                    outputBuffer.get(encodedData, 0, bufferInfo.size);
                    outputBuffer.get(nalUnit, 0, bufferInfo.size);
                    // 写入文件
                    writeToFile(encodedData, isKeyFrame);
                    // 解析并处理 NAL 单元
                    processNalUnit(nalUnit, timestamp);
                    // 发送编码数据
                    sendEncodedData(encodedData, timestamp, isKeyFrame);
                    // 写入文件(保持原有逻辑)
                    boolean isKeyFrame = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0;
                    writeToFile(nalUnit, isKeyFrame);
                    // 回调
                    if (callback != null) {
                        callback.onFrameEncoded(encodedData, isKeyFrame);
                        callback.onFrameEncoded(nalUnit, isKeyFrame);
                    }
                }
@@ -528,6 +536,139 @@
        } catch (Exception e) {
            Timber.e(e,"Encode frame error");
        }
    }
    /**
     * 处理 NAL 单元,识别类型并分别处理
     */
    private void processNalUnit(byte[] data, long timestamp) {
        if (data == null || data.length < 5) {
            return;
        }
        // MediaCodec 输出的数据是 Annex-B 格式,可能包含多个 NAL 单元
        // 每个 NAL 单元以 0x00000001 或 0x000001 开头
        int offset = 0;
        while (offset < data.length) {
            // 查找起始码
            int startCodePos = findStartCode(data, offset);
            if (startCodePos == -1) {
                break; // 没有找到起始码,结束
            }
            // 跳过起始码,找到 NAL 单元数据
            int nalStart = startCodePos;
            int startCodeLength = (startCodePos + 2 < data.length &&
                                   data[startCodePos] == 0x00 &&
                                   data[startCodePos + 1] == 0x00 &&
                                   data[startCodePos + 2] == 0x01) ? 3 : 4;
            int nalDataStart = nalStart + startCodeLength;
            // 查找下一个起始码,确定当前 NAL 单元的结束位置
            int nextStartCodePos = findStartCode(data, nalDataStart);
            int nalDataEnd = (nextStartCodePos == -1) ? data.length : nextStartCodePos;
            int nalDataLength = nalDataEnd - nalDataStart;
            if (nalDataLength > 0 && nalDataStart < data.length) {
                // 提取 NAL 单元数据(包含起始码)
                byte[] nalUnit = new byte[nalDataLength + startCodeLength];
                System.arraycopy(data, nalStart, nalUnit, 0, startCodeLength);
                System.arraycopy(data, nalDataStart, nalUnit, startCodeLength, nalDataLength);
                // 获取 NAL 类型(起始码后的第一个字节的低5位)
                int nalType = (nalUnit[startCodeLength] & 0x1F);
                // 根据 NAL 类型处理
                switch (nalType) {
                    case 7: // SPS
                        spsBuffer = nalUnit.clone();
                        Timber.d("SPS cached, size: %d", spsBuffer.length);
                        break;
                    case 8: // PPS
                        ppsBuffer = nalUnit.clone();
                        Timber.d("PPS cached, size: %d", ppsBuffer.length);
                        break;
                    case 5: // IDR 帧 (关键帧)
                        // 关键帧必须与 SPS、PPS 一起发送
                        if (spsBuffer != null && ppsBuffer != null) {
                            // 将 SPS, PPS, IDR 帧数据组合成一个完整的"帧数据单元"
                            byte[] frameData = combineFrameData(spsBuffer, ppsBuffer, nalUnit);
                            // 发送组合后的数据
                            sendEncodedData(frameData, timestamp, true);
                        } else {
                            Timber.w("Received IDR frame, but SPS or PPS not ready, dropping");
                            // 如果没有 SPS/PPS,仍然发送 IDR 帧(可能包含在数据中)
                            sendEncodedData(nalUnit, timestamp, true);
                        }
                        break;
                    case 1: // 非 IDR 帧 (P 帧)
                        // 直接发送 P 帧
                        sendEncodedData(nalUnit, timestamp, false);
                        break;
                    default:
                        // 其他 NALU 类型(如 SEI 等),根据协议决定是否处理
                        // 这里可以选择发送或忽略
                        Timber.d("NAL unit type %d, size: %d", nalType, nalUnit.length);
                        break;
                }
            }
            // 移动到下一个可能的起始位置
            offset = nalDataEnd;
        }
    }
    /**
     * 查找起始码位置(0x00000001 或 0x000001)
     * @param data 数据数组
     * @param startPos 开始搜索的位置
     * @return 起始码位置,如果未找到返回 -1
     */
    private int findStartCode(byte[] data, int startPos) {
        for (int i = startPos; i < data.length - 2; i++) {
            if (data[i] == 0x00 && data[i + 1] == 0x00) {
                if (i + 2 < data.length && data[i + 2] == 0x01) {
                    return i; // 找到 0x000001
                }
                if (i + 3 < data.length && data[i + 2] == 0x00 && data[i + 3] == 0x01) {
                    return i; // 找到 0x00000001
                }
            }
        }
        return -1;
    }
    /**
     * 组合 SPS、PPS 和 IDR 帧数据
     * @param sps SPS 数据(包含起始码)
     * @param pps PPS 数据(包含起始码)
     * @param idr IDR 帧数据(包含起始码)
     * @return 组合后的数据
     */
    private byte[] combineFrameData(byte[] sps, byte[] pps, byte[] idr) {
        int totalLength = sps.length + pps.length + idr.length;
        byte[] combined = new byte[totalLength];
        int offset = 0;
        // 复制 SPS
        System.arraycopy(sps, 0, combined, offset, sps.length);
        offset += sps.length;
        // 复制 PPS
        System.arraycopy(pps, 0, combined, offset, pps.length);
        offset += pps.length;
        // 复制 IDR 帧
        System.arraycopy(idr, 0, combined, offset, idr.length);
        Timber.d("Combined SPS/PPS/IDR frame, total size: %d (SPS: %d, PPS: %d, IDR: %d)",
                 totalLength, sps.length, pps.length, idr.length);
        return combined;
    }
    /**
@@ -664,6 +805,10 @@
        // 关闭文件输出
        closeFileOutput();
        // 清理 SPS/PPS 缓存
        spsBuffer = null;
        ppsBuffer = null;
        Timber.d("H264 encoder stopped");
    }
app/src/main/java/com/anyun/h264/H264FileTransmitter.java
@@ -55,6 +55,10 @@
    private int frameRate = 25; // 帧率,用于计算时间戳间隔
    private long frameInterval = 1000 / 25; // 帧间隔(毫秒)
    
    // SPS/PPS缓存
    private byte[] spsBuffer;
    private byte[] ppsBuffer;
    // 时间戳管理
    private long lastIFrameTime = 0; // 上一个I帧时间
    private long lastFrameTime = 0; // 上一帧时间
@@ -151,6 +155,8 @@
        baseTimestamp = System.currentTimeMillis();
        lastIFrameTime = 0;
        lastFrameTime = 0;
        spsBuffer = null;
        ppsBuffer = null;
        
        Timber.d("Socket initialized successfully");
        return true;
@@ -427,36 +433,7 @@
            int dataType = isKeyFrame ? JT1076ProtocolHelper.DATA_TYPE_I_FRAME :
                    JT1076ProtocolHelper.DATA_TYPE_P_FRAME;
            
            // 分包发送(如果数据超过最大包大小)
            int offset = 0;
            int totalPackets = (int) Math.ceil((double) frameData.length / JT1076ProtocolHelper.MAX_PACKET_SIZE);
            for (int i = 0; i < totalPackets; i++) {
                int packetDataSize = Math.min(JT1076ProtocolHelper.MAX_PACKET_SIZE, frameData.length - offset);
                byte[] packetData = Arrays.copyOfRange(frameData, offset, offset + packetDataSize);
                // 确定分包标记
                int packetMark;
                if (totalPackets == 1) {
                    packetMark = JT1076ProtocolHelper.PACKET_MARK_ATOMIC;
                } else if (i == 0) {
                    packetMark = JT1076ProtocolHelper.PACKET_MARK_FIRST;
                } else if (i == totalPackets - 1) {
                    packetMark = JT1076ProtocolHelper.PACKET_MARK_LAST;
                } else {
                    packetMark = JT1076ProtocolHelper.PACKET_MARK_MIDDLE;
                }
                // 创建RTP包
                byte[] rtpPacket = protocolHelper.createVideoRtpPacket(
                        packetData, timestamp, dataType, packetMark,
                        lastIFrameInterval, lastFrameInterval);
                // 发送RTP包(UDP或TCP,根据协议类型自动选择)
                protocolHelper.sendPacket(rtpPacket);
                offset += packetDataSize;
            }
            processNalUnits(frameData, timestamp, lastIFrameInterval, lastFrameInterval);
            
            // 控制发送速率(模拟帧率)
            if (frameInterval > 0) {
@@ -498,10 +475,160 @@
            protocolHelper.closeSocket();
        }
        
        spsBuffer = null;
        ppsBuffer = null;
        Timber.d("H264 file transmitter stopped");
    }
    
    /**
     * 解析帧中的NAL单元并根据类型处理
     */
    private void processNalUnits(byte[] frameData, long timestamp,
                                 long lastIFrameInterval, long lastFrameInterval) {
        if (frameData == null || frameData.length == 0) {
            return;
        }
        boolean nalProcessed = false;
        int offset = 0;
        while (offset < frameData.length) {
            int startCodePos = findStartCode(frameData, offset);
            if (startCodePos < 0) {
                break;
            }
            int startCodeLen = getStartCodeLength(frameData, startCodePos);
            int nalDataStart = startCodePos + startCodeLen;
            if (nalDataStart >= frameData.length) {
                break;
            }
            int nextStart = findStartCode(frameData, nalDataStart);
            int nalEnd = (nextStart == -1) ? frameData.length : nextStart;
            int nalLength = nalEnd - startCodePos;
            if (nalLength <= startCodeLen) {
                break;
            }
            byte[] nalUnit = Arrays.copyOfRange(frameData, startCodePos, nalEnd);
            int nalType = nalUnit[startCodeLen] & 0x1F;
            handleNalUnit(nalUnit, nalType, timestamp, lastIFrameInterval, lastFrameInterval);
            nalProcessed = true;
            offset = nalEnd;
        }
        if (!nalProcessed) {
            // 没有解析出NAL单元时,直接按P帧发送
            sendFramePayload(frameData, timestamp, false, lastIFrameInterval, lastFrameInterval);
        }
    }
    private void handleNalUnit(byte[] nalUnit, int nalType, long timestamp,
                               long lastIFrameInterval, long lastFrameInterval) {
        switch (nalType) {
            case 7: // SPS
                spsBuffer = nalUnit.clone();
                Timber.d("Cached SPS, size: %d", spsBuffer.length);
                break;
            case 8: // PPS
                ppsBuffer = nalUnit.clone();
                Timber.d("Cached PPS, size: %d", ppsBuffer.length);
                break;
            case 5: // IDR
                if (spsBuffer != null && ppsBuffer != null) {
                    byte[] combined = combineFrameData(spsBuffer, ppsBuffer, nalUnit);
                    sendFramePayload(combined, timestamp, true, lastIFrameInterval, lastFrameInterval);
                } else {
                    Timber.w("IDR frame without SPS/PPS cache, sending raw IDR");
                    sendFramePayload(nalUnit, timestamp, true, lastIFrameInterval, lastFrameInterval);
                }
                break;
            case 1: // P frame
                sendFramePayload(nalUnit, timestamp, false, lastIFrameInterval, lastFrameInterval);
                break;
            default:
                Timber.d("Forwarding NAL type %d, size: %d", nalType, nalUnit.length);
                sendFramePayload(nalUnit, timestamp, false, lastIFrameInterval, lastFrameInterval);
                break;
        }
    }
    private void sendFramePayload(byte[] payload, long timestamp, boolean isKeyFrame,
                                  long lastIFrameInterval, long lastFrameInterval) {
        if (payload == null || payload.length == 0) {
            return;
        }
        int dataType = isKeyFrame ? JT1076ProtocolHelper.DATA_TYPE_I_FRAME :
                JT1076ProtocolHelper.DATA_TYPE_P_FRAME;
        int offset = 0;
        int totalPackets = (int) Math.ceil((double) payload.length / JT1076ProtocolHelper.MAX_PACKET_SIZE);
        for (int i = 0; i < totalPackets; i++) {
            int packetDataSize = Math.min(JT1076ProtocolHelper.MAX_PACKET_SIZE, payload.length - offset);
            byte[] packetData = Arrays.copyOfRange(payload, offset, offset + packetDataSize);
            int packetMark;
            if (totalPackets == 1) {
                packetMark = JT1076ProtocolHelper.PACKET_MARK_ATOMIC;
            } else if (i == 0) {
                packetMark = JT1076ProtocolHelper.PACKET_MARK_FIRST;
            } else if (i == totalPackets - 1) {
                packetMark = JT1076ProtocolHelper.PACKET_MARK_LAST;
            } else {
                packetMark = JT1076ProtocolHelper.PACKET_MARK_MIDDLE;
            }
            byte[] rtpPacket = protocolHelper.createVideoRtpPacket(
                    packetData, timestamp, dataType, packetMark,
                    lastIFrameInterval, lastFrameInterval);
            protocolHelper.sendPacket(rtpPacket);
            offset += packetDataSize;
        }
    }
    private int findStartCode(byte[] data, int startPos) {
        if (data == null || startPos >= data.length - 3) {
            return -1;
        }
        for (int i = startPos; i < data.length - 3; i++) {
            if (isStartCodeAt(data, i)) {
                return i;
            }
        }
        // 处理剩余不足4字节但可能是3字节起始码的情况
        for (int i = Math.max(startPos, data.length - 3); i < data.length - 2; i++) {
            if (isStartCodeAt(data, i)) {
                return i;
            }
        }
        return -1;
    }
    private byte[] combineFrameData(byte[] sps, byte[] pps, byte[] idr) {
        int totalLength = (sps != null ? sps.length : 0) +
                (pps != null ? pps.length : 0) +
                (idr != null ? idr.length : 0);
        byte[] combined = new byte[totalLength];
        int offset = 0;
        if (sps != null) {
            System.arraycopy(sps, 0, combined, offset, sps.length);
            offset += sps.length;
        }
        if (pps != null) {
            System.arraycopy(pps, 0, combined, offset, pps.length);
            offset += pps.length;
        }
        if (idr != null) {
            System.arraycopy(idr, 0, combined, offset, idr.length);
        }
//        Timber.d("Combined SPS/PPS/IDR payload, total: %d", totalLength);
        return combined;
    }
    /**
     * 释放资源
     */
    public void release() {
app/src/main/java/com/anyun/h264/JT1076ProtocolHelper.java
@@ -35,7 +35,7 @@
    public static final int PACKET_MARK_MIDDLE = 0x03; // 中间包
    
    // RTP负载类型
    public static final int RTP_PAYLOAD_TYPE_VIDEO = 96;  // 视频负载类型
    public static final int RTP_PAYLOAD_TYPE_VIDEO = 98;  // 视频负载类型
    public static final int RTP_PAYLOAD_TYPE_AUDIO = 97;  // 音频负载类型
    
    // 传输协议类型
@@ -67,6 +67,7 @@
    public void setServerAddress(String ip, int port) {
        this.serverIp = ip;
        this.serverPort = port;
        Timber.w("设置IP="+ip+"_port:"+port);
        // 如果TCP客户端已存在,更新地址
        if (tcpClient != null) {
            tcpClient.setServerAddress(ip, port);
app/src/main/java/com/anyun/h264/JT1076TcpClient.java
@@ -141,8 +141,15 @@
        try {
            // 将字节数组包装为ByteBuf
            ByteBuf buffer = Unpooled.wrappedBuffer(packet);
            String str = BytesUtils.bytesToHexString(  BytesUtils.subArray(buffer.array(),0,30));
            Timber.i( "Send TCP packet:"+ str);
       /*     byte[] dataBytes = buffer.array();
            int len = 0;
            if (dataBytes.length<100){
                len = dataBytes.length;
            }else {
                len = 100;
            }
            String str = BytesUtils.bytesToHexString(  BytesUtils.subArray(dataBytes,0,len));
            Timber.i( "Send TCP packet:"+ str);*/
            // 异步写入
            ChannelFuture future = channel.writeAndFlush(buffer);
            
app/src/main/java/com/anyun/h264/MainActivity.kt
@@ -1,7 +1,7 @@
package com.anyun.h264
import android.os.Bundle
import timber.log.Timber
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
@@ -12,18 +12,22 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.anyun.h264.ui.theme.MyApplicationTheme
import timber.log.Timber
import java.io.File
class MainActivity : ComponentActivity() {
    private var h264Encoder: H264Encoder? = null
    private var transmitter: H264FileTransmitter? = null
    companion object{
        const val TAG ="MainActivity"
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            var isRunning by remember { mutableStateOf(false) }
            MyApplicationTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    MainScreen(
@@ -44,40 +48,79 @@
            }
        }
    }
    override fun onDestroy() {
        super.onDestroy()
        stopH264Encoder()
    }
    private fun startFileTransmitter():Boolean {
        if (transmitter != null) {
            Timber.w("H264Encoder is already running")
            return false
        }
        try {
            transmitter = H264FileTransmitter()
            Log.i(TAG,"startFileTransmitter")
            transmitter?.setServerAddress("192.168.16.138", 1078)
            transmitter?.setProtocolType(JT1076ProtocolHelper.PROTOCOL_TYPE_TCP) // 或 PROTOCOL_TYPE_UDP
            // 设置协议参数
            transmitter?.setProtocolParams("013120122580", 1.toByte())
            // 设置帧率(用于计算时间戳间隔)
            transmitter?.setFrameRate(25)
            // 初始化Socket
            if (transmitter?.initialize()==true) {
                // 开始传输文件
                transmitter?.transmitFile("/storage/emulated/0/Android/data/com.anyun.h264/files/h264_1764574451071.h264")
                return true
            }else{
                return false
            }
        } catch (e: Exception) {
            Timber.e(e, "Failed to start H264Encoder")
            transmitter = null
            return false
        }
    }
    private fun startH264Encoder(): Boolean {
        if (h264Encoder != null) {
            Timber.w("H264Encoder is already running")
            return false
        }
        try {
            // 创建编码器
            h264Encoder = H264Encoder()
            // 设置编码参数
            h264Encoder?.setEncoderParams(640, 480, 25, 2000000)
            // 设置输出文件(可选)
            val outputFile = File(getExternalFilesDir(null), "test.h264")
            val outputFile = File(getExternalFilesDir(null), "test2.h264")
            h264Encoder?.setOutputFile(outputFile.absolutePath)
            h264Encoder?.setEnableFileOutput(true) // 启用文件输出
            h264Encoder?.setEnableFileOutput(false) // 启用文件输出
            // 设置UDP服务器地址(可选)
//             h264Encoder?.setServerAddress("58.48.93.67", 11935)
            h264Encoder?.setEnableNetworkTransmission(false)
             h264Encoder?.setServerAddress("192.168.16.12", 11935)
             h264Encoder?.setProtocolParams("013120122580", 1)
            h264Encoder?.setEnableNetworkTransmission(true)
            h264Encoder?.setServerAddress("192.168.16.138", 1078)
            h264Encoder?.setProtocolParams("013120122580", 1)
            // 初始化并启动
            val cameraIdRange = intArrayOf(1, 2)
            val resolution = intArrayOf(640, 480)
            if (h264Encoder?.initialize(cameraIdRange, null, resolution, false) == true) {
                h264Encoder?.start()
                Timber.d("H264Encoder started successfully")
@@ -94,7 +137,7 @@
            return false
        }
    }
    private fun stopH264Encoder() {
        h264Encoder?.let { encoder ->
            try {
@@ -126,9 +169,9 @@
            text = "H264 编码器",
            style = MaterialTheme.typography.headlineMedium
        )
        Spacer(modifier = Modifier.height(32.dp))
        Button(
            onClick = onStartH264Click,
            enabled = !isRunning,
@@ -138,9 +181,9 @@
        ) {
            Text("启动 H264")
        }
        Spacer(modifier = Modifier.height(16.dp))
        Button(
            onClick = onStopH264Click,
            enabled = isRunning,
@@ -153,9 +196,9 @@
        ) {
            Text("停止 H264")
        }
        Spacer(modifier = Modifier.height(32.dp))
        if (isRunning) {
            Text(
                text = "编码器运行中...",