使用 MediaCodec 和 MediaMuxer 编码和混合视频

Posted

技术标签:

【中文标题】使用 MediaCodec 和 MediaMuxer 编码和混合视频【英文标题】:Encoding and muxing video using MediaCodec and MediaMuxer 【发布时间】:2014-07-21 23:03:48 【问题描述】:

我正在开发一个应用程序,我在其中解码视频并替换某些帧并使用 MediaMuxerMediaCodec 重新编码。如果我不替换任何帧(我在下面解释的 1080p 视频除外),该应用程序可以正常工作,但是当我这样做时,替换后的帧会像素化并且视频不连贯。

另外,当我用 1920x1080 视频尝试我的应用时,我得到一个奇怪的输出,视频没有显示任何内容,直到我滚动到视频的开头,然后视频开始显示(但同样的问题之前提到过编辑后的像素化。

这是我如何配置我的编码器:

Video_format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, interval);
Video_format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
Video_format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
Video_format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 0);
int color_format=MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar;
Video_format.setInteger(MediaFormat.KEY_COLOR_FORMAT, color_format);

encoder.configure(Video_format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

所以总结一下,我有两个问题:

1- 像素化帧和修改帧后的断断续续的视频。

2- 除非我滚动到开头,否则 1920x1080 视频会损坏。

编辑

这是一个未经编辑的示例1080p video,当我在 VLC 上播放时会出现绿屏,并且在手机上播放不正确,除非我滚动开始,现在奇怪地在 YouTube 上正常工作,除了开始时的绿框

这是一个示例720p video 编辑,开始时还有一个绿框,并且在编辑后清晰像素化和滞后

这是我用来解码重新编码的代码:

do
  Bitmap b1;

  if(edited_frames.containsKey(extractor.getSampleTime()))
    b1=BitmapFactory.decodeFile(edited_frames.get(extractor.getSampleTime()));
  else
    b1=decode(extractor.getSampleTime(),Preview_width,Preview_Height);

  if(b1==null) continue;

  Bitmap b_scal=Bitmap.createScaledBitmap(b1, Preview_width, Preview_Height, false);
  if(b_scal==null) continue;
  encode(b_scal, encoder, muxer, videoTrackIndex);
  lastTime=extractor.getSampleTime();
while(extractor.advance());

解码方法:

private Bitmap decode(final long time,final int width,final int height)
  MediaFormat newFormat = codec.getOutputFormat();
  Bitmap b = null;
  final int TIMEOUT_USEC = 10000;
  ByteBuffer[] decoderInputBuffers = codec.getInputBuffers();
  MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();

  boolean outputDone = false;
  boolean inputDone = false;
  while (!outputDone) 
    if (!inputDone) 
      int inputBufIndex = codec.dequeueInputBuffer(TIMEOUT_USEC);
      if (inputBufIndex >= 0) 
        ByteBuffer inputBuf = decoderInputBuffers[inputBufIndex];

        int chunkSize = extractor.readSampleData(inputBuf, 0);
        if (chunkSize < 0) 
          codec.queueInputBuffer(inputBufIndex, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
          inputDone = true;
         else 
          long presentationTimeUs = extractor.getSampleTime();
          codec.queueInputBuffer(inputBufIndex, 0, chunkSize, presentationTimeUs, 0 );
        
        inputBuf.clear();
        decoderInputBuffers[inputBufIndex].clear();
       else 
      
    
    ByteBuffer[] outputBuffers;
    if (!outputDone) 
      int decoderStatus = codec.dequeueOutputBuffer(info, TIMEOUT_USEC);
      if (decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) 
       else if (decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) 
        outputBuffers = codec.getOutputBuffers();
       else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) 
        newFormat = codec.getOutputFormat();
       else if (decoderStatus < 0) 
       else  
        if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) 
          outputDone = true;
        

        boolean doRender = (info.size != 0);

        codec.releaseOutputBuffer(decoderStatus, false);
        if (doRender) 
          outputBuffers = codec.getOutputBuffers();
          ByteBuffer buffer = outputBuffers[decoderStatus];
          buffer = outputBuffers[decoderStatus];

          outputDone = true;

          byte[] outData = new byte[info.size];
          buffer.get(outData);
          buffer.clear();
          outputBuffers[decoderStatus].clear();
          try 
            int colr_format=-1;
            if(newFormat!=null && newFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT)==21)
              colr_format=ImageFormat.NV21;
            else if(newFormat!=null && newFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT)!=21)            
              Toast.makeText(getApplicationContext(), "Unknown color format "+format.getInteger(MediaFormat.KEY_COLOR_FORMAT), Toast.LENGTH_LONG).show();
              finish();
              return null;
            

            int[] arrrr=new int[format.getInteger(MediaFormat.KEY_WIDTH)* format.getInteger(MediaFormat.KEY_HEIGHT)];
            YUV_NV21_TO_RGB(arrrr, outData, format.getInteger(MediaFormat.KEY_WIDTH), format.getInteger(MediaFormat.KEY_HEIGHT));

            lastPresentationTimeUs = info.presentationTimeUs;

            b = Bitmap.createBitmap(arrrr, format.getInteger(MediaFormat.KEY_WIDTH), format.getInteger(MediaFormat.KEY_HEIGHT), Bitmap.Config.ARGB_8888);
           catch (Exception e) 
            e.printStackTrace();
          
        
      
    
  
  return b;

这里是编码方法:

