塞尚《在诺曼底农场》
这个公众号会路线图式的遍历分享音视频技术音视频基础(完成) → 音视频工具(完成) → 音视频工程示例(进行中) → 音视频工业实战(准备)关注一下成本不高,错过干货损失不小 ↓↓↓

iOS/Android 客户端开发同学如果想要开始学习音视频开发,最丝滑的方式是对音视频基础概念知识有一定了解后,再借助 iOS/Android 平台的音视频能力上手去实践音视频的采集 → 编码 → 封装 → 解封装 → 解码 → 渲染过程,并借助音视频工具来分析和理解对应的音视频数据。
音视频工程示例这个栏目,我们将通过拆解采集 → 编码 → 封装 → 解封装 → 解码 → 渲染流程并实现 Demo 来向大家介绍如何在 iOS/Android 平台上手音视频开发。
这里是 Android 第十一篇:Android 视频转封装 Demo。这个 Demo 里包含以下内容:
  • 1)实现一个音视频解封装模块;
  • 2)实现一个音视频封装模块;
  • 3)实现对 MP4 文件中音视频的解封装逻辑,将解封装后的音视频编码数据重新封装存储为一个新的 MP4 文件;
  • 4)详尽的代码注释,帮你理解代码逻辑和原理。
在本文中,我们将详解一下 Demo 的具体实现和源码。读完本文内容相信就能帮你掌握相关知识。
不过,如果你的需求是:1)直接获得全部工程源码;2)想进一步咨询音视频技术问题;3)咨询音视频职业发展问题。可以根据自己的需要考虑是否加入『关键帧的音视频开发圈』。
长按识别二维码→加入我们

1、音视频解封装模块

视频编码模块即 KFMP4Demuxer,复用了《Android 音频解封装 Demo》中介绍的 demuxer,这里就不再重复介绍了,其接口如下:
KFMP4Demuxer.java
publicclassKFMP4Demuxer
{

publicKFMP4Demuxer(KFDemuxerConfig config, KFDemuxerListener listener)
///< 构造方法:配置 & 回调。
publicvoidrelease()
///< 释放解封装器实例。
publicbooleanhasVideo()
///< 是否包含视频。
publicbooleanhasAudio()
///< 是否包含音频。
publicintduration()
///< 文件时长。
publicintrotation()
///< 视频旋转角度。
publicbooleanisHEVC()
///< 是否为 H265。
publicintwidth()
///< 视频宽度。
publicintheight()
///< 视频高度。
publicintsamplerate()
///< 音频采样率。
publicintchannel()
///< 音频声道数。
publicintaudioProfile()
///< 音频 profile。
publicintvideoProfile()
///< 视频 profile。
public MediaFormat audioMediaFormat()
///< 音频格式描述。
public MediaFormat videoMediaFormat()
//< 视频格式描述。
public ByteBuffer readAudioSampleData(MediaCodec.BufferInfo bufferInfo)
///< 读取音频帧。
public ByteBuffer readVideoSampleData(MediaCodec.BufferInfo bufferInfo)
///< 读取视频帧。
}

2、音视频封装模块

视频编码模块即 KFMP4Muxer,复用了《Android 音频封装 Demo》中介绍的 muxer,这里就不再重复介绍了,其接口如下:
KFMP4Muxer.java
publicclassKFMP4Muxer
{

publicKFMP4Muxer(KFMuxerConfig config, KFMuxerListener listener)
///< 构造方法,配置、回调。
publicvoidstart()
///< 开始。
publicvoidstop()
///< 关闭。
publicvoidsetVideoMediaFormat(MediaFormat mediaFormat)
///< 设置音频描述。
publicvoidsetAudioMediaFormat(MediaFormat mediaFormat)
///< 设置视频描述。
publicvoidwriteSampleData(boolean isVideo, ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo)
///< 写入音视频数据(编码后数据)。
publicvoidrelease()
///< 清理。
}

3、音视频转封装逻辑

