当前位置: 首页 > 知识库问答 >
问题:

将Android MediaCodec编码的H264数据包多路复用到RTMP中

狄海
2023-03-14

我来自一个线程编码H.264从摄像头与Android媒体编解码器。我的设置很相似。然而,我尝试用javacv编写mux编码帧和并通过RTMP广播它们。

rtmpclient.java

...
private volatile BlockingQueue<byte[]> mFrameQueue = new LinkedBlockingQueue<byte[]>(MAXIMUM_VIDEO_FRAME_BACKLOG);
...
private void startStream() throws FrameRecorder.Exception, IOException {
    if (TextUtils.isEmpty(mDestination)) {
        throw new IllegalStateException("Cannot start RtmpClient without destination");
    }

    if (mCamera == null) {
        throw new IllegalStateException("Cannot start RtmpClient without camera.");
    }

    Camera.Parameters cameraParams = mCamera.getParameters();

    mRecorder = new FFmpegFrameRecorder(
            mDestination,
            mVideoQuality.resX,
            mVideoQuality.resY,
            (mAudioQuality.channelType.equals(AudioQuality.CHANNEL_TYPE_STEREO) ? 2 : 1));

    mRecorder.setFormat("flv");

    mRecorder.setFrameRate(mVideoQuality.frameRate);
    mRecorder.setVideoBitrate(mVideoQuality.bitRate);
    mRecorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);

    mRecorder.setSampleRate(mAudioQuality.samplingRate);
    mRecorder.setAudioBitrate(mAudioQuality.bitRate);
    mRecorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);

    mVideoStream = new VideoStream(mRecorder, mVideoQuality, mFrameQueue, mCamera);
    mAudioStream = new AudioStream(mRecorder, mAudioQuality);

    mRecorder.start();

    // Setup a bufferred preview callback
    setupCameraCallback(mCamera, mRtmpClient, DEFAULT_PREVIEW_CALLBACK_BUFFERS,
            mVideoQuality.resX * mVideoQuality.resY * ImageFormat.getBitsPerPixel(
                    cameraParams.getPreviewFormat())/8);

    try {
        mVideoStream.start();
        mAudioStream.start();
    }
    catch(Exception e) {
        e.printStackTrace();
        stopStream();
    }
}
...
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
    boolean frameQueued = false;

    if (mRecorder == null || data == null) {
        return;
    }

    frameQueued = mFrameQueue.offer(data);

    // return the buffer to be reused - done in videostream
    //camera.addCallbackBuffer(data);
}
...
...
@Override
public void run() {
    try {
        mMediaCodec = MediaCodec.createEncoderByType("video/avc");
        MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", mVideoQuality.resX, mVideoQuality.resY);
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, mVideoQuality.bitRate);
        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, mVideoQuality.frameRate);
        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
        mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        mMediaCodec.start();
    }
    catch(IOException e) {
        e.printStackTrace();
    }

    long startTimestamp = System.currentTimeMillis();
    long frameTimestamp = 0;
    byte[] rawFrame = null;

    try {
        while (!Thread.interrupted()) {
            rawFrame = mFrameQueue.take();

            frameTimestamp = 1000 * (System.currentTimeMillis() - startTimestamp);

            encodeFrame(rawFrame, frameTimestamp);

            // return the buffer to be reused
            mCamera.addCallbackBuffer(rawFrame);
        }
    }
    catch (InterruptedException ignore) {
        // ignore interrup while waiting
    }

    // Clean up video stream allocations
    try {
        mMediaCodec.stop();
        mMediaCodec.release();
        mOutputStream.flush();
        mOutputStream.close();
    } catch (Exception e){
        e.printStackTrace();
    }
}
...
private void encodeFrame(byte[] input, long timestamp) {
    try {
        ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
        ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers();

        int inputBufferIndex = mMediaCodec.dequeueInputBuffer(0);

        if (inputBufferIndex >= 0) {
            ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
            inputBuffer.clear();
            inputBuffer.put(input);
            mMediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, timestamp, 0);
        }

        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();

        int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0);

        if (outputBufferIndex >= 0) {
            while (outputBufferIndex >= 0) {
                ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];

                // Should this be a direct byte buffer?
                byte[] outData = new byte[bufferInfo.size - bufferInfo.offset];
                outputBuffer.get(outData);

                mFrameRecorder.record(outData, bufferInfo.offset, outData.length, timestamp);

                mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
                outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0);
            }
        }
        else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
            outputBuffers = mMediaCodec.getOutputBuffers();
        } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
            // ignore for now
        }
    } catch (Throwable t) {
        t.printStackTrace();
    }

}
...
...
// Hackish codec copy frame recording function
public boolean record(byte[] encodedData, int offset, int length, long frameCount) throws Exception {
    int ret;

    if (encodedData == null) {
        return false;
    }

    av_init_packet(video_pkt);

    // this is why i wondered whether I should get outputbuffer data into direct byte buffer
    video_outbuf.put(encodedData, 0, encodedData.length);

    video_pkt.data(video_outbuf);
    video_pkt.size(video_outbuf_size);

    video_pkt.pts(frameCount);
    video_pkt.dts(frameCount);

    video_pkt.stream_index(video_st.index());

    synchronized (oc) {
        /* write the compressed frame in the media file */
        if (interleaved && audio_st != null) {
            if ((ret = av_interleaved_write_frame(oc, video_pkt)) < 0) {
                throw new Exception("av_interleaved_write_frame() error " + ret + " while writing interleaved video frame.");
            }
        } else {
            if ((ret = av_write_frame(oc, video_pkt)) < 0) {
                throw new Exception("av_write_frame() error " + ret + " while writing video frame.");
            }
        }
    }
    return (video_pkt.flags() & AV_PKT_FLAG_KEY) == 1;
}
...
ffprobe version 2.5.3 Copyright (c) 2007-2015 the FFmpeg developers
  built on Jan 19 2015 12:56:57 with gcc 4.1.2 (GCC) 20080704 (Red Hat 4.1.2-55)
  configuration: --prefix=/usr --bindir=/usr/bin --datadir=/usr/share/ffmpeg --incdir=/usr/include/ffmpeg --libdir=/usr/lib64 --mandir=/usr/share/man --arch=x86_64 --optflags='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector --param=ssp-buffer-size=4 -m64 -mtune=generic' --enable-bzlib --disable-crystalhd --enable-libass --enable-libdc1394 --enable-libfaac --enable-nonfree --disable-indev=jack --enable-libfreetype --enable-libgsm --enable-libmp3lame --enable-openal --enable-libopencv --enable-libopenjpeg --enable-libopus --enable-librtmp --enable-libtheora --enable-libvorbis --enable-libvpx --enable-libx264 --enable-libxvid --enable-x11grab --enable-avfilter --enable-avresample --enable-postproc --enable-pthreads --disable-static --enable-shared --enable-gpl --disable-debug --disable-stripping --enable-libcaca --shlibdir=/usr/lib64 --enable-runtime-cpudetect
  libavutil      54. 15.100 / 54. 15.100
  libavcodec     56. 13.100 / 56. 13.100
  libavformat    56. 15.102 / 56. 15.102
  libavdevice    56.  3.100 / 56.  3.100
  libavfilter     5.  2.103 /  5.  2.103
  libavresample   2.  1.  0 /  2.  1.  0
  libswscale      3.  1.101 /  3.  1.101
  libswresample   1.  1.100 /  1.  1.100
  libpostproc    53.  3.100 / 53.  3.100
Metadata:
  Server                NGINX RTMP (github.com/arut/nginx-rtmp-module)
  width                 320.00
  height                240.00
  displayWidth          320.00
  displayHeight         240.00
  duration              0.00
  framerate             0.00
  fps                   0.00
  videodatarate         261.00
  videocodecid          7.00
  audiodatarate         62.00
  audiocodecid          10.00
  profile
  level
[live_flv @ 0x1edb0820] Could not find codec parameters for stream 0 (Video: none, none, 267 kb/s): unknown codec
Consider increasing the value for the 'analyzeduration' and 'probesize' options
Input #0, live_flv, from 'rtmp://<server>/input/<stream id>':
  Metadata:
    Server          : NGINX RTMP (github.com/arut/nginx-rtmp-module)
    displayWidth    : 320
    displayHeight   : 240
    fps             : 0
    profile         :
    level           :
  Duration: 00:00:00.00, start: 16.768000, bitrate: N/A
    Stream #0:0: Video: none, none, 267 kb/s, 1k tbr, 1k tbn, 1k tbc
    Stream #0:1: Audio: aac (LC), 16000 Hz, mono, fltp, 63 kb/s
Unsupported codec with id 0 for input stream 0

我不是,无论如何,一个专家在H264或视频编码。我知道从MediaCodec出来的编码帧包含SPS NAL、PPS NAL和帧NAL单元。我还将MediaCodec输出写入了一个文件,并且能够回放它(我确实必须指定格式和帧率,否则它会播放得太快)。

