調用Android Camera組件,獲取預覽時的byte[]數組,以後渲染到Activity的TextureView中,同時採用MediaCodec進行AVC(即H264)編碼,使用MediaMuxer進行打包,生成MP4文件。java
整個功能模塊分爲以下幾個子功能:數組
這裏採用的是Android.Hardware.Camera
類,注意區分Android.graphic.Camera
和Android.Hardware.Camera2
,前者是用於3D圖形繪製的工具,然後者是新的Camera操做類,這裏選擇的是第一代的Camera。markdown
首先最重要的一件事就是在清單中,申請權限。網絡
拿到權限後,咱們須要對 Camera進行初始化:架構
主要是初始化:cameraId和outputSizes屬性,前者是相機的ID,後者是相機輸出的畫幅尺寸。app
private fun initCamera() {
//初始化相機的一些參數
val instanceOfCameraUtil = CameraUtils.getInstance(this).apply {
this@CameraActivity.cameraManager = this.cameraManager!!
cameraId = this.getCameraId(false)!! //默認使用後置相機
//獲取指定相機的輸出尺寸列表
outPutSizes = this.getCameraOutputSizes(cameraId, SurfaceTexture::class.java)!!.get(0)
}
}
複製代碼
假定此時,你的Layout文件中,已經還有一個TextureView(id:textureView),咱們須要聲明一個TextureView.SurfaceTextureListener
:ide
private val mSurfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
}
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
return false
}
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
openCameraPreview(surface, width, height)
}
}
複製代碼
咱們須要關注的是第四個重寫方法,該方法將在TextureView可用時,被回調,這時,咱們就能夠根據該方法來構建預覽畫面了,這部分的代碼在網絡上不少的帖子中都有作過敘述。須要注意的是,這裏並不包含畫面對焦等等功能,若是有須要能夠自行百度一下。函數
一開始個人設想是構建一個手機豎屏視頻全屏播放器,那麼(橫縱)尺寸必定是:1080 * 1920。這樣一來,咱們輸入編碼器的長寬分別是:1080 * 1920,可是,咱們在setPreviewCallback
得到的照片數據:byte[]數組中,咱們的照片是橫着擺放的,這樣一來,尺寸就變成了:1920 * 1080。這個數據直接送入編碼器會致使畫面的異常:工具
因此,這個一維的byte[]數組中存放的nv21數據,咱們須要將它對應的位置給旋轉90度,這就是rotateYUV420Degree90
方法(方法參考文末的【附】)oop
private fun openCameraPreview(surfaceTexture: SurfaceTexture, width: Int, height: Int) {
//初始化預覽尺寸,這些屬性必須等到Texture可用後再回調,不然會出問題。
mPreviewSize = Size(1080, 1920) //初始化編碼器,強制聲明成1080*1920,也能夠根據這的長寬來定,1080P是一個比較通用的尺寸,可是放到全面屏中的全屏TextureView可能會致使畫面拉伸等等問題,須要另外去解決。
mTextureView.setAspectRation(mPreviewSize.width, mPreviewSize.height);
mCameraDevice = Camera.open(0)
mCameraDevice.setDisplayOrientation(90)
/** * 得到捕獲的視頻信息。 */
mCameraDevice.parameters = mCameraDevice.parameters.apply {
this!!.setPreviewSize(mPreviewSize.height, mPreviewSize.width)
this.setPictureSize(mPreviewSize.height, mPreviewSize.width)
this.previewFormat = CAMERA_COLOR_FORMAT
}
/** * Camera做爲生產者,生產的圖像數據,交給SurfaceTexture處理。 * 或者是進一步渲染 * 或者是顯示,這裏設置的PreviewTexture天然是顯示。 * 這裏的surfaceTexture其實是當咱們‘預覽’TextureView可用的時候,被回調的這個回調函數中提供了一個鉤子:surfaceTexture * 這個surfaceTexure將會做爲顯示的載體,直接被顯示出來。 */
mCameraDevice.setPreviewTexture(surfaceTexture)
mCameraDevice.setPreviewCallback { data, camera ->
//注意:照片的寬高是反着的,曰,而不是日
if (::mHandler.isInitialized) {
mHandler.post {
//把橫版視頻分辨率:1920 * 1080 轉換成豎版: 1080 * 1920
val verticalData = ImageFormatUtils.rotateYUV420Degree90(data, mPreviewSize.height,mPreviewSize.width)
onFrameAvailable(verticalData)
}
}
}
mCameraDevice.startPreview()
}
複製代碼
鑑於各類設備DSP芯片的區別,各類設備支持的色彩格式等等參數也有不一樣,在這裏我就使用在小米10上高通865可用的色彩格式之一:COLOR_FormatYUV420SemiPlanar,即NV21,接下來,咱們初始化MediaCodec
和MediaMuxer
。具體支持的格式須要真正運行時動態地去判斷、獲取。
若是設備的DSP芯片比較差,支持的格式也更少,硬解碼是沒法使用的,所以也應該適時地引入手段進行軟件解碼(FFmpeg等等)。這裏僅例舉MediaCodec的使用。格式必須配套,不配套的話會致使:色彩和位置之間的誤差、偏色、花屏等等各類問題。
private val MEDIA_TYPE = MediaFormat.MIMETYPE_VIDEO_AVCprivate
val MEDIACODEC_COLOR_FORMAT = MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar//接受的編NV21
private fun initEncoder() {
val supportedColorFormat = ImageFormatUtils.getSupportColorFormat()//獲取支持的色彩格式
try {
mMediaCodec = MediaCodec.createEncoderByType(MEDIA_TYPE)
mMediaFormat = MediaFormat.createVideoFormat(MEDIA_TYPE,mPreviewSize.width,mPreviewSize.height).apply { setInteger(MediaFormat.KEY_COLOR_FORMAT, MEDIACODEC_COLOR_FORMAT)//設置輸入的顏色 I420,咱們要先轉換NV21成I420
setInteger(MediaFormat.KEY_BIT_RATE, 10000000)
setInteger(MediaFormat.KEY_FRAME_RATE, 30)
setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5)
}
mMediaCodec.configure(mMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
//佈置混合器
val fileName = this.obbDir.absolutePath + "/" + System.currentTimeMillis() + ".mp4" mMuxer = MediaMuxer(fileName, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) } catch (e: Exception) {
e.printStackTrace()
return
}
}
複製代碼
若是到Muxer沒有出現錯誤,那麼說明Codec和Muxer都構建成功了。
通常通用的色彩格式是:I420,在這裏使用的應該是
COLOR_FormatYUV420Flexible
這個變量。須要在數據編碼前,將Nv21轉換爲I420的編碼,若是不轉換,使用主流的播放器也沒有太大的問題。
咱們須要開一個新的線程來做編碼的記錄,咱們在Camera的預覽界面拿到一幀數據後咱們經過子線程的Handler,爲其POST一個任務。
//編碼線程
private lateinit var mHandler: Handler
private lateinit var mWorkerThread: HandlerThread
private fun startEncoder() {
isEncoding = true //開始編碼
mMediaCodec.start() //構建鏈接器。
mWorkerThread = HandlerThread("WorkerThread-Encoder")
mWorkerThread.start()
mHandler = Handler(mWorkerThread.looper)
}
複製代碼
注意,咱們並不在此處就開啓Muxer,咱們會在子線程中接受數據的時候的某個狀態開始進行混合。
mCameraDevice.setPreviewCallback { data, camera ->
if (::mHandler.isInitialized) {
mHandler.post {
//把橫版視頻分辨率:1920 * 1080 轉換成豎版: 1080 * 1920
val verticalData = ImageFormatUtils.rotateYUV420Degree90(data, mPreviewSize.height, mPreviewSize.width)
onFrameAvailable(verticalData)
}
}
}
複製代碼
我在查詢Camera支持的分辨率的時候,發現全部的分辨率都是橫版的分辨率,即:1920*1080版本的,可是咱們MediaCodec最初設定的分辨率是豎版的,這裏也是一個坑。
onFrameAvailable()方法中,咱們不斷地插入一個byte數組,這個數組中是相機實時傳來的預覽畫面,咱們對這個畫面進行編碼便可。編碼完成後,將編碼出來的畫面接入到Muxer中:
private fun onFrameAvailable(_data: ByteArray?) {
if (!isEncoding) {
return;
}
//(可選NV21->I420),而後送入解碼器
val data: ByteArray = _data!!
var index = 0
try {
index = mMediaCodec.dequeueInputBuffer(0)
} catch (e: Exception) {
e.printStackTrace()
return
}
if (index >= 0) {
val inputBuffer = mMediaCodec.getInputBuffer(index)
inputBuffer!!.clear()
inputBuffer.put(data, 0, data.size)
mMediaCodec.queueInputBuffer(
index,
0,
data.size,
System.nanoTime() / 1000,
0)
}
while (true) {
val bufferInfo = MediaCodec.BufferInfo()
val encoderStatus = mMediaCodec.dequeueOutputBuffer(bufferInfo, 10_000)
if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
break//稍後再試
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
//輸出的格式發生了改變,此處開啓混合器
val newFormat = mMediaCodec.outputFormat
mVideoTrack = mMuxer!!.addTrack(newFormat)
mMuxer!!.start()
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
//
} else {
//正常編碼則得到緩衝區下標
val encodedDat = mMediaCodec.getOutputBuffer(encoderStatus)
if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
bufferInfo.size = 0
}
if (bufferInfo.size != 0) {
//設置從XX地方開始讀取數據
encodedDat!!.position(bufferInfo.offset)
//設置讀數據總長度
encodedDat.limit(bufferInfo.offset + bufferInfo.size)
//寫出MP4
if (!isEncoding) {
return
}
mMuxer!!.writeSampleData(mVideoTrack, encodedDat, bufferInfo)
}
//釋放緩衝區
mMediaCodec.releaseOutputBuffer(encoderStatus, false)
}
}
}
複製代碼
這個方法是在子線程中執行的。
private fun pauseRecord() {
+send//顯示發送按鈕
record.isRunning = false
Timer.cancel()//取消計時
showBackOrCancel()
if (isEncoding) {
stopEncoder()
}
}
private fun stopEncoder() {
isEncoding = false
Toast(this.obbDir.absolutePath + "\\下")
try {
mMuxer?.stop()
mMuxer?.release()
//中止
mMediaCodec.stop()
mMediaCodec.release()
} catch (e: Exception) {
e.printStackTrace()
}
}
複製代碼
這樣一來,咱們在存儲目錄中的Android/obb/包名/
下就有生成的文件了。
整體來講仍是挺簡陋的,好比沒有根據具體的設備動態地去判斷錄製的尺寸、錄製的色彩格式選擇等等,相機相關的功能閃光燈、對焦也未加入。
MediaCodec自己是編解碼器,和FFmpeg不一樣,它會優先進行硬件解碼,效率高,功耗低,可是缺點就是,兼容性、可擴展性相對於軟件解碼來講會更低。有一部分的播放軟件,將硬解仍是軟解的選擇權交給了用戶,這樣既能夠兼顧到擴展性,又能夠兼顧到功耗。
最終實現的效果(沒對焦):
public byte[] rotateYUV420Degree90(byte[] data, int imageWidth, int imageHeight) {
byte[] yuv = new byte[imageWidth * imageHeight * 3 / 2];
// Rotate the Y luma
int i = 0;
for (int x = 0; x < imageWidth; x++) {
for (int y = imageHeight - 1; y >= 0; y--) {
yuv[i] = data[y * imageWidth + x];
i++;
}
}
// Rotate the U and V color components
i = imageWidth * imageHeight * 3 / 2 - 1;
for (int x = imageWidth - 1; x > 0; x = x - 2) {
for (int y = 0; y < imageHeight / 2; y++) {
yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + (x - 1)];
i--;
yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + x];
i--;
}
}
return yuv;
}
複製代碼
public static int getSupportColorFormat() {
int numCodecs = MediaCodecList.getCodecCount();
MediaCodecInfo codecInfo = null;
for (int i = 0; i < numCodecs && codecInfo == null; i++) {
MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
if (!info.isEncoder()) {
continue;
}
String[] types = info.getSupportedTypes();
boolean found = false;
for (int j = 0; j < types.length && !found; j++) {
if (types[j].equals("video/avc")) {
Log.d("TAG:", "found");
found = true;
}
}
if (!found)
continue;
codecInfo = info;
}
Log.e("TAG", "Found " + codecInfo.getName() + " supporting " + "video/avc");
// Find a color profile that the codec supports
MediaCodecInfo.CodecCapabilities capabilities = codecInfo.getCapabilitiesForType("video/avc");
Log.e("TAG",
"length-" + capabilities.colorFormats.length + "==" + Arrays.toString(capabilities.colorFormats));
for (int i = 0; i < capabilities.colorFormats.length; i++) {
Log.d(TAG, "TAG MediaCodecInfo COLOR FORMAT :" + capabilities.colorFormats[i]);
if ((capabilities.colorFormats[i] == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar) || (capabilities.colorFormats[i] == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar)) {
return capabilities.colorFormats[i];
}
}
return MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible;
}
複製代碼