private void encode(Bitmap b, MediaCodec encoder, MediaMuxer muxer, int track_indx)
  MediaCodec.BufferInfo enc_info = new MediaCodec.BufferInfo();
  boolean enc_outputDone = false;
  boolean enc_inputDone = false;

  final int TIMEOUT_USEC = 10000;

  ByteBuffer[] encoderInputBuffers = encoder.getInputBuffers();
  ByteBuffer[] enc_outputBuffers = encoder.getOutputBuffers();

  while (!enc_outputDone) 
    if (!enc_inputDone) 
      int inputBufIndex = encoder.dequeueInputBuffer(TIMEOUT_USEC);
      if (inputBufIndex >= 0) 
        ByteBuffer inputBuf = encoderInputBuffers[inputBufIndex];
        int chunkSize = 0;

        if(b==null)
        else
          int mWidth = b.getWidth();
          int mHeight = b.getHeight();

          byte [] yuv = new byte[mWidth*mHeight*3/2];
          int [] argb = new int[mWidth * mHeight];

          b.getPixels(argb, 0, mWidth, 0, 0, mWidth, mHeight);
          encodeYUV420SP(yuv, argb, mWidth, mHeight);

          b.recycle();
          b=null;
          inputBuf.put(yuv);
          chunkSize = yuv.length;
        

        if (chunkSize < 0) 
          encoder.queueInputBuffer(inputBufIndex, 0, 0, 0L,
                            MediaCodec.BUFFER_FLAG_END_OF_STREAM);
         else 
          long presentationTimeUs = extractor.getSampleTime();
          Log.i("Encode","Encode Time: "+presentationTimeUs);
          encoder.queueInputBuffer(inputBufIndex, 0, chunkSize, presentationTimeUs, 0);
          inputBuf.clear();

          encoderInputBuffers[inputBufIndex].clear();
          enc_inputDone=true;
        
      
    
    if (!enc_outputDone) 
      int enc_decoderStatus = encoder.dequeueOutputBuffer(enc_info, TIMEOUT_USEC);
      if (enc_decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) 
       else if (enc_decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) 
        enc_outputBuffers = encoder.getOutputBuffers();
       else if (enc_decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) 
        MediaFormat newFormat = encoder.getOutputFormat();
       else if (enc_decoderStatus < 0) 
       else  
        if ((enc_info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) 
          enc_outputDone = true;
      

      boolean enc_doRender = (enc_info.size != 0);
      encoder.releaseOutputBuffer(enc_decoderStatus, false);
      if (enc_doRender) 
        enc_outputDone = true;
        ByteBuffer enc_buffer = enc_outputBuffers[enc_decoderStatus];

        try 
          muxer.writeSampleData(track_indx, enc_buffer, enc_info);
         catch (Exception e) 
          e.printStackTrace();
        
        enc_buffer.clear();
        enc_outputBuffers[enc_decoderStatus].clear();
      
    
  

【问题讨论】:

您能否在 decode() 方法中的所有代码流路径(带有微秒精度时间戳)中添加调试打印并在此处发布输出?我感觉 decode() 处理帧花费了太多时间,并且您在这里遇到了多个错误场景,这些场景没有得到正确处理。 你能澄清一下吗?我从extractor.getSampleTime(); 得到presentationTimeUs 而不是系统时间,那么decode() 处理时间的影响是什么? 我现在无法回答你。但是绿屏是第一帧是整个空的,就像YUV缓冲区充满了0,所以它显示为绿色。您应该检查第一帧(绿框)的输出以找出空缓冲区发出的原因。 有关工作示例,请参阅 Bigflake 上的 DecodeEditEncode (bigflake.com/mediacodec/#DecodeEditEncodeTest)。它需要 API 18(MediaMuxer 无论如何都需要它),但使用 Surfaces 操作会显着提高性能并避免特定于设备的 YUV 格式问题。不利的一面是,您需要使用一点 OpenGL ES。 【参考方案1】:

像素化很可能是由于错误的帧时间戳造成的,因此请确保帧的时间戳单调增加,并且在将它们传递给 MediaCodec 和 MediaMuxer 时相同。在这种特定情况下,您只需替换要替换的帧的数据,使其时间戳保持在原始流中。

确保将位图转换为 YUV 颜色空间并且使用正确的像素格式。 android 将位图存储在 RGBA 中,每个像素 4 个字节,您需要将其转换为 YUV,每个像素的 Y 值和 2x2 块的 U 和 V 值,然后将它们放置在进入字节数组的单独平面中编解码器。

另外,前段时间我制作了一个使用 MediaCodec 调整视频大小的示例应用程序,它也可能对您有所帮助:https://github.com/grishka/android-video-transcoder

【讨论】:

我尝试手动输入时间戳(通过将视频时间除以帧数并按此步骤为每一帧递增),我得到了相同的结果。在将位图输入编码器之前,我也会将位图转换为 YUV。 请记住,这些时间戳以微秒为单位(1/1000 毫秒)。您可能使用了错误的单位。

以上是关于使用 MediaCodec 和 MediaMuxer 编码和混合视频的主要内容,如果未能解决你的问题,请参考以下文章

Android媒体解码MediaCodec MediaExtractor学习

使用 MediaCodec 和 MediaMuxer 将图像转换为视频

在 Android 上使用 AudioRecord 和 MediaCodec 编码 AAC 音频

使用 MediaCodec 和 MediaMuxer 录制视频,但码率和帧率不正确

如何在 MediaCodec 编码器和 CameraX 之间共享 Surface

MediaCodec 硬编码