最近晚上和周末基本都在排隊(duì)練車伏伐,累成狗,好久沒寫文章了~
抽空整理了一下音視頻采集的方式晕拆,最終生成mp4藐翎。
一、音頻采集,得到PCM數(shù)據(jù)
音頻采集比較簡單实幕,通過 AudioRecord
錄音吝镣,然后在子線程不斷去讀PCM數(shù)據(jù)
記得聲明錄音權(quán)限 <uses-permission android:name="android.permission.RECORD_AUDIO" />
開始錄音
//默認(rèn)參數(shù)
private static final int AUDIO_SOURCE = MediaRecorder.AudioSource.MIC;
private static final int SAMPLE_RATE = 44100;
private static final int CHANNEL_CONFIGS = AudioFormat.CHANNEL_IN_STEREO;
private static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
private int bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIGS, AUDIO_FORMAT);
private AudioRecord audioRecord;
public void start() {
start(AUDIO_SOURCE, SAMPLE_RATE, CHANNEL_CONFIGS, AUDIO_FORMAT);
}
public void start(int audioSource, int sampleRate, int channels, int audioFormat) {
if (isStartRecord) {
Log.d(TAG, "音頻錄制已經(jīng)開啟");
return;
}
bufferSize = AudioRecord.getMinBufferSize(sampleRate, channels, audioFormat);
if (bufferSize == AudioRecord.ERROR_BAD_VALUE) {
Log.d(TAG, "無效參數(shù)");
return;
}
audioRecord = new AudioRecord(audioSource, sampleRate, channels, audioFormat, bufferSize);
audioRecord.startRecording();
isStopRecord = false;
threadCapture = new Thread(new CaptureRunnable());
threadCapture.start();
}
主要是創(chuàng)建AudioRecord的一些參數(shù),然后調(diào)用audioRecord.startRecording();
開始錄制昆庇,然后啟動(dòng)一個(gè)子線程末贾,去讀取錄制的PCM格式的數(shù)據(jù)
讀取PCM
/**
* 子線程讀取采集到的PCM數(shù)據(jù)
*/
private class CaptureRunnable implements Runnable {
@Override
public void run() {
while (!isStopRecord) {
byte[] buffer = new byte[bufferSize];
int readRecord = audioRecord.read(buffer, 0, bufferSize);
if (readRecord > 0) {
if (captureListener != null)
captureListener.onCaptureListener(buffer,readRecord);
Log.d(TAG, "音頻采集數(shù)據(jù)源 -- ".concat(String.valueOf(readRecord)).concat(" -- bytes"));
} else {
Log.d(TAG, "錄音采集異常");
}
//延遲寫入 SystemClock -- Android專用
SystemClock.sleep(10);
}
}
}
讀取PCM 比較簡單,就是通過audioRecord.read(buffer, 0, bufferSize)
整吆,最終PCM格式的數(shù)據(jù)會(huì)讀到這個(gè)buffer里
拿到錄制的每一幀PCM數(shù)據(jù)之后拱撵,可以用AudioTrack播放辉川,這里就不播放了,回調(diào)出去拴测,后面合成mp4要用到乓旗。
上面兩個(gè)步驟,可以封裝一個(gè)錄音的管理類集索。
【傳送門】(待補(bǔ)充)
現(xiàn)在獲取的音頻是PCM格式屿愚,我們要將它編碼成aac,然后跟視頻數(shù)據(jù)合成mp4务荆,這里要用到 MediaCodec
和 MediaMuxer
MediaCodec 使用
MediaCodec 是一個(gè)音視頻編解碼器妆距,本篇主要用于:
- 將PCM格式的音頻數(shù)據(jù)編碼成aac格式,
- 將NV21格式的相機(jī)預(yù)覽數(shù)據(jù)編碼成avc格式函匕。
API 簡介
getInputBuffers:獲取需要編碼數(shù)據(jù)的輸入流隊(duì)列娱据,返回的是一個(gè)ByteBuffer數(shù)組
queueInputBuffer:輸入流入隊(duì)列
dequeueInputBuffer:從輸入流隊(duì)列中取數(shù)據(jù)進(jìn)行編碼操作
getOutputBuffers:獲取編解碼之后的數(shù)據(jù)輸出流隊(duì)列,返回的是一個(gè)ByteBuffer數(shù)組
dequeueOutputBuffer:從輸出隊(duì)列中取出編碼操作之后的數(shù)據(jù)
releaseOutputBuffer:處理完成浦箱,釋放ByteBuffer數(shù)據(jù)
初始化音頻編解碼器
private MediaCodec mAudioCodec;
String audioType = MediaFormat.MIMETYPE_AUDIO_AAC; //編碼成aac格式
int sampleRate = 44100;
int channels = 2;//單聲道 channelCount=1 , 雙聲道 channelCount=2
private void initAudioCodec(String audioType, int sampleRate, int channels) {
try {
mAudioCodec = MediaCodec.createEncoderByType(audioType);
MediaFormat audioFormat = MediaFormat.createAudioFormat(audioType, sampleRate, channels);
int BIT_RATE = 96000;
audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE,
MediaCodecInfo.CodecProfileLevel.AACObjectLC);
int MAX_INOUT_SIZE = 8192;
audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, MAX_INOUT_SIZE);
mAudioCodec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
} catch (IOException e) {
Log.e(TAG, "initAudioCodec: 音頻類型無效");
}
}
視頻編解碼器的初始化同理
String videoType = MediaFormat.MIMETYPE_VIDEO_AVC;
private void initVideoCodec(String videoType, int width, int height) {
try {
mVideoCodec = MediaCodec.createEncoderByType(videoType);
MediaFormat videoFormat = MediaFormat.createVideoFormat(videoType, width, height);
videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
//MediaFormat.KEY_FRAME_RATE -- 可通過Camera#Parameters#getSupportedPreviewFpsRange獲取
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
//mSurfaceWidth*mSurfaceHeight*N N標(biāo)識碼率低吸耿、中、高酷窥,類似可設(shè)置成1咽安,3,5蓬推,碼率越高視頻越大妆棒,也越清晰
videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 4);
//每秒關(guān)鍵幀數(shù)
videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
videoFormat.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileHigh);
videoFormat.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel31);
}
mVideoCodec.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
//注意這里,獲取視頻解碼器的surface沸伏,之后要將opengl輸出到這個(gè)surface中
mSurface = mVideoCodec.createInputSurface();
} catch (IOException e) {
Log.e(TAG, "initVideoCodec: 視頻類型無效");
}
}
這里要注意的是糕珊,音頻數(shù)據(jù)從AudioRecord 直接讀出來,但是視頻數(shù)據(jù)處理有些不同毅糟,視頻數(shù)據(jù)不直接讀相機(jī)預(yù)覽红选,而是通過相機(jī)紋理id,利用OpenGL直接渲染到視頻編解碼器的surface上姆另,可以直接被編碼喇肋,效率比較高,所以這里獲取了MediaCodec的surface迹辐,mSurface = mVideoCodec.createInputSurface();
蝶防,后面通過EGL創(chuàng)建NatieWindow的時(shí)候要用到,這里大概先了解一下就行明吩,關(guān)于GLSurfaceView原理间学,EGL的使用,后面再整理一篇文章吧。
兩個(gè)編解碼器創(chuàng)建好了低葫,接下來要用到混合器详羡, MediaMuxer
MediaMuxer 使用詳解
MediaMuxer 是一個(gè)音視頻混合器,我們錄制音頻和視頻數(shù)據(jù)氮采,經(jīng)過MediaCodec編碼殷绍,然后再將編碼后的音視頻數(shù)據(jù)混合在一起,最終生成mp4鹊漠。
MediaMuxer主要方法:
1.int addTrack(@NonNull MediaFormat format)
一個(gè)視頻文件是包含一個(gè)或多個(gè)音視頻軌道的主到,而這個(gè)方法就是用于添加一個(gè)音頻頻或視頻軌道,并返回對應(yīng)的ID躯概。之后我們可以通過這個(gè)ID向相應(yīng)的軌道寫入數(shù)據(jù)登钥。用于新建音視頻軌道的MediaFormat是需要從MediaCodec.getOutputFormat()獲取的,而不是自己簡單構(gòu)造的MediaFormat娶靡。
2.start()
當(dāng)我們添加完所有音視頻軌道之后牧牢,需要調(diào)用這個(gè)方法告訴Muxer,我要開始寫入數(shù)據(jù)了姿锭。需要注意的是塔鳍,調(diào)用了這個(gè)方法之后,我們是無法再次addTrack了的呻此。
3.void writeSampleData(int trackIndex, @NonNull ByteBuffer byteBuf,
@NonNull BufferInfo bufferInfo)
用于向Muxer寫入編碼后的音視頻數(shù)據(jù)轮纫。trackIndex是我們addTrack的時(shí)候返回的ID,byteBuf便是要寫入的數(shù)據(jù)焚鲜,而bufferInfo是跟這一幀byteBuf相關(guān)的信息掌唾,包括時(shí)間戳、數(shù)據(jù)長度和數(shù)據(jù)在ByteBuffer中的位移
4.void stop()
與start()相對應(yīng)忿磅,用于停止寫入數(shù)據(jù)糯彬,并生成文件。
5.void release()
釋放Muxer資源葱她。
MediaMuxer 實(shí)戰(zhàn)
我們先來構(gòu)造一個(gè) MediaMuxer 撩扒,需要兩個(gè)參數(shù),第一個(gè)是音視頻文件的保存路徑吨些,第二個(gè)是音視頻封裝文件的格式搓谆,可以選擇mp4或3gp,我們使用mp4就好
int mediaFormat = MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4;
private void initMediaMuxer(String filePath, int mediaFormat) {
try {
mMediaMuxer = new MediaMuxer(filePath, mediaFormat);
} catch (IOException e) {
Log.e(TAG, "initMediaMuxer: 文件打開失敗,path=" + filePath);
}
}
添加音頻軌道
添加音頻軌道锤灿,在音頻編碼線程 AudioCodecThread 處理
public void run() {
super.run();
mIsStop = false;
audioCodec.start();
while (true) {
if (mMediaEncodeManager == null) {
Log.e(TAG, "run: mediaEncodeManagerWeakReference == null");
return;
}
if (mIsStop) {
mMediaEncodeManager.audioStop();
return;
}
//獲取一幀解碼完成的數(shù)據(jù)到bufferInfo挽拔,沒有數(shù)據(jù)就阻塞
int outputBufferIndex = audioCodec.dequeueOutputBuffer(bufferInfo, 0);
//第一次會(huì)返回-2辆脸,在這時(shí)候添加音軌
if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
mAudioTrackIndex = mediaMuxer.addTrack(audioCodec.getOutputFormat());
mMediaEncodeManager.mAudioTrackReady = true;
Log.d(TAG, "run: 添加音軌 mAudioTrackIndex= " + mAudioTrackIndex);
mMediaEncodeManager.startMediaMuxer();
} else {
while (outputBufferIndex != 0) {
if (!mMediaEncodeManager.mEncodeStart) {
Log.d(TAG, "run: 混合器還沒開始但校,線程延遲");
SystemClock.sleep(10);
continue;
}
ByteBuffer outputBuffer = audioCodec.getOutputBuffers()[outputBufferIndex];
outputBuffer.position(bufferInfo.offset);
outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
if (mPresentationTimeUs == 0) {
mPresentationTimeUs = bufferInfo.presentationTimeUs;
}
bufferInfo.presentationTimeUs = bufferInfo.presentationTimeUs - mPresentationTimeUs;
mediaMuxer.writeSampleData(mAudioTrackIndex, outputBuffer, bufferInfo);
audioCodec.releaseOutputBuffer(outputBufferIndex, false);
outputBufferIndex = audioCodec.dequeueOutputBuffer(bufferInfo, 0);
}
}
}
}
添加視頻軌道
添加視頻軌道,在視頻編碼線程 VideoCodecThread 處理
public void run() {
mIsStop = false;
videoCodec.start();
while (true) {
if (mMediaEncodeManager == null) {
Log.e(TAG, "run: mMediaEncodeManager == null");
return;
}
if (mIsStop) {
mMediaEncodeManager.videoStop();
return;
}
int outputBufferIndex = videoCodec.dequeueOutputBuffer(bufferInfo, 0);
//第一次返回 -2啡氢,在這個(gè)時(shí)候添加音軌
if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
mVideoTrackIndex = mediaMuxer.addTrack(videoCodec.getOutputFormat());
Log.d(TAG, "添加視頻軌道状囱,mVideoTrackIndex = " + mVideoTrackIndex);
mMediaEncodeManager.mVideoTrackReady = true;
mMediaEncodeManager.startMediaMuxer();
} else {
while (outputBufferIndex >= 0) {
if (!mMediaEncodeManager.mEncodeStart) {
Log.d(TAG, "run: 混合器還沒開始术裸,線程延遲");
SystemClock.sleep(10);
continue;
}
ByteBuffer outputBuffer = videoCodec.getOutputBuffers()[outputBufferIndex];
outputBuffer.position(bufferInfo.offset);
outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
if (mPresentationTimeUs == 0) {
mPresentationTimeUs = bufferInfo.presentationTimeUs;
}
bufferInfo.presentationTimeUs = bufferInfo.presentationTimeUs - mPresentationTimeUs;
mediaMuxer.writeSampleData(mVideoTrackIndex, outputBuffer, bufferInfo);
if (bufferInfo != null) {
mMediaEncodeManager.onRecordTimeCallBack((int) (bufferInfo.presentationTimeUs / 1000000));
}
videoCodec.releaseOutputBuffer(outputBufferIndex, false);
outputBufferIndex = videoCodec.dequeueOutputBuffer(bufferInfo, 0);
}
}
}
}
添加音頻軌道和視頻軌道后,就可以啟動(dòng)混合器亭枷,然后不斷從編解碼器MediaCodec中讀取已經(jīng)編碼成功的數(shù)據(jù)袭艺,然后調(diào)用mediaMuxer.writeSampleData(mAudioTrackIndex, outputBuffer, bufferInfo);
將編碼后的音/視頻數(shù)據(jù)寫到混合器里,停止的時(shí)候要調(diào)用
mMediaMuxer.stop();
mMediaMuxer.release();
如果不出意外的話叨粘,會(huì)在指定目錄下生成mp4文件猾编。
然后在PCM回調(diào)那里,將PCM數(shù)據(jù)扔到 MediaCodec 里面去升敲,這樣AudioCodecThread 里面就能讀到已經(jīng)編碼的aac格式數(shù)據(jù)答倡。
public void setPcmSource(byte[] pcmBuffer, int buffSize) {
try {
int buffIndex = mAudioCodec.dequeueInputBuffer(0);
if (buffIndex < 0) {
return;
}
ByteBuffer byteBuffer;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
byteBuffer = mAudioCodec.getInputBuffer(buffIndex);
} else {
byteBuffer = mAudioCodec.getInputBuffers()[buffIndex];
}
byteBuffer.clear();
byteBuffer.put(pcmBuffer);
//mPresentationTimeUs = 1000000L * (buffSize / 2) / mSampleRate
//一幀音頻幀大小 int size = 采樣率 x 位寬 x 采樣時(shí)間 x 通道數(shù)
// 1s時(shí)間戳計(jì)算公式 mPresentationTimeUs = 1000000L * (totalBytes / mSampleRate/ mAudioFormat / mChannelCount / 8 )
//totalBytes : 傳入編碼器的總大小
//1000 000L : 單位為 微秒,換算后 = 1s,
//除以8 : pcm原始單位是bit, 1 byte = 8 bit, 1 short = 16 bit, 用 Byte[]驴党、Short[] 承載則需要進(jìn)行換算
mPresentationTimeUs += (long) (1.0 * buffSize / (mSampleRate * mChannelCount * (mAudioFormat / 8)) * 1000000.0);
Log.d(TAG, "pcm一幀時(shí)間戳 = " + mPresentationTimeUs / 1000000.0f);
mAudioCodec.queueInputBuffer(buffIndex, 0, buffSize, mPresentationTimeUs, 0);
} catch (IllegalStateException e) {
//mAudioCodec 線程對象已釋放MediaCodec對象
Log.d(TAG, "setPcmSource: " + "MediaCodec對象已釋放");
}
}
視頻數(shù)據(jù)通過OpenGL渲染到視頻編解碼器的surface中瘪撇,只要打開相機(jī),視頻編碼器就能獲取到編碼后的視頻數(shù)據(jù)港庄,然后寫到混合器里倔既,跟音頻處理基本差不多。當(dāng)然鹏氧,這里涉及到自定義GLSurfaceView渤涌,參照GLSurfaceView中對EGL的處理,自己寫EglHelper度帮,這里不是本文重點(diǎn)歼捏,后面有時(shí)間再說下GLSurfaceView源碼。
接下來再簡單看一下如何通過Camera1采集視頻數(shù)據(jù)
視頻數(shù)據(jù)采集
相機(jī)功能封裝在 CameraManager中笨篷,使用的是Camera1瞳秽,需要注意的是設(shè)置預(yù)覽數(shù)據(jù)格式,還有一個(gè)是SurfaceTexture率翅,在外部創(chuàng)建(OpenGL創(chuàng)建紋理的時(shí)候)练俐,然后再啟動(dòng)相機(jī),把紋理傳過去冕臭,簡單貼下啟動(dòng)相機(jī)代碼
private void startCamera(int cameraId) {
try {
camera = Camera.open(cameraId);
camera.setPreviewTexture(surfaceTexture);
Camera.Parameters parameters = camera.getParameters();
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
parameters.setPreviewFormat(ImageFormat.NV21);
//設(shè)置對焦模式腺晾,后置攝像頭開啟時(shí)打開,切換到前置時(shí)關(guān)閉(三星辜贵、華為不能設(shè)置前置對焦,魅族悯蝉、小米部分機(jī)型可行)
if (cameraId == 0) {
//小米、魅族手機(jī)存在對焦無效情況托慨,需要針對設(shè)備適配鼻由,想要無感知對焦完全適配最好是監(jiān)聽加速度傳感器
camera.cancelAutoFocus();
//這種設(shè)置方式存在屏幕閃爍一下問題,包括Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
}
Camera.Size size = getCameraSize(parameters.getSupportedPreviewSizes(), screenWidth,
screenHeight, 0.1f);
parameters.setPreviewSize(size.width, size.height);
//水平方向未旋轉(zhuǎn),所以寬就是豎直方向的高,對應(yīng)旋轉(zhuǎn)操作
Log.d(TAG, "startCamera: 預(yù)覽寬:" + size.width + " -- " + "預(yù)覽高:" + size.height);
previewWidth = size.width;
previewHeight = size.height;
size = getCameraSize(parameters.getSupportedPictureSizes(), screenWidth, screenHeight, 0.1f);
parameters.setPictureSize(size.width, size.height);
//水平方向未旋轉(zhuǎn)蕉世,所以寬就是豎直方向的高
Log.d(TAG, "startCamera: 圖片寬:" + size.width + " -- " + "圖片高:" + size.height);
camera.setParameters(parameters);
camera.startPreview();
} catch (IOException e) {
e.printStackTrace();
}
}
相機(jī)啟動(dòng)之后預(yù)覽數(shù)據(jù)會(huì)輸出到 surfaceTexture蔼紧,這個(gè)surfaceTexture 關(guān)聯(lián)一個(gè)紋理id,就是通過OpenGL創(chuàng)建并綁定的紋理id
/**
* 創(chuàng)建攝像頭預(yù)覽擴(kuò)展紋理
*/
private void createCameraTexture() {
int[] textureIds = new int[1];
GLES20.glGenTextures(1, textureIds, 0);
cameraTextureId = textureIds[0];
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, cameraTextureId);
//環(huán)繞(超出紋理坐標(biāo)范圍) (s==x t==y GL_REPEAT 重復(fù))
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);
//過濾(紋理像素映射到坐標(biāo)點(diǎn)) (縮小狠轻、放大:GL_LINEAR線性)
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
surfaceTexture = new SurfaceTexture(cameraTextureId);
surfaceTexture.setOnFrameAvailableListener(this);
if (onSurfaceListener != null) {
//回調(diào)給CameraManager獲取surfaceTexture:通過camera.setPreviewTexture(surfaceTexture);
onSurfaceListener.onSurfaceCreate(surfaceTexture, fboTextureId);
}
// 解綁擴(kuò)展紋理
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
}
總結(jié)一下相機(jī)數(shù)據(jù)采集過程
- OpenGL創(chuàng)建紋理奸例,綁定一個(gè)紋理id
- 啟動(dòng)相機(jī),傳入前面創(chuàng)建的紋理向楼,這樣相機(jī)預(yù)覽數(shù)據(jù)就會(huì)輸出到OpenGL綁定的紋理查吊。
- 紋理并不能直接拿來編碼,需要參考GLSurfaceView的顯示原理湖蜕,創(chuàng)建EGL菩貌,通過OpenGL不斷將紋理渲染到MediaCodec的surface上,然后在一個(gè)子線程不斷獲取MediaCodec 中編碼成功的數(shù)據(jù)重荠,后面就跟音頻處理一樣箭阶,添加到混合器里,最終合成mp4文件戈鲁。
對OpenGL不熟悉的話沒關(guān)系仇参,有時(shí)間的話可以去學(xué)一下,不需要太深婆殿,也可以在我的簡書主頁查看OpenGL的入門系列文章
http://www.reibang.com/u/282785a6b12f
這篇文章內(nèi)容屬于音視頻開發(fā)的基礎(chǔ)部分了诈乒,后面要整理相機(jī)推流,會(huì)涉及到音視頻采集婆芦,也就是本章內(nèi)容怕磨。
在后面章節(jié)完成之后會(huì)把源碼提交到github,
想讓自己變優(yōu)秀消约,就要少看頭條肠鲫,少刷抖音,堅(jiān)持學(xué)習(xí)或粮,寫文章导饲,不然的話可能如下圖: