浅析Camera视频实时采集中涉及的参数配置
作者: |
蒋东国 |
时间: |
2017年1月20日 星期五 |
应用来源: |
mpu |
博客地址: |
http://blog.csdn.net/andrexpert/article/details/54631629 |
1.录制视频
(1)帧率(frame rate):在1秒钟时间里传输的图片数量,也可以理解为图形处理器每秒钟能够刷新几次,一般NTSC①是30,PAL②是25,单位FPS 或Hz。它影响画面流畅度,与画面流畅度成正比:帧率越大,画面越流畅;帧率越小,画面越有跳动感。如果码率为变量,则帧率也会影响体积,帧率越高,每秒钟经过的画面越多,需要的码率也越高,体积也越大。
(2)关键帧(key frame):任何动画要表现运动或变化,至少前后要给出两个不同的关键状态,而中间状态的变化和衔接电脑可以自动完成,即表示关键状态的帧叫做关键帧。
(3)过渡帧(interim frame):在两个关键帧之间,电脑自动完成过渡画面的帧叫做过渡帧。
(4)码率(bit rate):视频中比特率又被称为码率,是指码率就是数据传输时单位时间传送的数据位数,单位是kbps即千位每秒(=1000*1bps)。它可以表示经过编码(压缩)后的音、视频数据每秒钟需要用多少个比特来表示,比特率越高,传输数据就越大,音、视频的质量就越好,但编码后的文件就越大。常见的视频码率使用场景:
* 16kbps:可视电话质量
* 128-384kbps:视频会议系统质量
* 1.25Mbps:VCD质量(使用MPEG1压缩)
* 5Mbps:DVD质量(使用MPEG2压缩)
* 8-15Mbps:高清晰度电视(HDTV) 质量(使用H.264压缩)
* 29.4 Mbps:HD DVD质量
* 40Mbps:蓝光光碟质量(使用MPEG2、H.264或VC-1压缩)
注意:码率的单位换算与网速的单位换算区别
1000 bit/s = 1 kbit/s (一千位每秒) 1024B/s = 1KB/s
1000 kbit/s = 1 Mbit/s (一兆或一百万位每秒) 1024KB/s = 1MB/s
1000 Mbit/s = 1 Gbit/s (一吉比特或十亿位每秒) 1024MB/s = 1GB/s
(5)清晰度(sharpness):指影像上各细部影纹及其边界的清晰程度。在码率一定的情况下,分辨率与清晰度成反比关系:分辨率越高,图像越不清晰,分辨率越低,图像越清晰。在分辨率一定的情况下,码率与清晰度成正比关系,码率越高,图像越清晰;码率越低,图像越不清晰。
(6)分辨率(resolution ratio):就是屏幕图像的精密度,显示器所能显示的像素的多少。可以把整个图像想象成是一个大型的棋盘,而分辨率的表示方式就是所有经线和纬线交叉点的数目。以分辨率为1024×768的屏幕来说,(即每一条水平线上包含有1024个像素点,共有768条线,总像素1024x768个),即扫描列数为1024列,行数为768行。分辨率影响图像大小,与图像大小成正比:分辨率越高,图像越大;分辨率越低,图像越小。
实例演示:使用MediaCodec将Camera采集图像编码为MP4格式视频
/** *@decrible 视频录像,使用MediaRecorder * * Create by jiangdongguo on 2017-1-6 上午9:27:47 */ public class VideoRecordRunnable implements Runnable { private static final String filePath = Environment .getExternalStorageDirectory().getAbsolutePath()+ File.separator+ System.currentTimeMillis() + ".mp4"; private MediaRecorder mMediaRecorder; private Camera mCamera; private SurfaceHolder mHolder; public VideoRecordRunnable(Camera mCamera,SurfaceHolder mHolder) { this.mCamera = mCamera; this.mHolder = mHolder; } private void initMediaRecorder() { if(mMediaRecorder == null){ mMediaRecorder = new MediaRecorder(); }else{ mMediaRecorder.reset(); } //需要调unlock,否则会报异常 mCamera.unlock(); mMediaRecorder.setCamera(mCamera); mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER); CamcorderProfile profile = CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH); mMediaRecorder.setProfile(profile); mMediaRecorder.setOutputFile(filePath); mMediaRecorder.setPreviewDisplay(mHolder.getSurface()); } @Override public void run() { initMediaRecorder(); // 使配置生效 try { mMediaRecorder.prepare(); mMediaRecorder.start(); } catch (IllegalStateException e) { e.printStackTrace(); stopMediaRecord(); } catch (IOException e) { e.printStackTrace(); stopMediaRecord(); } } private void stopMediaRecord(){ if(mMediaRecorder != null){ mMediaRecorder.stop(); mMediaRecorder.release(); mMediaRecorder = null; } } }2.录制音频
(1)音频源(audio source):即音频数据来源,定义在MediaRecorder.AudioSource中。MIC表示音频源为麦克风,VOICE_COMMUNICATION表示音频源为蓝牙耳机。
(2)采样率(sampling rate):采样率定义了每秒从连续信号中提取并组成离散信号的采样个数,它用赫兹(Hz)来表示。针对于音频而言,采样率则为计算机每秒钟采集声音样本的数量,数量越大声音质量就越好,体积随之也会增大。常见的有8000HZ、22050HZ、44100HZ、16000HZ、96000Hz等,它们分别适用于电话、无线电广播、MP3/VCD、音频CD/VCD/MP3、HD-DVD/BD-ROM,其中最常用的就是44100HZ。
(3)声道(audio channel):由于音频的采集和播放是可以叠加的,因此,可以同时从多个音频源采集声音,并分别输出到不同的扬声器,故声道数一般表示声音录制时的音源数量或回放时相应的扬声器数量。单声道(Mono)和双声道(Stereo)比较常见,顾名思义,前者的声道数为1,后者为2。双声道(Stero)的是立体的,不同的声音从左右两个音箱/耳机传出来合成在一起,有剧场感,效果逼真,对设备要求高。单声道(Mono)就是混合在一起的声音平均分配到两个耳机里,立体感差,没层次,普通设备就可以听
(4)采样位数/量化精度(audio format):每一个红色的采样点,都需要用一个数值来表示大小,
这个数值的数据类型大小可以是:4bit、8bit、16bit、32bit等等,位数越多,表示得就越精细,声音质量自然就越好,当然,数据量也会成倍增大。声音的采样位数就相当于画面的颜色数,表示每个取样的数据量,当然数据量越大,回放的声音越准确,不过受人的器官的机能限制,16位的声音已经是普通人类的极限了,更高位数就只能靠仪器才能分辨出来。AudioFormat .ENCODING_PCM_16BIT表示一个采样点PCM数据占16位b(最多设备支持)、AudioFormat. ENCODING_PCM_8BIT表示一个采样点PCM数据占8位(部分设备支持)。
(5)比特率(bit rate):比特率是指每秒传送的比特(bit)数,单位为 bps(Bit Per Second),比特率越高,传送的数据量越大,音质就越好。比如比特率为56kbps的通话语音,它每秒传输的数据量约为56000/8/1024 = 6KB;而比特率为1146 kbps的CD音频,它每秒传输的数据量约为1146000/8/1024 = 139KB。由此可知,比特率高的CD音频质量单位每秒传输的数据量远远大于通话音频,因此CD音频的质量优于通话语音。常见比特率应用场景:
* 800bps:能够分辨的语音所需最低码率
* 8kbps:电话质量(使用语音编码)
* 96kbps:FM质量
* 192 kbps:优良质量
* 224-500kps:高质量(有损音频模式),常用于MPEG1 Player1/2/3
* 500 kbps-1.4 Mbps:44.1KHz的无损音频,解码器为FLAC Audio,WavPack或Monkey's Audio
* 1.4Mbps-2.8Mbps:脉冲编码调制(PCM)声音格式CD光碟的数字音频
注意:1kbps = 1000bps,1Mbps=1000kbps要区别于1KB = 1024B
(6)音频采样率、比特率、采样位数、声道计算关系
比特率 =采样率 x 采用位数 x声道数
音频大小 = 比特率 x 时间(s)
比特率 = 音频文件大小 / 时间
比如,录制60分钟每秒44100次采样、双声道、每秒13位取样的音频数据到CD和60
分钟每秒8000次采样、单声道、每秒7位取样的通话语音,它们的比特率和大小应该是多少?
CD:比特率= 44100*2*13 =1146600 bps,即每秒的数据量约1146600/8(B)/1024 = 139KB
大小 = 139(KB/s) * (60 * 60)s = 499712KB = 488MB
通话:比特率=8000*1*7=56000bps,即每秒的数据量约为56000/8/1024 = 6KB
大小 = 6KB/s * (60 * 60)s = 21600 KB =21MB
实例演示:使用AudioRecord、AudioTrack实现边录边播
/** * @decrible 使用AudioRecord、AudioTrack实现边录边播 * * Create by jiangdongguo on 2017-1-6 上午9:21:16 */ public class AudioPlayingRunnable implements Runnable { private static final String TAG = "AudioPlayingThread"; /**流类型,其他的还有铃声、通知等*/ private static final int streamType = AudioManager.STREAM_MUSIC; /**模式,分为静态缓存和流两种*/ private static final int mode = AudioTrack.MODE_STREAM; /**采样率*/ private static final int sampleRateInHz = 41400; /**单声道输入、输出,立体声为STEREO*/ private static final int channelInConfiguration = AudioFormat.CHANNEL_IN_MONO; private static final int channelOutConfiguration = AudioFormat.CHANNEL_OUT_MONO; /**采样位数16bit*/ private static final int audioFormat = AudioFormat.ENCODING_PCM_16BIT; /**音频源为麦克风*/ private static final int RECODER_RESOURCE_MIC = MediaRecorder.AudioSource.MIC; //录音 private AudioRecord audioRecord = null; private boolean isRecording = false; private int recBufSize = 0; private byte[] resultBuffer; //播放 private AudioTrack audioTrack; private int trackBufSize = 0; private void initAudioRecord() { if(audioRecord != null){ stopRecording(); } //获得AudioRecord所需的最小缓存 recBufSize = AudioRecord.getMinBufferSize(sampleRateInHz, channelInConfiguration, audioFormat); if (recBufSize < 1600) { recBufSize = 1600; } resultBuffer = new byte[recBufSize]; audioRecord = new AudioRecord(RECODER_RESOURCE_MIC, sampleRateInHz, channelInConfiguration, audioFormat, recBufSize); //开始录音,得到PCM音频流 audioRecord.startRecording(); isRecording = true; Log.i(TAG, "初始化AudioRecord,并开始录制..."); } private void initAudioTrack(){ if(audioTrack != null){ audioTrack.stop(); audioTrack.release(); audioTrack = null; } trackBufSize = AudioTrack.getMinBufferSize(sampleRateInHz, channelOutConfiguration, audioFormat); if(trackBufSize < 1600){ trackBufSize = 1600; } audioTrack = new AudioTrack(streamType, sampleRateInHz, channelOutConfiguration, audioFormat, trackBufSize*2, mode ); if(isRecording){ audioTrack.play(); } Log.i(TAG, "初始化AudioRecord,并准备播放..."); } @Override public void run() { initAudioRecord(); initAudioTrack(); while (isRecording) { //将PCM音频流数据保存的大小为1024byte的缓存 int readBytes = audioRecord.read(resultBuffer, 0, resultBuffer.length); //播放缓存中PCM音频流 if(readBytes > 0){ audioTrack.write(resultBuffer, 0, readBytes); } } Log.i(TAG, "停止录制,结束录制线程"); } public void stopRecording() { isRecording = false; if(audioRecord != null){ audioRecord.stop(); audioRecord.release(); audioRecord = null; } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } if(audioTrack != null){ audioTrack.stop(); audioTrack.release(); audioTrack = null; } Log.i(TAG, "执行停止操作,释放资源"); } }
3.Medicodec的MediaFormat
封装描述多媒体(音、视频)数据格式的信息,这些格式信息由键值对指定。如果是对多媒体数据进行编码,则MediaFormat指定的是被编码后的多媒体文件;如果是对多媒体文件进行解码,则MediaFormat指定的就是要被解码的多媒体文件。
归属 |
键名 |
类型 |
描述 |
通用 |
KEY_MIME |
String |
指定多媒体文件类型,如acc音频"audio/mp4a-latm"、 avc视频"video/avc"等,详见MediaFormat |
KEY_MAX_INPUT_SIZE |
Integer |
指定输入数据最大缓存(可选) |
|
KEY_BIT_RATE |
Integer |
比特率表示经过编码(压缩)后的音、视频数据每秒钟需要用多少个比特来表示。通常,比特率越高,音、视频的质量就越好,但编码后的文件就越大。(只限编码使用) |
|
视频格式 |
KEY_WIDTH |
Integer |
指定video原始图像帧的宽度 |
KEY_HEIGHT |
Integer |
指定video原始图像帧的高度 |
|
KEY_COLOR_FORMAT |
Integer |
指定video数据的颜色格式,如RGB、YUV,Android Camera采集的原始帧颜色格式可为NV21或YV12,但Medicodec只对COLOR_FormatYUV420SemiPlanar格式有效,因此通常使用NV21转换为该格式。更多颜色格式详见MediaCodecInfo.CodecCapabilities |
|
KEY_FRAME_RATE |
Integer |
指定video帧率(只限编码使用) |
|
KEY_I_FRAME_INTERVAL |
Integer |
describing the frequency of I frames expressed in secs between I frames(只限编码使用) |
|
音频 格式 |
KEY_CHANNEL_COUNT |
Integer |
指定音频通道数量 |
KEY_SAMPLE_RATE |
Integer |
指定音频采样率 |
|
KEY_IS_ADTS |
Integer |
解码ACC音频文件时,设置为1表明前缀是ADTS头 |
实例演示:使用Medicodec编码PCM音频流生成ACC文件
/** *@decrible 使用MediaCodec将PCM音频流编码封装成Acc格式文件 * * Create by jiangdongguo on 2017-1-6 上午9:18:26 */ public class AudioAccRunnable implements Runnable{ private static final String TAG = "AudioAccRunnable"; //音频源,麦克风 private static final int audioSource = MediaRecorder.AudioSource.MIC; //采样率,44100Hz private static final int sampleRateInHz = 44100; //单声道 private static final int channelConfig = AudioFormat.CHANNEL_IN_MONO; //量化精度,16bit private static final int audioFormat = AudioFormat.ENCODING_PCM_16BIT; private MediaCodec mMediaCodec; private AudioRecord mAudioRecord; private int bufferSizeInBytes; private byte[] mFrameByte; private ByteBuffer[] inputBuffers; private ByteBuffer[] outputBuffers; private boolean isRecording = false; private Context mContext; //编码类型,acc(即"audio/mp4a-latm") private static final String codec_type = MediaFormat.MIMETYPE_AUDIO_AAC; //比特率,高质量192000bps private static final int codec_bitrate = 192000; //通道数量,1(单声道) private static final int codec_channel = 1; //最大输入缓存 private static final int codec_max_inputsize = 1600; private String filePath = Environment.getExternalStorageDirectory() .getAbsolutePath()+ File.separator+ System.currentTimeMillis() + ".acc"; private FileOutputStream outputStream; public AudioAccRunnable(Context mContext) { this.mContext = mContext; } private void initAudioRecord(){ if(mAudioRecord != null){ isRecording = false; stopAudioRecord(); } bufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat); mAudioRecord = new AudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes*2); //开始录音 mAudioRecord.startRecording(); } private void initMediaCodec(){ if(mMediaCodec != null){ isRecording = false; stopMediaCodec(); } //配置编码格式:编码类型、比特率、ACC配置信息、通道数量 MediaFormat format = MediaFormat.createAudioFormat(codec_type, sampleRateInHz, codec_channel); format.setInteger(MediaFormat.KEY_BIT_RATE,codec_bitrate); format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE,codec_max_inputsize ); try { mMediaCodec = MediaCodec.createEncoderByType(codec_type); } catch (IOException e) { e.printStackTrace(); } //配置指定编码器 mMediaCodec.configure(format, null, null,MediaCodec.CONFIGURE_FLAG_ENCODE); //启动编码器 mMediaCodec.start(); } @Override public void run() { initAudioRecord(); initMediaCodec(); //设置线程优先级 int len = -1; byte[] pcmBuffer = new byte[codec_max_inputsize]; android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO); isRecording = true; while(isRecording ){ len = mAudioRecord.read(pcmBuffer,0,codec_max_inputsize); if(len<0){ Log.w(TAG, "录音失败,数据无效"); } encodecPCM(pcmBuffer); } } @SuppressLint("NewApi") private void encodecPCM(byte[] data) { inputBuffers = mMediaCodec.getInputBuffers(); outputBuffers = mMediaCodec.getOutputBuffers(); /**得到编码器输入缓冲区句柄 向输入缓存区PCM原始音频,然后释放句柄*/ int inputBufferIndex = mMediaCodec.dequeueInputBuffer(-1); if(inputBufferIndex >= 0){ ByteBuffer inputBuffer = inputBuffers[inputBufferIndex]; inputBuffer.clear(); inputBuffer.put(data); inputBuffer.limit(data.length); mMediaCodec.queueInputBuffer(inputBufferIndex, 0, data.length, System.nanoTime(), 0); } /**得到编码器输出缓冲区句柄 对输出缓存区编码好的音频数据进行ACC封装*/ MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo(); int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo,0); while(outputBufferIndex >= 0){ if(outputBufferIndex >= 0){ ByteBuffer outputBuffer = outputBuffers[outputBufferIndex]; //给adts头字段空出7个字节 int length = mBufferInfo.size + 7; if(mFrameByte==null || mFrameByte.length < length){ mFrameByte = new byte[length]; } addADTtoPacket(mFrameByte,length); //将加入adts头字段的编码数据保存到mFrameByte缓存 outputBuffer.get(mFrameByte,7, mBufferInfo.size); savaACCAudio(mFrameByte); mMediaCodec.releaseOutputBuffer(outputBufferIndex, false); Log.i(TAG, "编码成功,存储到文件"+mFrameByte.length); }else if(outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED){ Log.w(TAG, "output buffer changed"); }else if(outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){ Log.w(TAG, "output format changed"); }else if(outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER){ Log.w(TAG, "get output index timeout"); }else{ Log.w(TAG, "unkown error"+outputBufferIndex); } outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 0); } } /** * 保存封装ACC头到文件 */ private void savaACCAudio(byte[] data) { try { if(outputStream==null){ File file = new File(filePath); outputStream = new FileOutputStream(file); } outputStream.write(data); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } /** 给编码出的acc数据流添加adts头字段 * @param mFrameByte2 缓存数组,需空出前7个字节,否则会搞乱数据 * @param length */ private void addADTtoPacket(byte[] packet, int pakcetLen) { int profile = 2; //ACC LC int freqIdx = 4; //44.1kHz int chanCfg = 2; //CPE packet[0] = (byte)0xFF; packet[1] = (byte)0xF9; packet[2] = (byte)(((profile-1)<<6)+(freqIdx<<2)+(chanCfg>>2)); packet[3] = (byte)(((chanCfg&3)<<6)+(pakcetLen>>11)); packet[4] = (byte)((pakcetLen&0x7FF)>>3); packet[5] = (byte)(((pakcetLen&7)<<5)+0x1F); packet[6] = (byte)0xFC; } public void stopRecording(){ isRecording = false; stopAudioRecord(); stopMediaCodec(); if(outputStream != null){ try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } private void stopAudioRecord() { if(mAudioRecord != null){ mAudioRecord.stop(); mAudioRecord.release(); mAudioRecord = null; } } private void stopMediaCodec(){ if(mMediaCodec != null){ mMediaCodec.stop(); mMediaCodec.release(); mMediaCodec = null; } } }