我们还是在一个 MainActivity 中来实现对 MP4 文件中音视频的解封装逻辑,然后将解封装后的音视频编码数据重新封装存储为一个新的 MP4 文件。
MainActivity.java
publicclassMainActivityextendsAppCompatActivity
{

private
 KFMP4Demuxer mDemuxer; 
///< 解封装器。
private
 KFDemuxerConfig mDemuxerConfig; 
///< 解封装配置。
private
 KFMP4Muxer mMuxer; 
///< 封装器。
private
 KFMuxerConfig mMuxerConfig; 
///< 封装配置。

@RequiresApi
(api = Build.VERSION_CODES.LOLLIPOP)

@Override
protectedvoidonCreate(Bundle savedInstanceState)
{

super
.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);


///< 申请采集存储权限。
if
 (ActivityCompat.checkSelfPermission(
this
, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(
this
, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED ||

                ActivityCompat.checkSelfPermission(
this
, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||

                ActivityCompat.checkSelfPermission(
this
, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {

            ActivityCompat.requestPermissions((Activity) 
this
,

new
 String[] {Manifest.permission.CAMERA,Manifest.permission.RECORD_AUDIO, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE},

1
);

        }


///< 解封装配置,控制仅输出视频。
        mDemuxerConfig = 
new
 KFDemuxerConfig();

        mDemuxerConfig.path = Environment.getExternalStorageDirectory().getPath() + 
"/2.mp4"
;

        mDemuxerConfig.demuxerType = KFMediaBase.KFMediaType.KFMediaAV;


///< 创建封装配置。
        mMuxerConfig = 
new
 KFMuxerConfig(Environment.getExternalStorageDirectory().getPath() + 
"/test.mp4"
);


        FrameLayout.LayoutParams startParams = 
new
 FrameLayout.LayoutParams(
200
120
);

        startParams.gravity = Gravity.CENTER_HORIZONTAL;

        Button startButton = 
new
 Button(
this
);

        startButton.setTextColor(Color.BLUE);

        startButton.setText(
"开始"
);

        startButton.setVisibility(View.VISIBLE);

        startButton.setOnClickListener(
new
 View.OnClickListener() {

@Override
publicvoidonClick(View view)
{

///< 创建解封装与封装。
if
 (mDemuxer == 
null
) {

                    mDemuxer = 
new
 KFMP4Demuxer(mDemuxerConfig,mDemuxerListener);

                    mMuxer = 
new
 KFMP4Muxer(mMuxerConfig,mMuxerListener);

                    mMuxer.start();

///< 设置格式描述。
                    mMuxer.setVideoMediaFormat(mDemuxer.videoMediaFormat());

                    mMuxer.setAudioMediaFormat(mDemuxer.audioMediaFormat());


///< 循环读取音视频数据写入封装器。
                    MediaCodec.BufferInfo videoBufferInfo = 
new
 MediaCodec.BufferInfo();

                    ByteBuffer videoNextBuffer = mDemuxer.readVideoSampleData(videoBufferInfo);


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

                    ByteBuffer audioNextBuffer = mDemuxer.readAudioSampleData(audioBufferInfo);

while
 (audioNextBuffer != 
null
 || videoNextBuffer != 
null
) {

if
 (audioNextBuffer != 
null
) {

                            mMuxer.writeSampleData(
false
,audioNextBuffer,audioBufferInfo);

                            audioNextBuffer = mDemuxer.readAudioSampleData(audioBufferInfo);

                        }


if
 (videoNextBuffer != 
null
) {

                            mMuxer.writeSampleData(
true
,videoNextBuffer,videoBufferInfo);

                            videoNextBuffer = mDemuxer.readVideoSampleData(videoBufferInfo);

                        }

                    }

                    mMuxer.stop();

                    Log.i(
"KFDemuxer"
,
"complete"
);

                }

            }

        });

        addContentView(startButton, startParams);

    }


private
 KFDemuxerListener mDemuxerListener = 
new
 KFDemuxerListener() {

@Override
///< 解封装出错回调。
publicvoiddemuxerOnError(int error, String errorMsg)
{

            Log.i(
"KFDemuxer"
,
"error"
 + error + 
"msg"
 + errorMsg);

        }

    };


private
 KFMuxerListener mMuxerListener = 
new
 KFMuxerListener() {

@Override
///< 封装器出错回调。
publicvoidmuxerOnError(int error, String errorMsg)
{

            Log.e(
"KFMuxer"
,
"error:"
 + error + 
"msg:"
 +errorMsg);

        }

    };

}

上面是 MainActivity 的实现,其中主要包含这几个部分:
  • 1)设置好待解封装的资源。
    • mDemuxerConfig 中实现,我们这里是一个 MP4 文件。
  • 2)启动封装器。
    • start 中实现。
    • 设置音视频格式描述。
  • 3)读取解封装后的音视频编码数据并送给封装器进行重新封装。
    • onClick 中实现。

4、用工具播放 MP4 文件

完成 Demo 后,可以将 sdcard 文件夹下面的 test.mp4 文件拷贝到电脑上,使用 ffplay 播放来验证一下效果是否符合预期:
$ ffplay -i test.mp4

我们还可以用《可视化音视频分析工具》第 3.1 节 MP4Box.js 等工具来查看它的格式。

- 完 -

推荐阅读
《Android AVDemo(10):视频解封装》
《Android AVDemo(9):视频封装》
《Android AVDemo(8):视频编码》
《Android AVDemo(7):视频采集》
《Android AVDemo(6):音频渲染》
《Android AVDemo(5):音频解码》
《Android AVDemo(4):音频解封装》
《Android AVDemo(3):音频封装》
《Android AVDemo(2):音频编码》
《Android AVDemo(1):音频采集》
《iOS AVDemo(7):视频采集》
《iOS 音频处理框架及重点 API 合集》
《iOS AVDemo(6):音频渲染》
《iOS AVDemo(5):音频解码》
《iOS AVDemo(4):音频解封装》
《iOS AVDemo(3):音频封装》
《iOS AVDemo(2):音频编码》
《iOS AVDemo(1):音频采集》
加我微信,拉你入群
谢谢看完全文,也点一下『赞』和『在看』吧 ↓
继续阅读
阅读原文