1.使用Camera做爲視頻源
2.使用MediaCodec進行視頻編碼
3.使用AudioRecord錄製音頻
4.使用google的libyuv編輯YUV數據(如旋轉,縮放,鏡像,nv21轉nv12)
5.使用ffmpeg進行 h264轉ts,合成多段音視頻,音視頻混合功能,以此實現分段錄製
6.抓取一幀圖片,使用libyuv轉成bitmap,實現拍照功能
複製代碼
首先初始化Camera對象,我封裝成CameraHelp了, 主要是設置預覽Size,先後攝像頭,旋轉角度,對焦等等, 最主要的是setPreviewCallback監聽預覽數據回傳, 代碼以下java
public void openCamera(Activity activity, int cameraId, SurfaceHolder surfaceHolder){
try {
this.cameraId = cameraId;
mCamera = Camera.open(cameraId);
displayOrientation = getCameraDisplayOrientation(activity, cameraId);
mCamera.setDisplayOrientation(displayOrientation);
mCamera.setPreviewDisplay(surfaceHolder);
mCamera.setPreviewCallback(previewCallback);
Camera.Parameters parameters = mCamera.getParameters();
previewSize = getPreviewSize();
parameters.setPreviewSize(previewSize[0], previewSize[1]);
parameters.setFocusMode(getAutoFocus());
parameters.setPictureFormat(ImageFormat.JPEG);
parameters.setPreviewFormat(ImageFormat.NV21);
mCamera.setParameters(parameters);
mCamera.startPreview();
} catch (Exception e) {
e.printStackTrace();
}
}
private void initMediaRecorder() {
mCameraHelp.setPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
//在此對視頻數據進行處理
}
});
}
複製代碼
onPreviewFrame會不斷被回調,大概一秒鐘30次,也就是說咱們錄製的視頻最多1秒鐘30幀,傳過來的byte數組是nv21格式的YUV420圖像數據
簡單來講就是Camera返回的YUV數據不能直接用,須要轉換, 並且爲了提升編輯速度 也須要轉換一下YUV數據格式android
與RGB相似,YUV也是一種顏色編碼方法,它將亮度信息(Y)與色彩信息(UV)分離,沒有UV信息同樣能夠顯示完整的圖像,只不過是黑白的,而且,YUV不像RGB那樣要求三個獨立的視頻信號同時傳輸,因此用YUV方式傳送佔用極少的頻寬.git
YUV格式有兩大類:planar和packed.
對於planar的YUV格式,先連續存儲全部像素點的Y,緊接着存儲全部像素點的U,隨後是全部像素點的V.
對於packed的YUV格式,每一個像素點的Y,U,V是連續交錯存儲的.
github
YUV裏分 YUV444, YUV422和YUV420
YUV 4:4:4採樣,每個Y對應一組UV份量
YUV 4:2:2採樣,每兩個Y共用一組UV份量
YUV 4:2:0採樣,每四個Y共用一組UV份量
數組
YUV420就是下區分NV21, NV12和I420
NV21:YYYYYYYY VU VU, 先Y而後VU交錯存儲
NV12:YYYYYYYY UV UV, 先Y而後UV交錯存儲
I420: YYYYYYYY UU VV, 先Y而後是U最後是V
緩存
android的Cardme返回的就是nv21就是屬於YUV420SP中的一種
bash
我這裏使用的YUV轉換流程: nv21 -> nv12 -> h264 -> mp4
框架
Camera返回nv21的YUV數據(這是原始數據),經過libyuv庫nv21轉nv12,而後使用MediaCidec把nv12轉成h264,最後使用ffmpeg把h264轉成mp4
這就是所有流程了.其中還包括對YUV圖像的編輯操做,下面的開始每步詳解ide
LanSoEditor.initSDK(this, null);
LanSongFileUtil.setFileDir("/sdcard/WeiXinRecorded/"+System.currentTimeMillis()+"/");
LibyuvUtil.loadLibrary();
複製代碼
1.先把nv21轉成I420 這樣方便對數據進行編輯操做, libyuv是封裝好的,直接使用就能夠了性能
//將 NV21 轉 I420
public static native void convertNV21ToI420(byte[] src, byte[] dst, int width, int height);
複製代碼
2.而後是圖像旋轉縮放鏡像
/**
* 壓縮 I420 數據
* <p>
* 執行順序爲:縮放->旋轉->鏡像
*
* @param src 原始數據
* @param srcWidth 原始寬度
* @param srcHeight 原始高度
* @param dst 輸出數據
* @param dstWidth 輸出寬度
* @param dstHeight 輸出高度
* @param degree 旋轉(90, 180, 270)
* @param isMirror 鏡像(鏡像在旋轉以後)
*/
public static native void compressI420(byte[] src, int srcWidth, int srcHeight,
byte[] dst, int dstWidth, int dstHeight,
int degree, boolean isMirror);
複製代碼
咱們經過Camera獲得的YUV數據都是橫向的, 因此咱們須要旋轉一下, 在前面初始化Camera時咱們已經獲得這個參數了, 通常來講後置攝像頭是90度, 前置攝像頭是270度(前置的還須要鏡像一下YUV數據), 若是你要壓縮的話, 也能夠傳入壓縮後的寬高.
3.最後是把I420轉成NV12, 下一步交給MediaCodec
/**
* 將 I420 轉 NV12
*/
public static native void convertI420ToNV12(byte[] src, byte[] dst, int width, int height);
複製代碼
1.先看看以前初始化的MediaCodec
private void initVideoMediaCodec()throws Exception{
MediaFormat mediaFormat;
if(rotation==90 || rotation==270){
//設置視頻寬高
mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, videoHeight, videoWidth);
}else{
mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, videoWidth, videoHeight);
}
//圖像數據格式 YUV420
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
//碼率
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, videoWidth*videoHeight*3);
//每秒30幀
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
//1秒一個關鍵幀
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
videoMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
videoMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
videoMediaCodec.start();
}
複製代碼
2.把nv12數據壓入數據隊列中
ByteBuffer inputBuffer = videoMediaCodec.getInputBuffer(inputIndex);
//把要編碼的數據添加進去
inputBuffer.put(nv12);
//塞到編碼序列中, 等待MediaCodec編碼
videoMediaCodec.queueInputBuffer(inputIndex, 0, nv12.length, System.nanoTime()/1000, 0);
複製代碼
3.而後從輸出隊列中獲得編碼後的h264數據
//讀取MediaCodec編碼後的數據
int outputIndex = videoMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
byte[] frameData = null;
int destPos = 0;
ByteBuffer outputBuffer = videoMediaCodec.getOutputBuffer(outputIndex);
byte[] h264 = new byte[bufferInfo.size];
//這步就是編碼後的h264數據了
outputBuffer.get(h264);
switch (bufferInfo.flags) {
case MediaCodec.BUFFER_FLAG_CODEC_CONFIG://視頻信息
configByte = new byte[bufferInfo.size];
configByte = h264;
break;
case MediaCodec.BUFFER_FLAG_KEY_FRAME://關鍵幀
videoOut.write(configByte, 0, configByte.length);
videoOut.write(h264, 0, h264.length);
break;
default://正常幀
videoOut.write(h264, 0, h264.length);
if(frameData == null) {
frameData = new byte[bufferInfo.size];
}
System.arraycopy(h264, 0, frameData, destPos, h264.length);
break;
}
videoOut.flush();
//數據寫入本地成功 通知MediaCodec釋放data
videoMediaCodec.releaseOutputBuffer(outputIndex, false);
複製代碼
這裏要區分視頻普通幀,關鍵幀和視頻頭信息 mp4會把視頻參數信息寫在視頻頭部(好比視頻長度,大小,編碼格式,音頻格式等等), 每隔1秒也會寫入一個關鍵幀
//把h264轉成ts文件
ffmpeg -i input -vcodec copy -vbsf h264_mp4toannexb output
//把ts轉成mp4 由於是分段錄製,這裏能夠是多個ts文件
ffmpeg -i concat:input1|input2|input3 -c copy -bsf:a aac_adtstoasc -y output
/**
* 執行成功,返回0, 失敗返回錯誤碼.
* 解析參數失敗 返回1
* sdk未受權 -1;
* 解碼器錯誤:69
* 收到線程的中斷信號:255
* 如硬件編碼器錯誤,則返回:26625---26630
* @param cmdArray ffmpeg命令的字符串數組, 可參考此文件中的各類方法舉例來編寫.
* @return 執行成功, 返回0, 失敗返回錯誤碼.
*/
private native int execute(Object cmdArray);
複製代碼
這步也是比較簡單, 經過調用VideoEditor的execute方法, 傳入ffmpeg語句, 交給ffmpeg就能夠了
1.以前咱們設定了視頻每秒30幀, 那麼每幀的間隔就是1000/30≈33毫秒 也就是說咱們須要在33毫秒內處理完這一幀的轉換過程, 那麼若是超出了33毫秒會怎麼樣呢?
MediaCodec在編碼數據時, 並無添加每幀的時間戳信息, 也就是視頻會以1秒30幀的速度播放, 但好比咱們處理一秒的時間是66毫秒, 咱們錄製1秒的視頻最後只有15幀數據,最後出來的視頻時間就是0.5秒
最後得出的結論是手機性能越差(處理一幀的時間大於33毫秒), 錄製出的視頻時間越短. 同理,手機性能越高(處理一幀的時間小於33毫秒), 錄製出的視頻時間越長. 好比處理一幀要17秒, 那麼一秒的視頻就有60幀, 錄製出的視頻時間就是2秒
2.解決方法我這裏有兩種, 第一種是使用MediaMuxer對音視頻進行封裝, 他會在內部同步視頻幀時間戳
我使用的是第二種, 針對手機性能不足,編碼時間過長的問題, 我使用libyuv替換了java代碼對YUV數據進行操做, 大大縮短了編輯時間
針對手機性能高, 編碼時間過快, 我在編碼YUV數據前, 進行時間戳比對,記錄當前總共編輯了多少視頻幀, 錄製時間多少, 判斷是否超出一秒30幀的限制.
3.整個視頻幀轉換過程, 小米8大概10毫秒左右,低端一點的機型大概20毫秒, 通常都會小於33毫秒,因此使用時間戳對比方式, 來進行視頻幀同步.
1.首先初始化AudioRecord, 須要傳入麥克風源,採樣率,聲道,緩存大小
private void initAudioRecord(){
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRateInHz, channelConfig, AudioFormat.ENCODING_PCM_16BIT, audioBufferSize);
}
複製代碼
2.而後開啓一個子線程, 不斷從audioRecord中取音頻數據, 直接寫入本地就好
private void startRecordAudio(){
RxJavaUtil.run(new RxJavaUtil.OnRxAndroidListener<Boolean>() {
@Override
public Boolean doInBackground() throws Throwable {
audioRecord.startRecording();
while (isRecording.get()) {
byte[] data = new byte[audioBufferSize];
if (audioRecord.read(data, 0, audioBufferSize) != AudioRecord.ERROR_INVALID_OPERATION) {
audioOut.write(data);
}
}
return true;
}
@Override
public void onFinish(Boolean result) {
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
}
});
}
複製代碼
3.最後咱們獲得的音頻數據是pcm, 經過ffmpeg轉成aac格式就可使用了(與ts文件合成mp4)
/**
* 把pcm格式的音頻文件編碼成AAC
* @param srcPach 源pcm文件
* @param samplerate pcm的採樣率
* @param channel pcm的通道數
* @return 輸出的m4a合成後的文件
*/
public String executePcmEncodeAac(String srcPach, int samplerate, int channel)
複製代碼
流程就是: 先把h264轉成ts, 而後合成多個ts, 最後ts轉mp4文件(這時是沒有音頻數據的)
接下來是音頻: 多個pcm音頻文件合成一個, 再把pcm轉成aac, 最後把mp4+aac合成完整的視頻
public void finishVideo(){
RxJavaUtil.run(new RxJavaUtil.OnRxAndroidListener<String>() {
@Override
public String doInBackground()throws Exception{
//h264轉ts
ArrayList<String> tsList = new ArrayList<>();
for (int x=0; x<segmentList.size(); x++){
String tsPath = LanSongFileUtil.DEFAULT_DIR+System.currentTimeMillis()+".ts";
mVideoEditor.h264ToTs(segmentList.get(x), tsPath);
tsList.add(tsPath);
}
//合成音頻
String aacPath = mVideoEditor.executePcmEncodeAac(syntPcm(), RecordUtil.sampleRateInHz, RecordUtil.channelCount);
//合成視頻
String mp4Path = mVideoEditor.executeConvertTsToMp4(tsList.toArray(new String[]{}));
//音視頻混合
mp4Path = mVideoEditor.executeVideoMergeAudio(mp4Path, aacPath);
return mp4Path;
}
@Override
public void onFinish(String result) {
closeProgressDialog();
Intent intent = new Intent(mContext, EditVideoActivity.class);
intent.putExtra(INTENT_PATH, result);
startActivityForResult(intent, REQUEST_CODE_KEY);
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
closeProgressDialog();
Toast.makeText(getApplicationContext(), "視頻編輯失敗", Toast.LENGTH_SHORT).show();
}
});
}
複製代碼
先得到攝像頭方向, 區分前置和後置, 前置的話還要鏡像圖片, 而後使用libyuv先把nv21轉成i420, 便於編輯圖像,而後調用LibyuvUtil.compressI420, 進行旋轉,鏡像,縮放
最後使用LibyuvUtil.convertI420ToBitmap輸出bitmap圖片, 保存在本地便可, 使用libyuv進行圖像編輯, 相較於使用java代碼操做圖片, 速度提高了3倍, 能夠達到毫秒級.
public String doInBackground() throws Throwable {
boolean isFrontCamera = mCameraHelp.getCameraId()== Camera.CameraInfo.CAMERA_FACING_FRONT;
int rotation;
if(isFrontCamera){
rotation = 270;
}else{
rotation = 90;
}
byte[] yuvI420 = new byte[nv21.length];
byte[] tempYuvI420 = new byte[nv21.length];
int videoWidth = mCameraHelp.getHeight();
int videoHeight = mCameraHelp.getWidth();
LibyuvUtil.convertNV21ToI420(nv21, yuvI420, mCameraHelp.getWidth(), mCameraHelp.getHeight());
LibyuvUtil.compressI420(yuvI420, mCameraHelp.getWidth(), mCameraHelp.getHeight(), tempYuvI420,
mCameraHelp.getWidth(), mCameraHelp.getHeight(), rotation, isFrontCamera);
Bitmap bitmap = Bitmap.createBitmap(videoWidth, videoHeight, Bitmap.Config.ARGB_8888);
LibyuvUtil.convertI420ToBitmap(tempYuvI420, bitmap, videoWidth, videoHeight);
String photoPath = LanSongFileUtil.DEFAULT_DIR+System.currentTimeMillis()+".jpeg";
FileOutputStream fos = new FileOutputStream(photoPath);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
fos.close();
return photoPath;
}
複製代碼