發(fā)表這篇文章目的是為了記錄一次解決Android開發(fā)中遇到的問題,總結(jié)解決思路及心得.這里要特別感謝指導(dǎo)我的劉老師,新項(xiàng)目的領(lǐng)導(dǎo).
現(xiàn)象:配置(CPU)稍微偏低的手機(jī)生成視頻播放時(shí)為黑屏.
初步分析:為寫入視頻時(shí)出錯(cuò)導(dǎo)致.
分析的思路如下:
下面是音視頻混合代碼:
EncoderVideoRunnable和MediaMuxerRunnable是兩個(gè)線程,前者生成編碼后的視頻數(shù)據(jù),后者將視頻數(shù)據(jù)寫入文件.
(AiMediaMuxer.java)
private class MediaMuxerRunnable implements Runnable {
@Override
public void run() {
initMuxer();
baseTimeStamp = System.nanoTime();
while (!isExit) {
// 混合器沒有啟動(dòng)或數(shù)據(jù)緩存為空,則阻塞混合線程等待啟動(dòng)(數(shù)據(jù)輸入)
if (isMuxerStarted) {
// 從緩存讀取數(shù)據(jù)寫入混合器中
if (mMuxerDatas.isEmpty()) {
// PaDebugUtil.i(TAG, "run--->混合器沒有數(shù)據(jù)乳幸,阻塞線程等待");
synchronized (lock) {
try {
lock.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
} else {
MuxerData data = mMuxerDatas.remove(0);
if (data != null) {
int track = 0;
try {
if (data.trackIndex == TRACK_VIDEO) {
track = videoTrack;
// PaDebugUtil.d(TAG, "---寫入視頻數(shù)據(jù)---");
} else if (data.trackIndex == TRACK_AUDIO) {
// PaDebugUtil.d(TAG, "---寫入音頻數(shù)據(jù)---");
track = audioTrack;
}
// PaDebugUtil.d(TAG, "before SampleData presentationTimeUs: "+data.bufferInfo.presentationTimeUs);
mMuxer.writeSampleData(track, data.byteBuf, data.bufferInfo);
prevOutputPTSUs = data.bufferInfo.presentationTimeUs;
} catch (Exception e) {
PaDebugUtil.e(TAG, "寫入數(shù)據(jù)到混合器失敗滩报,track=" + track);
e.printStackTrace();
}
}
}
} else {
PaDebugUtil.i(TAG, "run--->混合器沒有啟動(dòng)欺税,阻塞線程等待");
synchronized (lock) {
try {
lock.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
stopMuxer();
}
}
其中mMuxerDatas為自定義混合器數(shù)據(jù)集合,便于MediaMuxer.writeSampleData()使用.
private Vector<MuxerData> mMuxerDatas;
/**
* 封裝要混合器數(shù)據(jù)實(shí)體
*/
public static class MuxerData {
int trackIndex;
ByteBuffer byteBuf;
MediaCodec.BufferInfo bufferInfo;
public MuxerData(int trackIndex, ByteBuffer byteBuf, MediaCodec.BufferInfo bufferInfo) {
this.trackIndex = trackIndex;
this.byteBuf = byteBuf;
this.bufferInfo = bufferInfo;
}
}
組裝數(shù)據(jù)的地方:
(EncoderVideoRunnable.java)
@SuppressLint("NewApi")
private void encoderBytes(byte[] rawFrame) {
ByteBuffer[] inputBuffers = mVideoEncodec.getInputBuffers();
ByteBuffer[] outputBuffers = mVideoEncodec.getOutputBuffers();
//返回編碼器的一個(gè)輸入緩存區(qū)句柄跷跪,-1表示當(dāng)前沒有可用的輸入緩存區(qū)
int inputBufferIndex = mVideoEncodec.dequeueInputBuffer(TIMES_OUT);
if (inputBufferIndex >= 0) {
// 綁定一個(gè)被空的摘悴、可寫的輸入緩存區(qū)inputBuffer到客戶端
ByteBuffer inputBuffer = null;
if (!isLollipop()) {
inputBuffer = inputBuffers[inputBufferIndex];
} else {
inputBuffer = mVideoEncodec.getInputBuffer(inputBufferIndex);
}
// 向輸入緩存區(qū)寫入有效原始數(shù)據(jù),并提交到編碼器中進(jìn)行編碼處理
inputBuffer.clear();
inputBuffer.put(rawFrame);
mVideoEncodec.queueInputBuffer(inputBufferIndex, 0, rawFrame.length, getPTSUs(), 0);
}
// 返回一個(gè)輸出緩存區(qū)句柄饮怯,當(dāng)為-1時(shí)表示當(dāng)前沒有可用的輸出緩存區(qū)
// mBufferInfo參數(shù)包含被編碼好的數(shù)據(jù)蝌衔,timesOut參數(shù)為超時(shí)等待的時(shí)間
int outputBufferIndex = -1;
MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
do {
outputBufferIndex = mVideoEncodec.dequeueOutputBuffer(mBufferInfo, TIMES_OUT);
if (outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
// PaDebugUtil.i(TAG, "獲得編碼器輸出緩存區(qū)超時(shí)");
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
// 如果API小于21,APP需要重新綁定編碼器的輸入緩存區(qū)蝌蹂;
// 如果API大于21噩斟,則無(wú)需處理INFO_OUTPUT_BUFFERS_CHANGED
if (!isLollipop()) {
outputBuffers = mVideoEncodec.getOutputBuffers();
}
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// 編碼器輸出緩存區(qū)格式改變,通常在存儲(chǔ)數(shù)據(jù)之前且只會(huì)改變一次
// 這里設(shè)置混合器視頻軌道孤个,如果音頻已經(jīng)添加則啟動(dòng)混合器(保證音視頻同步)
MediaFormat newFormat = mVideoEncodec.getOutputFormat();
AiMediaMuxer mMuxerUtils = muxerRunnableRf.get();
if (mMuxerUtils != null) {
mMuxerUtils.setMediaFormat(AiMediaMuxer.TRACK_VIDEO, newFormat);
PaDebugUtil.i(TAG, "編碼器輸出緩存區(qū)格式改變剃允,添加視頻軌道到混合器");
}
} else {
// 獲取一個(gè)只讀的輸出緩存區(qū)inputBuffer ,它包含被編碼好的數(shù)據(jù)
ByteBuffer outputBuffer = null;
if (!isLollipop()) {
outputBuffer = outputBuffers[outputBufferIndex];
} else {
outputBuffer = mVideoEncodec.getOutputBuffer(outputBufferIndex);
}
// 如果API<=19,需要根據(jù)BufferInfo的offset偏移量調(diào)整ByteBuffer的位置
// 并且限定將要讀取緩存區(qū)數(shù)據(jù)的長(zhǎng)度斥废,否則輸出數(shù)據(jù)會(huì)混亂
if (isKITKAT()) {
outputBuffer.position(mBufferInfo.offset);
outputBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
}
// 根據(jù)NALU類型判斷幀類型
AiMediaMuxer mMuxerUtils = muxerRunnableRf.get();
int type = outputBuffer.get(4) & 0x1F;
// PaDebugUtil.d(TAG, "------還有數(shù)據(jù)---->" + type);
if (type == 7 || type == 8) {
// PaDebugUtil.e(TAG, "------PPS椒楣、SPS幀(非圖像數(shù)據(jù))憔四,忽略-------");
mBufferInfo.size = 0;
} else if (type == 5) {
// 錄像時(shí)探膊,第1秒畫面會(huì)靜止,這是由于音視軌沒有完全被添加
// Muxer沒有啟動(dòng)
// PaDebugUtil.e(TAG, "------I幀(關(guān)鍵幀)-------");
if (mMuxerUtils != null && mMuxerUtils.isMuxerStarted()) {
// mBufferInfo.presentationTimeUs = getPTSUs();
mMuxerUtils.addPreviewData(
new AiMediaMuxer.MuxerData(AiMediaMuxer.TRACK_VIDEO, outputBuffer, mBufferInfo));
prevPresentationTimes = mBufferInfo.presentationTimeUs;
isAddKeyFrame = true;
// PaDebugUtil.e(TAG, "----------->添加關(guān)鍵幀到混合器");
}
} else {
if (isAddKeyFrame) {
// PaDebugUtil.d(TAG, "------非I幀(type=1)蚂且,添加到混合器-------");
if (mMuxerUtils != null && mMuxerUtils.isMuxerStarted()) {
// mBufferInfo.presentationTimeUs = getPTSUs();
mMuxerUtils.addPreviewData(
new AiMediaMuxer.MuxerData(AiMediaMuxer.TRACK_VIDEO, outputBuffer, mBufferInfo));
prevPresentationTimes = mBufferInfo.presentationTimeUs;
// PaDebugUtil.d(TAG, "------添加到混合器");
}
}
}
// 處理結(jié)束统锤,釋放輸出緩存區(qū)資源
mVideoEncodec.releaseOutputBuffer(outputBufferIndex, false);
outputBuffer = null;
// outputBuffers = null;
// System.gc();
}
} while (outputBufferIndex >= 0);
}
錄制過(guò)程中,我們發(fā)現(xiàn)黑屏的視頻在MediaMuxer.writeSampleData()方法中catch到了異常:
MediaAdapter: "pushBuffer called before start"
我們找到MediaAdapter源碼(http://androidxref.com/7.0.0_r1/xref/frameworks/av/media/libstagefright/MediaAdapter.cpp)拋出異常的地方:
status_t MediaAdapter::pushBuffer(MediaBuffer *buffer) {
if (buffer == NULL) {
ALOGE("pushBuffer get an NULL buffer");
return -EINVAL;
}
Mutex::Autolock autoLock(mAdapterLock);
if (!mStarted) {
ALOGE("pushBuffer called before start");
return INVALID_OPERATION;
}
mCurrentMediaBuffer = buffer;
mBufferReadCond.signal();
ALOGV("wait for the buffer returned @ pushBuffer! %p", buffer);
mBufferReturnedCond.wait(mAdapterLock);
return OK;
}
這里寫明是mStarted = false的時(shí)候會(huì)拋出異常,往上查找到是調(diào)用了stop()方法后才置為false,那這里可以猜想到肯定是其他地方調(diào)用了stop()方法才導(dǎo)致的,那什么情況下會(huì)調(diào)用stop呢?
我們繼續(xù)看到adb日志里有一條:
MPEG4Writer:"do not support out of order frames (timestamp: 1892312322 < 1892312350"
我們找到MPEG4Writer源碼(http://androidxref.com/7.0.0_r1/xref/frameworks/av/media/libstagefright/MPEG4Writer.cpp)拋出異常的地方:
currDurationTicks =
((timestampUs * mTimeScale + 500000LL) / 1000000LL -
(lastTimestampUs * mTimeScale + 500000LL) / 1000000LL);
if (currDurationTicks < 0ll) {
ALOGE("do not support out of order frames (timestamp: %lld < last: %lld for %s track",
(long long)timestampUs, (long long)lastTimestampUs, trackName);
copy->release();
mSource->stop();
return UNKNOWN_ERROR;
}
通過(guò)閱讀源碼,我們發(fā)現(xiàn)這個(gè)時(shí)間戳應(yīng)該是底層寫入視頻數(shù)據(jù)時(shí)的時(shí)間戳,即我們?cè)趙riteSampleData()方法中傳入的BufferInfo的presentationTimeUs的值做了一些換算.
我們實(shí)現(xiàn)視頻數(shù)據(jù)寫入的邏輯中看到,EncoderVideoRunnable線程負(fù)責(zé)將編碼好的視頻數(shù)據(jù)交給MediaMuxerRunnable線程寫入文件.初步分析應(yīng)該是BufferInfo的presentationTimeUs在什么地方被修改了,然后我們?cè)趙riteSampleData和new MediaCodec.BufferInfo()這兩個(gè)地方都打印了BufferInfo的內(nèi)存地址和presentationTimeUs,然后發(fā)現(xiàn)在寫入視頻信息的時(shí)候BufferInfo的presentationTimeUs并不是上一次寫入的時(shí)間戳,
這里插入一段邏輯:
// 向MediaMuxer添加錄屏數(shù)據(jù)
public void addPreviewData(MuxerData data) {
if (needAddKeyPreviewData && (data.byteBuf.get(4) & 0x1F) != 5) {
return;
}
needAddKeyPreviewData = false;
if (isStopWriteDate || isReacordingScreen) {
return;
}
if (mMuxerDatas == null) {
PaDebugUtil.e(TAG, "添加數(shù)據(jù)失敗");
return;
}
data.bufferInfo.presentationTimeUs = getPTSUs();
mMuxerDatas.add(data);
// 解鎖
synchronized (lock) {
lock.notify();
}
}
/**
* 獲取下一個(gè)編碼的 presentationTimeUs
* @return
*/
public long getPTSUs() {
//long result = System.nanoTime() / 1000L;
long result = System.nanoTime();
// presentationTimeUs should be monotonic
// otherwise muxer fail to write
long time = (result - pauseDelayTime) / 1000;
if (time < prevOutputPTSUs){
return prevOutputPTSUs;
}
return time;
}
會(huì)判斷一次當(dāng)前時(shí)間戳與上一次寫入視頻信息的時(shí)間戳做一個(gè)比較取最大值,因而prevOutputPTSUs不可能比上一次小,那么問題就出在當(dāng)前presentationTimeUs在賦值正確的時(shí)間戳后去寫入視頻信息的時(shí)候,這個(gè)presentationTimeUs被更改了,這里的BufferInfo對(duì)象其實(shí)是在EncoderVideoRunnable中創(chuàng)建的,當(dāng)EncoderVideoRunnable中dequeueOutputBuffer的時(shí)候會(huì)被更改.
常規(guī)CPU運(yùn)行情況下,這種幾率幾乎可以忽略不計(jì),但是少數(shù)性能稍微差的手機(jī)就會(huì)大概率出現(xiàn)這種情況了.
這個(gè)時(shí)候只需要在dequeueOutputBuffer的時(shí)候,每次都創(chuàng)建一個(gè)新的BufferInfo對(duì)象,這樣就不會(huì)影響寫入的時(shí)候BufferInfo的presentationTimeUs被修改了.
修改后的EncoderVideoRunnable代碼:
do {
MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
outputBufferIndex = mVideoEncodec.dequeueOutputBuffer(mBufferInfo, TIMES_OUT);
if (outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
// PaDebugUtil.i(TAG, "獲得編碼器輸出緩存區(qū)超時(shí)");
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
// 如果API小于21毛俏,APP需要重新綁定編碼器的輸入緩存區(qū);
// 如果API大于21饲窿,則無(wú)需處理INFO_OUTPUT_BUFFERS_CHANGED
if (!isLollipop()) {
outputBuffers = mVideoEncodec.getOutputBuffers();
}
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// 編碼器輸出緩存區(qū)格式改變煌寇,通常在存儲(chǔ)數(shù)據(jù)之前且只會(huì)改變一次
// 這里設(shè)置混合器視頻軌道,如果音頻已經(jīng)添加則啟動(dòng)混合器(保證音視頻同步)
MediaFormat newFormat = mVideoEncodec.getOutputFormat();
AiMediaMuxer mMuxerUtils = muxerRunnableRf.get();
if (mMuxerUtils != null) {
mMuxerUtils.setMediaFormat(AiMediaMuxer.TRACK_VIDEO, newFormat);
PaDebugUtil.i(TAG, "編碼器輸出緩存區(qū)格式改變逾雄,添加視頻軌道到混合器");
}
} else {
// 獲取一個(gè)只讀的輸出緩存區(qū)inputBuffer 阀溶,它包含被編碼好的數(shù)據(jù)
ByteBuffer outputBuffer = null;
if (!isLollipop()) {
outputBuffer = outputBuffers[outputBufferIndex];
} else {
outputBuffer = mVideoEncodec.getOutputBuffer(outputBufferIndex);
}
// 如果API<=19,需要根據(jù)BufferInfo的offset偏移量調(diào)整ByteBuffer的位置
// 并且限定將要讀取緩存區(qū)數(shù)據(jù)的長(zhǎng)度鸦泳,否則輸出數(shù)據(jù)會(huì)混亂
if (isKITKAT()) {
outputBuffer.position(mBufferInfo.offset);
outputBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
}
// 根據(jù)NALU類型判斷幀類型
AiMediaMuxer mMuxerUtils = muxerRunnableRf.get();
int type = outputBuffer.get(4) & 0x1F;
// PaDebugUtil.d(TAG, "------還有數(shù)據(jù)---->" + type);
if (type == 7 || type == 8) {
// PaDebugUtil.e(TAG, "------PPS银锻、SPS幀(非圖像數(shù)據(jù)),忽略-------");
mBufferInfo.size = 0;
} else if (type == 5) {
// 錄像時(shí)辽故,第1秒畫面會(huì)靜止徒仓,這是由于音視軌沒有完全被添加
// Muxer沒有啟動(dòng)
// PaDebugUtil.e(TAG, "------I幀(關(guān)鍵幀)-------");
if (mMuxerUtils != null && mMuxerUtils.isMuxerStarted()) {
// mBufferInfo.presentationTimeUs = getPTSUs();
mMuxerUtils.addPreviewData(
new AiMediaMuxer.MuxerData(AiMediaMuxer.TRACK_VIDEO, outputBuffer, mBufferInfo));
prevPresentationTimes = mBufferInfo.presentationTimeUs;
isAddKeyFrame = true;
// PaDebugUtil.e(TAG, "----------->添加關(guān)鍵幀到混合器");
}
} else {
if (isAddKeyFrame) {
// PaDebugUtil.d(TAG, "------非I幀(type=1),添加到混合器-------");
if (mMuxerUtils != null && mMuxerUtils.isMuxerStarted()) {
// mBufferInfo.presentationTimeUs = getPTSUs();
mMuxerUtils.addPreviewData(
new AiMediaMuxer.MuxerData(AiMediaMuxer.TRACK_VIDEO, outputBuffer, mBufferInfo));
prevPresentationTimes = mBufferInfo.presentationTimeUs;
// PaDebugUtil.d(TAG, "------添加到混合器");
}
}
}
// 處理結(jié)束誊垢,釋放輸出緩存區(qū)資源
mVideoEncodec.releaseOutputBuffer(outputBufferIndex, false);
outputBuffer = null;
// outputBuffers = null;
// System.gc();
}
} while (outputBufferIndex >= 0);
其實(shí)只是在dequeueOutputBuffer前每次都創(chuàng)建新的BufferInfo.
改完運(yùn)行,發(fā)現(xiàn)問題解決了,呼呼...
最后,總結(jié)一下從發(fā)現(xiàn)問題到解決問題的全過(guò)程:
1,遇到問題不要覺得太難還沒開始就放棄思考,如果最后沒有解決問題,但是分析思路的養(yǎng)成也是非常重要.
2,盡量多分析源碼,對(duì)解決問題事半功倍.
3,代碼大忌生搬硬套,網(wǎng)上大手也有寫bug的情況,代碼抄過(guò)來(lái)要分析每一步的邏輯,養(yǎng)成好的編碼習(xí)慣.