我的假设是事情应该工作(看看我知道的有多少:))。知道了SPS和PPS被写出来,解码器应该知道足够多。然而,ffprobe无法识别编解码器、fps和其他视频信息。我需要将包标志信息传递给FFMPEGFrameRecorder.java:record()函数吗?或者我应该使用直接缓冲区?任何建议都将被感激!我应该用一个提示来弄清楚事情。

PS:我知道有些编解码器使用平面和其他半平面颜色格式。如果我过了这一关,这种区别就会出现。此外,我没有使用Surface到MediaCodec的方式,因为我需要支持API17,它需要比这条路由更多的更改,我认为这有助于我理解更基本的流程。阿根,我很感激任何建议。如果有什么需要澄清的请告诉我。

更新#1

所以在做了更多的测试之后,我看到我的编码器输出以下帧:

000000016742800DDA0507E806D0A1350000000168CE06E2
0000000165B840A6F1E7F0EA24000AE73BEB5F51CC7000233A84240...
0000000141E2031364E387FD4F9BB3D67F51CC7000279B9F9CFE811...
0000000141E40304423FFFFF0B7867F89FAFFFFFFFFFFCBE8EF25E6...
0000000141E602899A3512EF8AEAD1379F0650CC3F905131504F839...
...

第一帧包含SP和PPS。据我所知,这些只有一次传送。其余为NAL类型1和5。因此,我的假设是,为了让ffprobe不仅在流开始时看到流信息,我应该捕获SPS和PPS帧,并在一定数量的帧之后,或者在每个I帧之前,定期地重新发送它们。你觉得呢?

共有1个答案

彭弘方
2023-03-14

据我所知,你不需要复用视频和音频流。当您发送公告消息时,您还可以指定将音频和视频流发送到哪个端口。您需要将它们单独打包并用RTP发送。更多信息见维基百科。https://en.wikipedia.org/wiki/real_time_streaming_protocol

 类似资料:
  • 我正在开发一个通过RTP接收H264编码数据的应用程序,但我无法让Android的MediaCodec输出任何内容。我正在按照https://stackoverflow.com/a/7668578/10788248对RTP数据包进行解包 在编码帧被重新组装后,我将它们输入到出列的输入缓冲区中。 当我对输入缓冲区进行排队时,我不会得到任何错误,但是解码器的回调从来不会调用onOutputBuffer

  • 我的最终目标是对熊猫专栏进行热编码。在本例中,我想对一列“b”进行热编码,如下所示:保存苹果、香蕉和桔子,并将任何其他水果编码为“其他”。 示例:在下面的代码中,“葡萄柚”将被改写为“其他”,如果“猕猴桃”和“鳄梨”出现在我的数据中,它们也将被改写为“其他”。 以下代码有效: 我的问题是:有没有一种更短的方法来做业务?我尝试了

  • 我知道使用Wireshark和VLC保存RTP h264流是可能的。但为了学习更多关于视频流,我正在尝试自己做。有几个相关的问题有助于阅读这个主题: 如何处理原始UDP数据包,以便directshow源筛选器中的解码器筛选器对其进行解码 如何将H.264 UDP数据包转换为可播放的媒体流或文件(碎片整理) 以这些为背景,我目前的工作如下: 我可以通过UTP接收RTP数据包。 我按照上面问题中的讨论

  • 我阅读了H.264视频RFC的RTP有效载荷格式,如果我在视频流中发现有sps和pps数据包(元数据),然后是Idr(完整图像),然后在上一个Idr到当前状态之间改变数据包,再从一开始。 我知道每个包装h264数据的rtp包报头都有序列号。 我不明白的是,对于更改的数据包(在Idr数据包之间),他们如何知道与它们相关的每个Idr? 在h264报头/数据中是否有写入它们与哪个rtp序列号或h264序

  • 2)当我接收到一个数据包,并且它是“fragment_type==28”(不仅仅是这样,而是让我们认为我真的得到了一个片段),这意味着我有一个IDR片段。 3)每个报文都有一个由发信方生成的序列号,该序列号按如下顺序排列:如果报文a的序列号为20,则发信方发送的下一个报文为21,以此类推。 现在让我们来回答我的问题: A)如果我有一个IDR要重建,我如何知道什么数据包属于这个IDR?让我举一个例子

  • 我正在解码从Android上的wifi摄像头接收到的原始h264。 这是解码时产生的视频的一个例子,除了底部部分看起来很好。 我还注意到一些奇怪的事情,当我移动摄像机时,饲料似乎运行几乎完全流畅(底部没有垃圾),一旦我把它放下,垃圾视频返回(我会以为它是相反的方式...) 我正在将h264数据解析成以澳元开头的块,每个块以澳元开头,当另一个开始时结束。 我的理解是,每个解析的“块”(以AUD开头)