使用MediaCodec和MediaMuxer进行视频编码和复用

61
我正在开发一个应用程序,其中我解码视频并替换某些帧,然后使用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视频样本,在VLC上播放时会出现绿屏,并且在手机上播放时会出现不正确的情况,除非我滚动到开始位置,而现在在YouTube上奇怪地正常工作,只是在开头有一个绿色的帧。

这是一个720p视频样例,经过编辑后也有一个绿色的帧以及明显的像素化和延迟。

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

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()处理帧的时间太长了,并且你正在遇到多个错误场景,这些场景没有得到适当的处理。 - La Machine
请问您能否澄清一下?我是从extractor.getSampleTime();获取presentationTimeUs,而不是从系统时间获取,那么decode()处理时间会有什么影响呢? - Mohamed_AbdAllah
我现在无法给你一个答案。但是绿屏的问题在于第一帧完全为空,就像YUV缓冲区充满了0,所以它显示为绿色。你应该检查第一帧(绿色帧)的输出,找出为什么会发出空缓冲区的原因。 - Alan
有关工作示例,请参见bigflake上的DecodeEditEncode(http://bigflake.com/mediacodec/#DecodeEditEncodeTest)。它需要API 18(您无论如何都需要MediaMuxer),但使用Surfaces可以显着提高性能并避免特定于设备的YUV格式问题。不利的一面是,您需要使用一些OpenGL ES。 - fadden
1个回答

1
像素化问题很可能是由于帧时间戳错误导致的,因此请确保您的帧时间戳单调递增,并且在传递给MediaCodec和MediaMuxer时相同。在这种特定情况下,您只需要替换要替换的帧的数据,将其时间戳保留为原始流中的时间戳即可。
确保将位图转换为YUV颜色空间并使用正确的像素格式。Android将位图存储为RGBA,每个像素4个字节,您需要将其转换为具有每个像素的Y值和2x2块的U和V值,然后在进入编解码器的字节数组中将它们放置在单独的平面中。
此外,我之前制作了一个使用MediaCodec调整视频大小的示例应用程序,这也可能对您有所帮助:https://github.com/grishka/android-video-transcoder

我已经尝试手动输入时间戳(通过将视频时间除以帧数并逐帧增加此步骤),但结果相同。在将位图馈送给编码器之前,我还将其转换为YUV格式。 - Mohamed_AbdAllah
请注意,这些时间戳以微秒为单位(1/1000毫秒)。您可能正在使用错误的单位。 - Grishka

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接