Android音视频(一) Camera2 API采集数据
Android音视频(二)音频AudioRecord和AudioTrack
Android音视频(三)FFmpeg Camera2推流直播
MediaCodec类可以访问底层媒体编解码框架(StageFright 或 OpenMAX),即编解码组件,它是Android基本的多媒体支持基础架构的一部分,通常和MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface和AudioTrack一起使用。它本身并不是Codec,它通过调用底层编解码组件获得了Codec的能力。
MediaCodec处理输入数据产生输出数据。当异步处理数据时,使用一组输入和输出Buffer队列。通常,在逻辑上,客户端请求(或接收)数据后填入预先设定的空输入缓冲区,输入Buffer填满后将其传递到MediaCodec并进行编解码处理。之后MediaCodec编解码后的数据填充到一个输出Buffer中。最后,客户端请求(或接收)输出Buffer,消耗输出Buffer中的内容,用完后释放,给回MediaCodec重新填充输出数据。
必须保证输入和输出队列同时非空,即至少有一个输入Buffer和输出Buffer才能工作。
在MediaCodec的生命周期中存在三种状态 :Stopped、Executing、Released。
Stopped状态实际上还可以处在三种状态:Uninitialized、Configured、Error。
Executing状态也分为三种子状态:Flushed, Running、End-of-Stream。
从上图可以看出:
当创建编解码器的时候处于未初始化状态。首先你需要调用configure(…)方法让它处于Configured状态,然后调用start()方法让其处于Executing状态。在Executing状态下,你就可以使用上面提到的缓冲区来处理数据。
Executing的状态下也分为三种子状态:Flushed, Running、End-of-Stream。在start() 调用后,编解码器处于Flushed状态,这个状态下它保存着所有的缓冲区。一旦第一个输入buffer出现了,编解码器就会自动运行到Running的状态。当带有end-of-stream标志的buffer进去后,编解码器会进入End-of-Stream状态,这种状态下编解码器不在接受输入buffer,但是仍然在产生输出的buffer。此时你可以调用flush()方法,将编解码器重置于Flushed状态。
调用stop()将编解码器返回到未初始化状态,然后可以重新配置。 完成使用编解码器后,您必须通过调用release()来释放它。
在极少数情况下,编解码器可能会遇到错误并转到错误状态。 这是使用来自排队操作的无效返回值或有时通过异常来传达的。 调用reset()使编解码器再次可用。 您可以从任何状态调用它来将编解码器移回未初始化状态。 否则,调用 release()动到终端释放状态。
优点:功耗低,速度快
缺点:扩展性不强,不同芯片厂商提供的支持方案不同,导致程序移植性差
适用场景:适合有固定的硬件方案的项目,如智能家居类;需要长时间摄像。
做了一个Demo,使用AudioRecord录音,使用MediaCodec 编码为AAC并保存文件,然后可以从AAC解码为PCM数据,再用AudioTrack播放。
1、编码PCM数据,保存为AAC文件 初始化AudioRecord和编码器 1 2 3 4 5 6 7 8 private void initAudioRecord () { int audioSource = MediaRecorder.AudioSource.MIC; int sampleRate = 44100 ; int channelConfig = AudioFormat.CHANNEL_IN_MONO; int audioFormat = AudioFormat.ENCODING_PCM_16BIT; int minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat); mAudioRecorder = new AudioRecord(audioSource, sampleRate, channelConfig, audioFormat, Math.max(minBufferSize, 2048 )); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private void initAudioEncoder () { try { mAudioEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC); MediaFormat format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 44100 , 1 ); format.setInteger(MediaFormat.KEY_BIT_RATE, 96000 ); format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, MAX_BUFFER_SIZE); mAudioEncoder.configure(format, null , null , MediaCodec.CONFIGURE_FLAG_ENCODE); } catch (IOException e) { e.printStackTrace(); } if (mAudioEncoder == null ) { Log.e(TAG, "create mediaEncode failed" ); return ; } mAudioEncoder.start(); encodeInputBuffers = mAudioEncoder.getInputBuffers(); encodeOutputBuffers = mAudioEncoder.getOutputBuffers(); mAudioEncodeBufferInfo = new MediaCodec.BufferInfo(); }
开始录音、编码 使用线程池,两条线程,一个线程去录音,另一个线程做编码操作。录音线程会将PCM数据存入一个队列中,编码线程从队列中取出数据编码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 mExecutorService.submit(new Runnable() { @Override public void run () { startRecorder(); } }); mExecutorService.submit(new Runnable() { @Override public void run () { encodePCM(); } }); private void putPCMData (byte [] pcmChunk) { Log.e(TAG, "putPCMData" ); try { queue.put(pcmChunk); } catch (InterruptedException e) { e.printStackTrace(); } } private byte [] getPCMData() { try { if (queue.isEmpty()) { return null ; } return queue.take(); } catch (InterruptedException e) { e.printStackTrace(); } return null ; } public static void addADTStoPacket (int sampleRateType, byte [] packet, int packetLen) { int profile = 2 ; int chanCfg = 2 ; packet[0 ] = (byte ) 0xFF ; packet[1 ] = (byte ) 0xF9 ; packet[2 ] = (byte ) (((profile - 1 ) << 6 ) + (sampleRateType << 2 ) + (chanCfg >> 2 )); packet[3 ] = (byte ) (((chanCfg & 3 ) << 6 ) + (packetLen >> 11 )); packet[4 ] = (byte ) ((packetLen & 0x7FF ) >> 3 ); packet[5 ] = (byte ) (((packetLen & 7 ) << 5 ) + 0x1F ); packet[6 ] = (byte ) 0xFC ; }
音频数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 private void startRecorder () { try { mFilePath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/RecorderTest/" + System.currentTimeMillis() + ".aac" ; mAudioFile = new File(mFilePath); if (!mAudioFile.getParentFile().exists()) { mAudioFile.getParentFile().mkdirs(); } mAudioFile.createNewFile(); mFileOutputStream = new FileOutputStream(mAudioFile); mAudioBos = new BufferedOutputStream(mFileOutputStream, 200 * 1024 ); mAudioRecorder.startRecording(); start = System.currentTimeMillis(); while (mIsRecording) { int read = mAudioRecorder.read(mBuffer, 0 , 2048 ); if (read > 0 ) { byte [] audio = new byte [read]; System.arraycopy(mBuffer, 0 , audio, 0 , read); putPCMData(audio); } } } catch (IOException | RuntimeException e) { e.printStackTrace(); } finally { if (mAudioRecorder != null ) { mAudioRecorder.release(); mAudioRecorder = null ; } } }
编码 从队列中循环取出数据,MediaCodec 编码,将编码后的数据写入文件中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 private void encodePCM () { int inputIndex; ByteBuffer inputBuffer; int outputIndex; ByteBuffer outputBuffer; byte [] chunkAudio; int outBitSize; int outPacketSize; byte [] chunkPCM; while (mIsRecording || !queue.isEmpty()) { chunkPCM = getPCMData(); if (chunkPCM == null ) { continue ; } inputIndex = mAudioEncoder.dequeueInputBuffer(-1 ); if (inputIndex >= 0 ) { inputBuffer = encodeInputBuffers[inputIndex]; inputBuffer.clear(); inputBuffer.limit(chunkPCM.length); inputBuffer.put(chunkPCM); mAudioEncoder.queueInputBuffer(inputIndex, 0 , chunkPCM.length, 0 , 0 ); } outputIndex = mAudioEncoder.dequeueOutputBuffer(mAudioEncodeBufferInfo, 10000 ); while (outputIndex >= 0 ) { outBitSize = mAudioEncodeBufferInfo.size; outPacketSize = outBitSize + 7 ; outputBuffer = encodeOutputBuffers[outputIndex]; outputBuffer.position(mAudioEncodeBufferInfo.offset); outputBuffer.limit(mAudioEncodeBufferInfo.offset + outBitSize); chunkAudio = new byte [outPacketSize]; addADTStoPacket(44100 , chunkAudio, outPacketSize); outputBuffer.get(chunkAudio, 7 , outBitSize); outputBuffer.position(mAudioEncodeBufferInfo.offset); try { mAudioBos.write(chunkAudio, 0 , chunkAudio.length); } catch (IOException e) { e.printStackTrace(); } mAudioEncoder.releaseOutputBuffer(outputIndex, false ); outputIndex = mAudioEncoder.dequeueOutputBuffer(mAudioEncodeBufferInfo, 10000 ); } } stopRecorder(); }
2、解码AAC AudioTrack播放 初始化AudioTrack和解码器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private void initAudioTrack () { int streamType = AudioManager.STREAM_MUSIC; int sampleRate = 44100 ; int channelConfig = AudioFormat.CHANNEL_OUT_MONO; int audioFormat = AudioFormat.ENCODING_PCM_16BIT; int mode = AudioTrack.MODE_STREAM; int minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat); audioTrack = new AudioTrack(streamType, sampleRate, channelConfig, audioFormat, Math.max(minBufferSize, 2048 ), mode); audioTrack.play(); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 private void initAudioDecoder () { try { mMediaExtractor = new MediaExtractor(); mMediaExtractor.setDataSource(mFilePath); MediaFormat format = mMediaExtractor.getTrackFormat(0 ); String mime = format.getString(MediaFormat.KEY_MIME); if (mime.startsWith("audio" )) { mMediaExtractor.selectTrack(0 ); format.setString(MediaFormat.KEY_MIME, "audio/mp4a-latm" ); format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1 ); format.setInteger(MediaFormat.KEY_SAMPLE_RATE, 0 ); format.setInteger(MediaFormat.KEY_BIT_RATE, 96000 ); format.setInteger(MediaFormat.KEY_IS_ADTS, 1 ); format.setInteger(MediaFormat.KEY_AAC_PROFILE, 0 ); mAudioDecoder = MediaCodec.createDecoderByType(mime); mAudioDecoder.configure(format, null , null , 0 ); } else { return ; } } catch (IOException e) { e.printStackTrace(); } if (mAudioDecoder == null ) { Log.e(TAG, "mAudioDecoder is null" ); return ; } mAudioDecoder.start(); }
解码并播放 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 private void decodeAndPlay () { boolean isFinish = false ; MediaCodec.BufferInfo decodeBufferInfo = new MediaCodec.BufferInfo(); while (!isFinish && mIsPalying) { int inputIdex = mAudioDecoder.dequeueInputBuffer(10000 ); if (inputIdex < 0 ) { isFinish = true ; } ByteBuffer inputBuffer = mAudioDecoder.getInputBuffer(inputIdex); inputBuffer.clear(); int samplesize = mMediaExtractor.readSampleData(inputBuffer, 0 ); if (samplesize > 0 ) { mAudioDecoder.queueInputBuffer(inputIdex, 0 , samplesize, 0 , 0 ); mMediaExtractor.advance(); } else { isFinish = true ; } int outputIndex = mAudioDecoder.dequeueOutputBuffer(decodeBufferInfo, 10000 ); ByteBuffer outputBuffer; byte [] chunkPCM; while (outputIndex >= 0 ) { outputBuffer = mAudioDecoder.getOutputBuffer(outputIndex); chunkPCM = new byte [decodeBufferInfo.size]; outputBuffer.get(chunkPCM); outputBuffer.clear(); audioTrack.write(chunkPCM, 0 , decodeBufferInfo.size); mAudioDecoder.releaseOutputBuffer(outputIndex, false ); outputIndex = mAudioDecoder.dequeueOutputBuffer(decodeBufferInfo, 10000 ); } } stopPlay(); }
Demo完成,手机测试效果不错。MediaCodec的使用要比我预想的复杂,网上查了好久才完成这个Demo,希望能帮到需要的人。
如有问题欢迎留言,Github源码 - MediaCodecActivity