vx 搜索『gjzkeyframe』 關(guān)注『關(guān)鍵幀Keyframe』來及時(shí)獲得最新的音視頻技術(shù)文章乖酬。
iOS/Android 客戶端開發(fā)同學(xué)如果想要開始學(xué)習(xí)音視頻開發(fā)于未,最絲滑的方式是對音視頻基礎(chǔ)概念知識有一定了解后弱贼,再借助 iOS/Android 平臺的音視頻能力上手去實(shí)踐音視頻的采集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染
過程蒸苇,并借助音視頻工具來分析和理解對應(yīng)的音視頻數(shù)據(jù)。
在音視頻工程示例這個(gè)欄目吮旅,我們將通過拆解采集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染
流程并實(shí)現(xiàn) Demo 來向大家介紹如何在 iOS/Android 平臺上手音視頻開發(fā)溪烤。
這里是 Android 第八篇:Android 視頻編碼 Demo。這個(gè) Demo 里包含以下內(nèi)容:
- 1)實(shí)現(xiàn)一個(gè)視頻采集模塊庇勃;
- 2)實(shí)現(xiàn)兩個(gè)視頻編碼模塊
ByteBuffer
檬嘀、Surface
,支持 H.264/H.265责嚷; - 3)串聯(lián)視頻采集和編碼模塊鸳兽,將采集到的視頻數(shù)據(jù)輸入給編碼模塊進(jìn)行編碼,并存儲為文件罕拂;
- 4)詳盡的代碼注釋贸铜,幫你理解代碼邏輯和原理。
在本文中聂受,我們將詳解一下 Demo 的具體實(shí)現(xiàn)和源碼蒿秦。讀完本文內(nèi)容相信就能幫你掌握相關(guān)知識。
不過蛋济,如果你的需求是:1)直接獲得全部工程源碼棍鳖;2)想進(jìn)一步咨詢音視頻技術(shù)問題;3)咨詢音視頻職業(yè)發(fā)展問題碗旅《纱Γ可以根據(jù)自己的需要考慮是否加入『關(guān)鍵幀的音視頻開發(fā)圈』。
想要了解視頻編碼祟辟,可以看看這幾篇:
1医瘫、視頻采集模塊
在這個(gè) Demo 中,視頻采集模塊 KFVideoCapture
的實(shí)現(xiàn)與《Android 視頻采集 Demo》中一樣旧困,這里就不再重復(fù)介紹了醇份,其接口如下:
KFIVideoCapture.java
public interface KFIVideoCapture {
///< 視頻采集初始化。
public void setup(Context context, KFVideoCaptureConfig config, KFVideoCaptureListener listener, EGLContext eglShareContext);
///< 釋放采集實(shí)例吼具。
public void release();
///< 開始采集僚纷。
public void startRunning();
///< 關(guān)閉采集。
public void stopRunning();
///< 是否正在采集拗盒。
public boolean isRunning();
///< 獲取 OpenGL 上下文怖竭。
public EGLContext getEGLContext();
///< 切換攝像頭。
public void switchCamera();
}
2陡蝇、視頻 ByteBuffer 編碼模塊
在實(shí)現(xiàn)視頻編碼模塊之前痊臭,我們先實(shí)現(xiàn)一個(gè)視頻編碼配置類 KFVideoEncoderConfig
:
KFVideoEncoderConfig.java
public class KFVideoEncoderConfig {
public Size size = new Size(720,1280);
public int bitrate = 4 * 1024 * 1024;
public int fps = 30;
public int gop = 30 * 4;
public boolean isHEVC = false;
public int profile = MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline;
public int profileLevel = MediaCodecInfo.CodecProfileLevel.AVCLevel1;
public KFVideoEncoderConfig() {
}
}
這里可以配置各種編碼參數(shù)哮肚,但不同機(jī)型支持能力不同,通過此配置生成編碼格式描述 MediaFormat
广匙。
接下來绽左,我們來實(shí)現(xiàn)一個(gè)視頻編碼模塊 KFByteBufferCodec
,編碼模塊 KFByteBufferCodec
的實(shí)現(xiàn)與 《Android 音頻編碼 Demo》 中一樣艇潭,這里就不再重復(fù)介紹了拼窥,其接口如下
KFMediaCodecInterface.java
public interface KFMediaCodecInterface {
public static final int KFMediaCodecInterfaceErrorCreate = -2000;
public static final int KFMediaCodecInterfaceErrorConfigure = -2001;
public static final int KFMediaCodecInterfaceErrorStart = -2002;
public static final int KFMediaCodecInterfaceErrorDequeueOutputBuffer = -2003;
public static final int KFMediaCodecInterfaceErrorParams = -2004;
public static int KFMediaCodeProcessParams = -1;
public static int KFMediaCodeProcessAgainLater = -2;
public static int KFMediaCodeProcessSuccess = 0;
///< 初始化 Codec,第一個(gè)參數(shù)需告知使用編碼還是解碼蹋凝。
public void setup(boolean isEncoder,MediaFormat mediaFormat, KFMediaCodecListener listener, EGLContext eglShareContext);
///< 釋放 Codec鲁纠。
public void release();
///< 獲取輸出格式描述。
public MediaFormat getOutputMediaFormat();
///< 獲取輸入格式描述鳍寂。
public MediaFormat getInputMediaFormat();
///< 處理每一幀數(shù)據(jù)改含,編碼前與編碼后都可以,支持編解碼 2 種模式迄汛。
public int processFrame(KFFrame frame);
///< 清空 Codec 緩沖區(qū)捍壤。
public void flush();
}
上面是 KFByteBufferCodec
接口的設(shè)計(jì),與音頻編碼對比區(qū)別如下:
- 1)音頻編碼使用了繼承類
KFAudioByteBufferEncoder
鞍爱,視頻編碼則直接使用類KFByteBufferCodec
鹃觉。 - 音頻編碼使用了繼承類
KFByteBufferCodec
,目的是切割合適大小的數(shù)據(jù)2048
送入編碼器,因?yàn)?AAC 數(shù)據(jù)編碼每幀大小為1024 * 2(位深 16 Bit)
睹逃。 - 視頻編碼使用了類
KFByteBufferCodec
盗扇。
- 音頻編碼使用了繼承類
- 2)外層使用構(gòu)造方法時(shí)配置參數(shù)修改:
-
setup
接口mInputMediaFormat
需要設(shè)置視頻編碼的格式描述。
-
更具體細(xì)節(jié)見上述代碼及其注釋沉填。
3疗隶、視頻 Surface 編碼模塊
接下來,我們來實(shí)現(xiàn)一個(gè)視頻編碼模塊 KFVideoSurfaceEncoder
翼闹,在這里輸入采集后的數(shù)據(jù)斑鼻,輸出編碼后的數(shù)據(jù),同樣也需要實(shí)現(xiàn)接口 KFMediaCodecInterface
猎荠,參考模塊 KFByteBufferCodec
坚弱。
KFVideoSurfaceEncoder.java
public class KFVideoSurfaceEncoder implements KFMediaCodecInterface {
private static final String TAG = "KFVideoSurfaceEncoder";
private KFMediaCodecListener mListener = null; ///< 回調(diào)。
private KFGLContext mEGLContext = null; ///< GL 上下文法牲。
private KFGLFilter mFilter = null; ///< 渲染到 Surface 特效史汗。
private MediaCodec mEncoder = null; ///< 編碼器。
private Surface mSurface = null; ///< 渲染 Surface 緩存拒垃。
private HandlerThread mEncoderThread = null; ///< 編碼線程。
private Handler mEncoderHandler = null;
private Handler mMainHandler = new Handler(Looper.getMainLooper()); ///< 主線程瓷蛙。
private MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
private long mLastInputPts = 0;
private MediaFormat mOutputFormat = null; ///< 輸出格式描述悼瓮。
private MediaFormat mInputFormat = null; ///< 輸入格式描述戈毒。
public KFVideoSurfaceEncoder() {
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public void setup(boolean isEncoder,MediaFormat mediaFormat, KFMediaCodecListener listener, EGLContext eglShareContext) {
mInputFormat = mediaFormat;
mListener = listener;
mEncoderThread = new HandlerThread("KFSurfaceEncoderThread");
mEncoderThread.start();
mEncoderHandler = new Handler((mEncoderThread.getLooper()));
mEncoderHandler.post(()->{
if (mInputFormat == null) {
_callBackError(KFMediaCodecInterfaceErrorParams,"mInputFormat == null");
return;
}
///< 初始化編碼器。
boolean setupSuccess = _setupEnocder();
if (setupSuccess) {
mEGLContext = new KFGLContext(eglShareContext,mSurface);
mEGLContext.bind();
///< 初始化特效横堡,用于紋理渲染到編碼器 Surface 上埋市。
_setupFilter();
mEGLContext.unbind();
}
});
}
@Override
public MediaFormat getOutputMediaFormat() {
return mOutputFormat;
}
@Override
public MediaFormat getInputMediaFormat() {
return mInputFormat;
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public void release() {
mEncoderHandler.post(()->{
///< 釋放編碼器。
if (mEncoder != null) {
try {
mEncoder.stop();
mEncoder.release();
} catch (Exception e) {
Log.e(TAG, "release: " + e.toString());
}
mEncoder = null;
}
///< 釋放 GL 特效上下文命贴。
if (mEGLContext != null) {
mEGLContext.bind();
if (mFilter != null) {
mFilter.release();
mFilter = null;
}
mEGLContext.unbind();
mEGLContext.release();
mEGLContext = null;
}
///< 釋放 Surface 緩存道宅。
if (mSurface != null) {
mSurface.release();
mSurface = null;
}
mEncoderThread.quit();
});
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public int processFrame(KFFrame inputFrame) {
if (inputFrame == null || mEncoderHandler == null) {
return KFMediaCodeProcessParams;
}
KFTextureFrame frame = (KFTextureFrame)inputFrame;
mEncoderHandler.post(()-> {
if (mEncoder != null && mEGLContext != null) {
if (frame.isEnd) {
///< 最后一幀標(biāo)記。
mEncoder.signalEndOfInputStream();
} else {
///< 最近一幀時(shí)間戳胸蛛。
mLastInputPts = frame.usTime();
mEGLContext.bind();
///< 渲染紋理到編碼器 Surface 設(shè)置視口污茵。
GLES20.glViewport(0, 0, frame.textureSize.getWidth(), frame.textureSize.getHeight());
mFilter.render(frame);
///< 設(shè)置時(shí)間戳。
mEGLContext.setPresentationTime(frame.usTime() * 1000);
mEGLContext.swapBuffers();
mEGLContext.unbind();
///< 獲取編碼后的數(shù)據(jù)葬项,盡量拿出最多的數(shù)據(jù)出來泞当,回調(diào)給外層。
long outputDts = -1;
while (outputDts < mLastInputPts){
int bufferIndex = 0;
try {
bufferIndex = mEncoder.dequeueOutputBuffer(mBufferInfo, 10 * 1000);
} catch (Exception e) {
Log.e(TAG, "Unexpected MediaCodec exception in dequeueOutputBufferIndex, " + e);
_callBackError(KFMediaCodecInterfaceErrorDequeueOutputBuffer,e.getMessage());
return;
}
if (bufferIndex >= 0) {
ByteBuffer byteBuffer = mEncoder.getOutputBuffer(bufferIndex);
if (byteBuffer != null) {
outputDts = mBufferInfo.presentationTimeUs;
if (mListener != null) {
KFBufferFrame encodeFrame = new KFBufferFrame();
encodeFrame.buffer = byteBuffer;
encodeFrame.bufferInfo = mBufferInfo;
mListener.dataOnAvailable(encodeFrame);
}
} else {
break;
}
try {
mEncoder.releaseOutputBuffer(bufferIndex, false);
} catch (Exception e) {
Log.e(TAG, e.toString());
return;
}
} else {
if (bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
mOutputFormat = mEncoder.getOutputFormat();
}
break;
}
}
}
}
});
return KFMediaCodeProcessSuccess;
}
@Override
public void flush() {
mEncoderHandler.post(()-> {
///< 刷新緩沖區(qū)民珍。
if (mEncoder != null) {
try {
mEncoder.flush();
} catch (Exception e) {
Log.e(TAG, "flush error!" + e);
}
}
});
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private boolean _setupEnocder() {
///< 初始化編碼器襟士。
try {
String mimeType = mInputFormat.getString(MediaFormat.KEY_MIME);
mEncoder = MediaCodec.createEncoderByType(mimeType);
mEncoder.configure(mInputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
} catch (IOException e) {
Log.e(TAG, "createEncoderByType" + e);
_callBackError(KFMediaCodecInterfaceErrorCreate,e.getMessage());
return false;
}
///< 創(chuàng)建 Surface。
mSurface = mEncoder.createInputSurface();
///< 開啟編碼器嚷量。
try {
mEncoder.start();
} catch (Exception e) {
Log.e(TAG, "start" + e );
_callBackError(KFMediaCodecInterfaceErrorStart,e.getMessage());
return false;
}
return true;
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void _setupFilter() {
///< 創(chuàng)建渲染模塊陋桂,渲染到編碼器 Surface。
if (mFilter == null) {
mFilter = new KFGLFilter(true, KFGLBase.defaultVertexShader,KFGLBase.defaultFragmentShader);
}
}
private void _callBackError(int error, String errorMsg){
///< 出錯(cuò)回調(diào)蝶溶。
if (mListener != null) {
mMainHandler.post(()->{
mListener.onError(error,TAG + errorMsg);
});
}
}
}
上面是 KFVideoSurfaceEncoder
的實(shí)現(xiàn)章喉,與視頻編碼 KFByteBufferCodec
對比區(qū)別如下:
- 1)數(shù)據(jù)源輸入不同。
-
KFByteBufferCodec
輸入為 YUV 數(shù)據(jù)KFBufferFrame
身坐。 -
KFVideoSurfaceEncoder
輸入為紋理數(shù)據(jù)KFTextureFrame
秸脱。
-
- 2)編碼流水線不同。
-
KFByteBufferCodec
輸入 YUV 數(shù)據(jù)進(jìn)行編碼部蛇。 -
KFVideoSurfaceEncoder
輸入為紋理數(shù)據(jù)摊唇,執(zhí)行 OpenGL 渲染,將紋理渲染到編碼器緩存mSurface
涯鲁。使用mFilter.render
進(jìn)行渲染巷查,同時(shí)設(shè)置時(shí)間戳setPresentationTime
,交換前后臺緩沖區(qū)swapBuffers
抹腿,將紋理數(shù)據(jù)刷新到了mSurface
岛请。最后取出編碼后數(shù)據(jù),需要注意releaseOutputBuffer
方法第 2 個(gè)參數(shù)render
設(shè)置為 true警绩。
-
- 3)使用場景不同崇败。
-
KFVideoSurfaceEncoder
適用于輸入數(shù)據(jù)為紋理的情況,例如采集后添加特效。 -
KFByteBufferCodec
適用于非紋理數(shù)據(jù)后室,例如游戲直播缩膝、錄屏直播、圖片轉(zhuǎn)視頻等輸入數(shù)據(jù)為 ByteBuffer岸霹,此時(shí)沒必要再做數(shù)據(jù)轉(zhuǎn)換疾层。
-
更具體細(xì)節(jié)見上述代碼及其注釋。
4贡避、采集視頻數(shù)據(jù)進(jìn)行 H.264/H.265 編碼和存儲
我們在一個(gè) MainActivity
中來實(shí)現(xiàn)視頻采集及編碼邏輯痛黎,因?yàn)?Android 編碼的默認(rèn)輸出 AnnexB 碼流格式,所以這里不需要轉(zhuǎn)換刮吧。
MainActivity.java
public class MainActivity extends AppCompatActivity {
private KFIVideoCapture mCapture; ///< 采集器湖饱。
private KFVideoCaptureConfig mCaptureConfig; ///< 采集配置。
private KFRenderView mRenderView; ///< 渲染視圖皇筛。
private KFGLContext mGLContext; ///< OpenGL 上下文琉历。
private KFVideoEncoderConfig mEncoderConfig; ///< 編碼配置。
private KFMediaCodecInterface mEncoder; ///< 編碼水醋。
private FileOutputStream mStream = null;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
protected void onCreate(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);
}
///< 創(chuàng)建 GL 上下文旗笔。
mGLContext = new KFGLContext(null);
///< 創(chuàng)建渲染視圖。
mRenderView = new KFRenderView(this,mGLContext.getContext());
WindowManager windowManager = (WindowManager)this.getSystemService(this.WINDOW_SERVICE);
Rect outRect = new Rect();
windowManager.getDefaultDisplay().getRectSize(outRect);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(outRect.width(), outRect.height());
addContentView(mRenderView,params);
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
public void onClick(View view) {
///< 創(chuàng)建編碼器拄踪。
if (mEncoder == null) {
mEncoder = new KFVideoSurfaceEncoder();
MediaFormat mediaFormat = KFAVTools.createVideoFormat(mEncoderConfig.isHEVC,mEncoderConfig.size, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface,mEncoderConfig.bitrate,mEncoderConfig.fps,mEncoderConfig.gop / mEncoderConfig.fps,mEncoderConfig.profile,mEncoderConfig.profileLevel);
mEncoder.setup(true,mediaFormat,mVideoEncoderListener,mGLContext.getContext());
((Button)view).setText("停止");
} else {
mEncoder.release();
mEncoder = null;
((Button)view).setText("開始");
}
}
});
addContentView(startButton, startParams);
///< 創(chuàng)建采集器蝇恶。
mCaptureConfig = new KFVideoCaptureConfig();
mCaptureConfig.cameraFacing = LENS_FACING_FRONT;
mCaptureConfig.resolution = new Size(720,1280);
mCaptureConfig.fps = 30;
boolean useCamera2 = false;
if (useCamera2) {
mCapture = new KFVideoCaptureV2();
} else {
mCapture = new KFVideoCaptureV1();
}
mCapture.setup(this,mCaptureConfig,mVideoCaptureListener,mGLContext.getContext());
mCapture.startRunning();
mEncoderConfig = new KFVideoEncoderConfig();
}
private KFVideoCaptureListener mVideoCaptureListener = new KFVideoCaptureListener() {
@Override
public void cameraOnOpened(){}
@Override
public void cameraOnClosed() {
}
@Override
public void cameraOnError(int error,String errorMsg) {
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public void onFrameAvailable(KFFrame frame) {
///< 采集數(shù)據(jù)回調(diào),進(jìn)入編碼器惶桐。
mRenderView.render((KFTextureFrame) frame);
if (mEncoder != null) {
mEncoder.processFrame(frame);
}
}
};
private KFMediaCodecListener mVideoEncoderListener = new KFMediaCodecListener() {
@Override
public void onError(int error, String errorMsg) {
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
@Override
public void dataOnAvailable(KFFrame frame) {
///< 編碼數(shù)據(jù)回調(diào)寫入本地文件撮弧。
if (mStream == null) {
try {
mStream = new FileOutputStream(Environment.getExternalStorageDirectory().getPath() + "/test.h264");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
KFBufferFrame bufferFrame = (KFBufferFrame)frame;
try {
byte[] dst = new byte[bufferFrame.bufferInfo.size];
bufferFrame.buffer.get(dst);
mStream.write(dst);
} catch (IOException e) {
e.printStackTrace();
}
}
};
}
上面是 MainActivity
的實(shí)現(xiàn),主要分為以下幾個(gè)部分:
1)創(chuàng)建 OpenGL 上下文姚糊。
- 創(chuàng)建上下文
mGLContext
贿衍,這樣好處是采集與預(yù)覽可以共享,提高擴(kuò)展性救恨。
- 創(chuàng)建上下文
2)創(chuàng)建采集實(shí)例贸辈。
- 這里需要注意的是,我們通過開關(guān)
useCamera2
選擇Camera
或Camera2
肠槽。 - 參數(shù)配置
mCaptureConfig
擎淤,可自定義攝像頭方向、幀率秸仙、分辨率嘴拢。
- 這里需要注意的是,我們通過開關(guān)
\
- 采集數(shù)據(jù)回調(diào)
onFrameAvailable
,將數(shù)據(jù)輸入給渲染模塊與編碼模塊寂纪。
- 采集數(shù)據(jù)回調(diào)
4)編碼數(shù)據(jù)回調(diào)
KFMediaCodecListener
的dataOnAvailable
中席吴,將編碼數(shù)據(jù)存儲為 H.264/H.265 文件。
5、用工具播放 H.264/H.265 文件
完成視頻采集和編碼后抢腐,可以將 sdcard
文件夾下面的 test.h264
或 test.h265
文件拷貝到電腦上姑曙,使用 ffplay
播放來驗(yàn)證一下效果是否符合預(yù)期:
$ ffplay -I test.h264
$ ffplay -I test.h265
關(guān)于播放 H.264/H.265 文件的工具襟交,可以參考《FFmpeg 工具》第 2 節(jié) ffplay 命令行工具和《可視化音視頻分析工具》第 2.1 節(jié) StreamEye迈倍。
- 完 -
推薦閱讀