Search code examples
androidvideoandroid-mediacodec

Transcoding with MediaCodec results in green and messed up video


I am trying to reduce video file size by decoding and re-encoding the video with MediaCodec. However, the new video file generated by the muxer is all messed up. Can you tell me what I've been doing wrong?

My code is modified from this CTS with two major differences:

  1. I am using codecs in async mode with callbacks.
  2. Instead of using helper classes, I pass the surface generated by encoder.createInputSurface() directly to decoder.configure().

I tried to decode the video into a SurfaceView by passing surfaceView.getHolder().getSurface() to decoder.configure(), and the video played correctly. I think this might be something wrong on the encoder/muxer part.

Here is my code:

The decoder callback:

private class VideoDecodeCallBack extends MediaCodec.Callback {

    @Override
    public void onInputBufferAvailable(MediaCodec codec, int index) {
        ByteBuffer buffer = codec.getInputBuffer(index);
        if (extractorDone) {
            return;
        }

        int size = videoExtractor.readSampleData(buffer, 0);
        long pts = videoExtractor.getSampleTime();
        if (size >= 0) {
            codec.queueInputBuffer(index, 0, size, pts, videoExtractor.getSampleFlags());
        }
        extractorDone = !videoExtractor.advance();
        if (extractorDone) {
            codec.queueInputBuffer(index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
        }
    }

    @Override
    public void onOutputBufferAvailable(
            MediaCodec codec, int index, MediaCodec.BufferInfo info) {
        if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
            codec.releaseOutputBuffer(index, false);
            return;
        }

        boolean render = info.size != 0;
        codec.releaseOutputBuffer(index, render);

        if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
            videoEncoder.signalEndOfInputStream();
        }

    }

    @Override
    public void onError(
            MediaCodec codec, MediaCodec.CodecException e) {

    }

    @Override
    public void onOutputFormatChanged(
            MediaCodec codec, MediaFormat format) {
    }

The encoder callback:

private class VideoEncodeCallBack extends MediaCodec.Callback {

    @Override
    public void onInputBufferAvailable(MediaCodec codec, int index) {
    }

    @Override
    public void onOutputBufferAvailable(
            MediaCodec codec, int index, MediaCodec.BufferInfo info) {
        ByteBuffer buffer = codec.getOutputBuffer(index);

        if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
            codec.releaseOutputBuffer(index, false);
            return;
        }

        if (info.size != 0) {
            muxer.writeSampleData(outputVideoTrack, buffer, info);
        }

        if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
            muxer.stop();
            muxer.release();
        }

        codec.releaseOutputBuffer(index, false);
    }

    @Override
    public void onError(
            MediaCodec codec, MediaCodec.CodecException e) {

    }

    @Override
    public void onOutputFormatChanged(
            MediaCodec codec, MediaFormat format) {
        outputVideoTrack = muxer.addTrack(format);
        muxer.start();
    }
}

How I configure the codecs:

public void configure(String input, String output) throws IOException {
    // Extractors
    videoExtractor = new MediaExtractor();
    videoExtractor.setDataSource(input);

    int videoTrack = getAndSelectVideoTrackIndex(videoExtractor);

    MediaFormat videoFormat = videoExtractor.getTrackFormat(videoTrack);
    String mime = videoFormat.getString(MediaFormat.KEY_MIME);

    // Codecs
    videoDecoder = MediaCodec.createDecoderByType(mime);
    videoEncoder = MediaCodec.createEncoderByType(mime);

    videoDecoder.setCallback(new VideoDecodeCallBack(), videoDecodeThreadHandler);
    videoEncoder.setCallback(new VideoEncodeCallBack(), videoEncodeThreadHandler);

    videoEncoder.configure(createOutputFormat(), null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
    Surface surface = videoEncoder.createInputSurface();
    videoDecoder.configure(videoFormat, surface, null, 0);

    // Muxer
    muxer = new MediaMuxer(output, OutputFormat.MUXER_OUTPUT_MPEG_4);
}

The desired output format:

private static MediaFormat createOutputFormat() {
    MediaFormat result = MediaFormat.createVideoFormat("video/avc", 540, 960);
    result.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
    result.setInteger(MediaFormat.KEY_BIT_RATE, 1300000);
    result.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
    result.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
    return result;
}

The complete code is here


Solution

  • I had this issue too. My guess is that Android cannot rescale the frame when the decoder render directly to the encoder's input surface.

    The solution can be to use openGL for the rescaling.

    This project does that. https://github.com/hoolrory/AndroidVideoSamples/blob/master/CommonVideoLibrary/src/com/roryhool/commonvideolibrary/VideoResampler.java

    (I didn't run this project but I have implemented something similar so it should work)