StitchCoder 2025-04-15 17:18 采纳率: 0%
浏览 15

Miracast Sink Android实现端的Mpeg2Ts解码播放问题

解析M2TS成h264视频流与AAC音频流然后使用MediaCodec进行播放,然而视频卡顿严重,MediaCodec给出的日志是错误的视频帧,我将接收到的TS包保存进ts文件,然后利用一些播放软件播放却没有卡顿问题,下面是我的代码,希望有相关处理经验的人帮忙看看问题出在哪,可以解决的可有偿
UDP接收数据线程

```kotlin
    //缓存接收到的TS包
    private val tsPacketQueue: LinkedBlockingQueue<ByteArray> = LinkedBlockingQueue()
    //缓存处理好的H264数据
    private var h264Queue: LinkedBlockingQueue<MediaFrame> = LinkedBlockingQueue()
    //缓存处理好的AAC音频数据
    private var aacQueue: LinkedBlockingQueue<MediaFrame> = LinkedBlockingQueue()


//udp线程,接受RTP数据包并解析成一个个Ts包
    private val rtpThread = Thread {
        udpSocket = DatagramSocket(20011)
        var lastSeqNo: Int? = null
        try {
            udpSocket!!.receiveBufferSize = 2 * 1024 * 1024
            val packet = DatagramPacket(ByteArray(1328), 1328)
            //会话成功建立 开启udp监听
            while (true) {
                udpSocket!!.receive(packet)
                var startIndex = 12
                var endIndex = 12 + 188
                while (endIndex <= packet.data.size) {
                    val tsPacket = ByteArray(TS_PACKET_LENGTH)
                    System.arraycopy(packet.data, startIndex, tsPacket, 0, tsPacket.size)
                    if (tsPacket[0] == TS_PACKET_SYNC) {
                        tsPacketQueue.put(tsPacket)
                    } else {
                        Log.d(TAG, "error ts packet:[${tsPacket.toHexString()}]")
                    }
                    startIndex += 188
                    endIndex += 188
                }
            }
        } catch (e: InterruptedException) {
            Log.d(TAG, "RTPThread Interrupted")
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            Log.d(TAG, "UDP Closed")
            udpSocket?.close()
        }
    }
解析Mpeg2Ts线程

```kotlin
//解析ts包线程
    private val tsParseThread = Thread {
        //暂存h264视频帧
        val h264PesStream = ByteArrayOutputStream(H264_STREAM_SIZE)
        //暂存aac音频帧
        val aacPesStream = ByteArrayOutputStream(AAC_STREAM_SIZE)
        try {
            while (true) {
                val tsPacket = tsPacketQueue.take()
                //ts包的数据头
                val tsHeader = ByteArray(4)
                System.arraycopy(tsPacket, 0, tsHeader, 0, tsHeader.size)
                //ts包的数据体
                val tsPayload = ByteArray(TS_PACKET_LENGTH - 4)
                System.arraycopy(tsPacket, 4, tsPayload, 0, tsPayload.size)
                //是否为一帧的第一包
                val payloadIndicator = (tsHeader[1].toInt() ushr 6) and 0x01
                //获取Ts的pid 用于区分节目类型
                val pid = ((tsHeader[1].toInt() and 0x1F) shl 8) or (tsHeader[2].toInt() and 0xFF)
                //是否有适配域标识
                val adapterIndicator = (tsHeader[3].toInt() ushr 4) and 0x03
                //获取连续性计数器
                val continuityCounter = tsHeader[3].toInt() and 0x0F
                if (pid == H264_PID) {
                    parseVideoTs(
                        TsPacket(
                            continuityCounter, payloadIndicator, adapterIndicator, tsPayload
                        ), h264PesStream
                    )
                }
                if (pid == ACC_PID) {
                    //音频流包
                    parseAudioTs(
                        TsPacket(
                            continuityCounter, payloadIndicator, adapterIndicator, tsPayload
                        ), aacPesStream
                    )
                }
            }
        } catch (e: InterruptedException) {
            Log.d(TAG, "TSParseThread Interrupted")
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }


    //解析音频Ts包
    private fun parseAudioTs(
        tsPacket: TsPacket, aacPesStream: ByteArrayOutputStream
    ) {
        //若不是第一包且pes包为空,丢弃包
        if (tsPacket.payloadIndicator != 1 && aacPesStream.size() == 0) {
            return
        }
        //若是第一包,将之前的pes包数据发出并清空
        if (tsPacket.payloadIndicator == 1 && aacPesStream.size() > 0) {
            // 一帧的开头 将上一帧写入展示层
            val pesPacket = aacPesStream.toByteArray()
            val aacFrame = parsePES(pesPacket)
            if (aacFrame != null && receive) {
                aacQueue.put(aacFrame)
            }
            //重置pes包
            aacPesStream.reset()
        }
        //若有适配域,则去掉适配域
        if (tsPacket.adapterIndicator == 1) {
            aacPesStream.write(tsPacket.tsPayload)
        } else if (tsPacket.adapterIndicator == 3) {
            val adapterSize = tsPacket.tsPayload[0].toInt() and 0xFF
            val payload = ByteArray(tsPacket.tsPayload.size - adapterSize - 1)
            System.arraycopy(tsPacket.tsPayload, adapterSize + 1, payload, 0, payload.size)
            aacPesStream.write(payload)
        }
    }

    //解析视频TS包
    private fun parseVideoTs(
        tsPacket: TsPacket, pesPktStream: ByteArrayOutputStream
    ) {
        //若不是第一包且pes包为空,丢弃包
        if (tsPacket.payloadIndicator != 1 && pesPktStream.size() == 0) {
            Log.d(TAG, "丢弃不完整的包")
            return
        }
        //若是第一包,将之前的pes包数据发出并清空
        if (tsPacket.payloadIndicator == 1 && pesPktStream.size() > 0) {
            // 一帧的开头 将上一帧写入展示层
            val pesPacket = pesPktStream.toByteArray()
            val h264Frame = parsePES(pesPacket)
            if (h264Frame != null && receive) {
                h264Queue.put(h264Frame)
            } else {
                Log.d(TAG, "包有问题,丢掉")
            }
            //重置pes包
            pesPktStream.reset()
        }
        //若有适配域,则去掉适配域
        if (tsPacket.adapterIndicator == 1) {
            pesPktStream.write(tsPacket.tsPayload)
        } else if (tsPacket.adapterIndicator == 3) {
            val adapterSize = tsPacket.tsPayload[0].toInt() and 0xFF
            val payload = ByteArray(tsPacket.tsPayload.size - adapterSize - 1)
            System.arraycopy(tsPacket.tsPayload, adapterSize + 1, payload, 0, payload.size)
            pesPktStream.write(payload)
        }
    }

    //解析PES包
    private fun parsePES(pesPacket: ByteArray): MediaFrame? {
        // 解析pes包 得到PTS
        val startCode = ByteArray(3)
        System.arraycopy(pesPacket, 0, startCode, 0, startCode.size)
        val stream = ByteArray(1)
        System.arraycopy(pesPacket, 3, stream, 0, stream.size)
        val pesHeader = ByteArray(3)
        System.arraycopy(pesPacket, 6, pesHeader, 0, pesHeader.size)
        //检查PES头部 若不匹配抛弃
        val headerTag = (pesHeader[0].toInt() ushr 6) and 0x03
        if (headerTag != 2) {
            return null
        }
        val ptsTag = (pesHeader[1].toInt() ushr 7) and 0x01
        if (ptsTag != 1) {
            return null
        }
        val headerDataSize = pesHeader[2].toInt()
        val headerData = ByteArray(5)
        System.arraycopy(pesPacket, 9, headerData, 0, 5)
        val pts = parsePTS(headerData)
        val streamBytes = ByteArray(pesPacket.size - 9 - headerDataSize)
        System.arraycopy(
            pesPacket, 9 + headerDataSize, streamBytes, 0, pesPacket.size - 9 - headerDataSize
        )
        return MediaFrame(streamBytes, pts)
    }

    //通过pts字节码计算出pts时间
    private fun parsePTS(ptsBytes: ByteArray): Long {
        val ptsValue =
            ((ptsBytes[0].toLong() and 0x0E) shl 29) or ((ptsBytes[1].toLong() and 0xFF) shl 22) or ((ptsBytes[2].toLong() and 0xFE) shl 14) or ((ptsBytes[3].toLong() and 0xFF) shl 7) or ((ptsBytes[4].toLong() and 0xFE) ushr 1)
        return ((ptsValue * 100) / 9)
    }

MediaCodec解码播放线程

private fun initVideoMediaCodec() {
        try {
            //创建解码器 H264
            mediaCodec = MediaCodec.createDecoderByType("video/avc")
            Log.d(TAG, "H264 Decoder:${mediaCodec.codecInfo.name}")
            //创建配置
            val mediaFormat = createVideoFormat("video/avc", 1920, 1080)
            //配置绑定mediaFormat和surface
            mediaCodec.configure(mediaFormat, surface, null, 0)
        } catch (e: IOException) {
            e.printStackTrace()
            //创建解码失败
            Log.e(TAG, "Create Decoder Failure")
        }
    }

    /**
     * 解码播放
     */
    fun decodePlay() {
        mediaCodec.start()
        //渲染H.264数据包
        decodeThread.start()
    }

    private val decodeThread = Thread {
        try {
            var startFrameTime = 0L
            var lastFrameTime = 0L
            while (true) {
                val h264frame = h264Queue.take()
                val h264Packet = h264frame.bytes
                val arrPts = h264frame.pts
                if (startFrameTime == 0L) startFrameTime = arrPts
                val pts = arrPts - startFrameTime
                if (pts < lastFrameTime) continue
                lastFrameTime = pts
                // 查询10000毫秒后,如果dSP芯片的buffer全部被占用,返回-1;存在则大于0
                val inIndex = mediaCodec.dequeueInputBuffer(10000)
                if (inIndex >= 0) {
                    //根据返回的index拿到可以用的buffer
                    val byteBuffer = mediaCodec.getInputBuffer(inIndex)
                    //清空缓存
                    byteBuffer!!.clear()
                    //开始为buffer填充数据
                    byteBuffer.put(h264Packet)
                    //填充数据后通知mediacodec查询inIndex索引的这个buffer,
                    mediaCodec.queueInputBuffer(inIndex, 0, h264Packet.size, pts, 0)
                }
                val info = MediaCodec.BufferInfo()
                //mediaCodec 查询 "mediaCodec的输出方队列"得到索引
                var outIndex = mediaCodec.dequeueOutputBuffer(info, 10000)
                while (outIndex >= 0) {
                    //如果surface绑定了,则直接输入到surface渲染并释放
                    mediaCodec.releaseOutputBuffer(outIndex, true)
                    outIndex = mediaCodec.dequeueOutputBuffer(info, 0)
                }
            }
        } catch (e: InterruptedException) {
            Log.d(TAG, "H264 Render Thread Interrupted")
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

  • 写回答

4条回答 默认 最新

  • 紫雾凌寒 新星创作者: 人工智能技术领域 2025-04-15 18:07
    关注

    让 “紫雾凌寒” 来帮你回答,本回答结合 DeepSeek

    如果还有问题,请评论或点击头像私信,如果有帮助到您, 请关注、采纳、点赞

    1. 关键点分析:
      - 从Miracast Sink端接收Mpeg2Ts流,需要准确解析成H264视频流和AAC音频流。
      - 使用MediaCodec播放解析后的流时出现视频卡顿且日志显示错误视频帧。
      - 保存的TS包能被其他播放软件正常播放,说明问题出在解析或MediaCodec播放环节。
    2. 解决方案:
      - Mpeg2Ts解析
      • 可以使用第三方库如FFmpeg来进行Mpeg2Ts的解析。FFmpeg是一个强大的多媒体框架,能高效地处理各种音视频格式。以下是一个简单的使用FFmpeg解析Mpeg2Ts的示例代码片段(基于FFmpeg Android API):
    #include <libavformat/avformat.h>
    
    // 初始化FFmpeg
    av_register_all();
    
    AVFormatContext *pFormatCtx = avformat_alloc_context();
    if (avformat_open_input(&pFormatCtx, "input.ts", NULL, NULL)!= 0) {
        // 打开文件失败处理
        return -1;
    }
    
    if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
        // 查找流信息失败处理
        return -1;
    }
    
    int video_stream_index = -1;
    int audio_stream_index = -1;
    
    for (unsigned int i = 0; i < pFormatCtx->nb_streams; i++) {
        if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            video_stream_index = i;
        } else if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
            audio_stream_index = i;
        }
    }
    
    // 提取视频流和音频流
    if (video_stream_index >= 0) {
        AVCodecParameters *video_codecpar = pFormatCtx->streams[video_stream_index]->codecpar;
        AVCodec *video_codec = avcodec_find_decoder(video_codecpar->codec_id);
        AVCodecContext *video_codec_ctx = avcodec_alloc_context3(video_codec);
        if (avcodec_parameters_to_context(video_codec_ctx, video_codecpar) < 0) {
            // 参数转换失败处理
            return -1;
        }
        if (avcodec_open2(video_codec_ctx, video_codec, NULL) < 0) {
            // 打开视频解码器失败处理
            return -1;
        }
        // 这里可以进一步处理视频帧,比如保存为H264文件等
    }
    
    if (audio_stream_index >= 0) {
        AVCodecParameters *audio_codecpar = pFormatCtx->streams[audio_stream_index]->codecpar;
        AVCodec *audio_codec = avcodec_find_decoder(audio_codecpar->codec_id);
        AVCodecContext *audio_codec_ctx = avcodec_alloc_context3(audio_codec);
        if (avcodec_parameters_to_context(audio_codec_ctx, audio_codecpar) < 0) {
            // 参数转换失败处理
            return -1;
        }
        if (avcodec_open2(audio_codec_ctx, audio_codec, NULL) < 0) {
            // 打开音频解码器失败处理
            return -1;
        }
        // 这里可以进一步处理音频帧,比如保存为AAC文件等
    }
    
    // 清理资源
    avformat_close_input(&pFormatCtx);
    
    • MediaCodec播放
      • 检查MediaCodec的配置参数是否正确。例如,确保视频的MIME类型(如”video/avc”)和音频的MIME类型(如”audio/mp4a-latm”)设置正确。
      • 确认输入数据的格式和时间戳是否符合MediaCodec的要求。可能需要对解析后的H264和AAC数据进行格式调整,确保它们能被MediaCodec正确处理。
      • 检查MediaCodec的输出表面(用于视频显示)是否正确设置和绑定。
      • 对于视频卡顿问题,可以尝试调整MediaCodec的缓冲区大小和配置参数,例如增加视频缓冲区的数量以减少丢帧。示例代码如下:
    MediaMuxer mediaMuxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
    mediaMuxer.start();
    
    MediaCodec mediaCodec = MediaCodec.createDecoderByType("video/avc");
    MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
    
    mediaCodec.configure(null, mediaCodec.createInputSurface(), null, 0);
    mediaCodec.start();
    
    // 模拟解析后的H264数据输入
    for (int i = 0; i < numFrames; i++) {
        byte[] h264Data = getH264Data(i);
        int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);
        if (inputBufferIndex >= 0) {
            ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferIndex);
            inputBuffer.clear();
            inputBuffer.put(h264Data);
    
            mediaCodec.queueInputBuffer(inputBufferIndex, 0, h264Data.length, presentationTimeUs, 0);
        }
    
        int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
        while (outputBufferIndex >= 0) {
            mediaCodec.getOutputBuffer(outputBufferIndex, bufferInfo);
            mediaCodec.getOutputBuffer(outputBufferIndex, bufferInfo);
            mediaCodec.getOutputBuffer(outputBufferIndex, bufferInfo);
            mediaCodec.getOutputBuffer(outputBufferIndex, bufferInfo);
            mediaCodec.getOutputBuffer(outputBufferIndex, bufferInfo);
    
            mediaCodec.getOutputBuffer(outputBufferIndex, bufferInfo);
    
    
            ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferIndex);
            byte[] outputData = new byte[bufferInfo.size];
            outputBuffer.get(outputData);
    
            // 可以在这里将输出数据进行保存或进一步处理
    
            mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
            outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
        }
    }
    
    mediaCodec.stop();
    mediaCodec.release();
    mediaMuxer.stop();
    mediaMuxer.release();
    
    1. 多种解决方案优缺点:
      - 使用FFmpeg解析
      • 优点:功能强大,能处理多种复杂的音视频格式转换,广泛应用于多媒体领域,社区支持丰富,遇到问题容易找到解决方案。
      • 缺点:引入了额外的库,增加了项目的复杂度和包体积,如果对FFmpeg不熟悉,配置和使用可能会有一定难度。
      • 优化MediaCodec播放
      • 优点:基于Android原生框架,性能可能更好,与Android系统集成度高,容易进行调试和优化。
      • 缺点:需要对MediaCodec的底层机制有深入了解,调试相对复杂,可调整的参数有限,对于复杂的格式问题可能处理能力不足。
    2. 总结:
      - 首先通过FFmpeg准确解析Mpeg2Ts成H264和AAC流,确保解析过程正确。然后仔细检查MediaCodec的配置和输入数据格式,针对视频卡顿问题调整缓冲区等参数,通过逐步排查和优化,有望解决视频卡顿及错误视频帧的问题。

    需要注意的是,实际应用中还需要根据具体情况进行更多的错误处理和细节优化,上述代码仅为示例,帮助理解基本思路。

    希望以上解答对您有所帮助。如果您有任何疑问,欢迎在评论区提出。

    评论

报告相同问题?

问题事件

  • 修改了问题 4月15日
  • 修改了问题 4月15日
  • 修改了问题 4月15日
  • 修改了问题 4月15日
  • 展开全部