AVPlayer 添加音频播放功能
- 2019 年 10 月 10 日
- 筆記
01 前言
大家好,本文是 iOS/Android 音视频开发专题 的第八篇,该专题中 AVPlayer 项目代码将在 Github 进行托管,你可在微信公众号(GeekDev)后台回复 资料 获取项目地址。
在上篇文章 使用AudioTrack播放音频轨道 中我们使用 AudioTrack 播放了视频音轨数据。本篇文章中我们将为 AVPlayer 添加音效,并实现音视频同步。
本期内容:
- 封装解码器代码
- 实现音视频同步
- 结束语
02 封装解码器代码
首先,我们对 DemoMediaPlayerActivity 进行改造,将解码器相关代码进行封装,以便音频解码可以完美复用。
AVAssetTrackDecoder :
public class AVAssetTrackDecoder { /** 解码的轨道类型 */ private String mDecodeMimeType; private Context mContext; private Uri mUri; private AVAssetTrackDecoderDelegate mDelegate; private boolean mRuning; public interface AVAssetTrackDecoderDelegate { void newFrameReady(ByteBuffer byteBuffer, MediaCodec.BufferInfo bufferInfo); void outputFormatChaned(MediaFormat mediaFormat); } public AVAssetTrackDecoder(Context context,Uri uri,String mimeType) { this.mContext = context; this.mUri = uri; this.mDecodeMimeType = mimeType; } /** * 设置委托 * @param delegate */ public void setDelegate(AVAssetTrackDecoderDelegate delegate) { this.mDelegate = delegate; } /** * 喂入数据到解码器 * * @return true 喂入成功 * @since v3.0.1 */ private boolean feedInputBuffer(MediaExtractor source, MediaCodec codec) { if (source == null || codec == null) return false; int inIndex = codec.dequeueInputBuffer(0); if (inIndex < 0) return false; ByteBuffer codecInputBuffer = codec.getInputBuffers()[inIndex]; codecInputBuffer.position(0); int sampleDataSize = source.readSampleData(codecInputBuffer,0); if (sampleDataSize <=0 ) { // 通知解码器结束 if (inIndex >= 0) codec.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); return false; } MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); bufferInfo.offset = 0; bufferInfo.presentationTimeUs = source.getSampleTime(); bufferInfo.size = sampleDataSize; bufferInfo.flags = source.getSampleFlags(); switch (inIndex) { case INFO_TRY_AGAIN_LATER: return true; default: { codec.queueInputBuffer(inIndex, bufferInfo.offset, bufferInfo.size, bufferInfo.presentationTimeUs, bufferInfo.flags ); source.advance(); return true; } } } /** * 吐出解码后的数据 * * @return true 有可用数据吐出 * @since v3.0.1 */ private boolean drainOutputBuffer(MediaCodec mediaCodec) { if (mediaCodec == null) return false; final MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); int outIndex = mediaCodec.dequeueOutputBuffer(info, 0); if ((info.flags & BUFFER_FLAG_END_OF_STREAM) != 0) { mediaCodec.releaseOutputBuffer(outIndex, false); return false; } switch (outIndex) { case INFO_OUTPUT_BUFFERS_CHANGED: return true; case INFO_TRY_AGAIN_LATER: return true; case INFO_OUTPUT_FORMAT_CHANGED: { MediaFormat outputFormat = mediaCodec.getOutputFormat(); if(mDelegate != null) mDelegate.outputFormatChaned(outputFormat); return true; } default: { if (outIndex >= 0 && info.size > 0) { MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); bufferInfo.presentationTimeUs = info.presentationTimeUs; bufferInfo.size = info.size; bufferInfo.flags = info.flags; bufferInfo.offset = info.offset; ByteBuffer outputBuffer = mediaCodec.getOutputBuffers()[outIndex]; outputBuffer.position(bufferInfo.offset); outputBuffer.limit(bufferInfo.offset + bufferInfo.size); if (mDelegate != null && mDecodeMimeType.equalsIgnoreCase("audio/")) { mDelegate.newFrameReady(outputBuffer,bufferInfo); mediaCodec.releaseOutputBuffer(outIndex,true); }else { mediaCodec.releaseOutputBuffer(outIndex,true); mDelegate.newFrameReady(outputBuffer,bufferInfo); } } return true; } } } /** * 启动解码器 */ public void doDecoder(Surface surface){ // step 1:创建一个媒体分离器 MediaExtractor extractor = new MediaExtractor(); // step 2:为媒体分离器装载媒体文件路径 // 指定文件路径 try { extractor.setDataSource(mContext, mUri, null); } catch (IOException e) { e.printStackTrace(); } // step 3:获取并选中指定类型的轨道 // 媒体文件中的轨道数量 (一般有视频,音频,字幕等) int trackCount = extractor.getTrackCount(); // mime type 指示需要分离的轨道类型 String extractMimeType = mDecodeMimeType; MediaFormat trackFormat = null; // 记录轨道索引id,MediaExtractor 读取数据之前需要指定分离的轨道索引 int trackID = -1; for (int i = 0; i < trackCount; i++) { trackFormat = extractor.getTrackFormat(i); if (trackFormat.getString(MediaFormat.KEY_MIME).startsWith(extractMimeType)) { trackID = i; break; } } // 媒体文件中存在视频轨道 // step 4:选中指定类型的轨道 if (trackID != -1) extractor.selectTrack(trackID); // step 5:根据 MediaFormat 创建解码器 MediaCodec mediaCodec = null; try { mediaCodec = MediaCodec.createDecoderByType(trackFormat.getString(MediaFormat.KEY_MIME)); mediaCodec.configure(trackFormat,surface,null,0); mediaCodec.start(); } catch (IOException e) { e.printStackTrace(); } mRuning = true; while (mRuning) { // step 6: 向解码器喂入数据 boolean ret = feedInputBuffer(extractor,mediaCodec); // step 7: 从解码器吐出数据 boolean decRet = drainOutputBuffer(mediaCodec); if (!ret && !decRet)break;; } // step 8: 释放资源 // 释放分离器,释放后 extractor 将不可用 extractor.release(); // 释放解码器 mediaCodec.release(); } public void stop(){ mRuning = false; } }
AVAssetTrackDecoder 只是将之前的代码进行了封装,并没有任何新的内容。
03 实现音视频同步
音视频同步通常有三种方式:一种是参考视频,第二种是参考音频,第三种时互相参考。我们示例 demo 使用的为第一种和第二种,音视频自身完成同步。
说简单点音视频同步就是根据帧的显示时间,对解码线程进行锁定,已达到视频同步效果。下面是完整同步器代码:
public class AVMediaSyncClock { private static final long TIME_UNSET = Long.MIN_VALUE + 1; private static final long TIME_END_OF_SOURCE = Long.MIN_VALUE; /** 帧基准时间 */ private long mBasePositionUs; /** 指示当前播放速度 */ private float mSpeed = 1; /** 运行基准时间 */ private long mBaseElapsedMs; /** 当前时钟是否已开始计时 */ private boolean mStarted; /** 启动时钟 */ public void start() { if (mStarted) return; this.reset(); mStarted = true; } /** 停止时钟 */ public void stop() { mBasePositionUs = 0; mStarted = false; mBaseElapsedMs = 0; } private void reset() { mBasePositionUs = 0; mBaseElapsedMs = SystemClock.elapsedRealtime(); } /** * 锁定 * * @param positionUs 必须保证真实显示时间 (连续递增) */ public void lock(long positionUs,long diff) { if (!mStarted) { return; } if (mBasePositionUs == 0) mBasePositionUs = positionUs; long speedPositionUs = (long)((positionUs - mBasePositionUs) * (1.f/mSpeed)); long duraitonMs = usToMs(speedPositionUs) + diff; long endTimeMs = mBaseElapsedMs + duraitonMs; long sleepTimeMs = endTimeMs - SystemClock.elapsedRealtime(); if (sleepTimeMs > 0) { try { // 睡眠 锁定线程 TimeUnit.MILLISECONDS.sleep(sleepTimeMs); } catch (InterruptedException e) { e.printStackTrace(); } } } /** * 设置播放速度 * * @param speed */ public void setSpeed(float speed) { /** 设置速率时必须重置相关基数 */ reset(); mSpeed = speed; } /** * 获取当前播放速度 * * @return */ public float getSpeed() { return mSpeed; } public static long usToMs(long timeUs) { return (timeUs == TIME_UNSET || timeUs == TIME_END_OF_SOURCE) ? timeUs : (timeUs / 1000); } public static long msToUs(long timeMs) { // 防止越界 return (timeMs == TIME_UNSET || timeMs == TIME_END_OF_SOURCE) ? timeMs : (timeMs * 1000); } }
代码其实很简单,我就不在详细赘述了。
现在我们整合 AVAssetTrackDecoder 及 AVMediaSyncClock 实现完整播放器功能。
/** * 启动解码器 */ private void doDecoder() { final Uri videoPathUri = Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.demo_video); mMediaSyncClock = new AVMediaSyncClock(); mMediaSyncClock.start(); new Thread(new Runnable() { @Override public void run() { mVideoDecoder = new AVAssetTrackDecoder(DemoAVPlayer01Activity.this, videoPathUri, "video/"); mVideoDecoder.setDelegate(mVideoDecoderDelegate); mVideoDecoder.doDecoder(mSurfaceTexture.getSurface()); } }).start(); new Thread(new Runnable() { @Override public void run() { mAudioDecoder = new AVAssetTrackDecoder(DemoAVPlayer01Activity.this, videoPathUri, "audio/"); mAudioDecoder.setDelegate(mAudioDecoderDelegate); mAudioDecoder.doDecoder(null); } }).start(); } /** 视频解码器回调 */ private AVAssetTrackDecoder.AVAssetTrackDecoderDelegate mVideoDecoderDelegate = new AVAssetTrackDecoder.AVAssetTrackDecoderDelegate() { @Override public void newFrameReady(ByteBuffer byteBuffer, MediaCodec.BufferInfo bufferInfo) { // 锁定时钟 mMediaSyncClock.lock(bufferInfo.presentationTimeUs,0); } @Override public void outputFormatChaned(MediaFormat mediaFormat) { } }; /** 音频解码器回调 */ private AVAssetTrackDecoder.AVAssetTrackDecoderDelegate mAudioDecoderDelegate = new AVAssetTrackDecoder.AVAssetTrackDecoderDelegate() { @RequiresApi(api = Build.VERSION_CODES.M) @Override public void newFrameReady(ByteBuffer byteBuffer, MediaCodec.BufferInfo bufferInfo) { // 锁定时钟 mMediaSyncClock.lock(bufferInfo.presentationTimeUs,0); mAudioTrack.write(byteBuffer,bufferInfo.size,WRITE_BLOCKING,bufferInfo.presentationTimeUs); } @Override public void outputFormatChaned(MediaFormat outputFormat) { int sampleRate = 44100; if (outputFormat.containsKey(MediaFormat.KEY_SAMPLE_RATE)) sampleRate = outputFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE); int channelConfig = AudioFormat.CHANNEL_OUT_MONO; if (outputFormat.containsKey(MediaFormat.KEY_CHANNEL_COUNT)) channelConfig = outputFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT) == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO; int audioFormat = AudioFormat.ENCODING_PCM_16BIT; if (outputFormat.containsKey("bit-width")) audioFormat = outputFormat.getInteger("bit-width") == 8 ? AudioFormat.ENCODING_PCM_8BIT : AudioFormat.ENCODING_PCM_16BIT; mBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat) * 2; mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,sampleRate,channelConfig,audioFormat,mBufferSize,AudioTrack.MODE_STREAM); mAudioTrack.play(); } };
关键音视频代码在 newFrameRead 据帧显示时间计算线程暂停时间。
具体代码见:DemoAVPlayer01Activity
04 结束语
在公众号后台经常催我实现音视频同步 ,为了大家能尽早看到这部分内容恕我偷懒了。
来源: GeekDev 公众号