前言
此篇博客講解MediaExtractor將一個視頻文件分離視頻與音頻,如果你對MediaExtractor還沒有一個籠統的概念建議先了解我的另一篇入門博客:https://www.cnblogs.com/guanxinjing/p/11378133.html
直接上代碼
已經大量注釋了就不另外切分講解了... 另外注意,實際項目里請將這些放到線程中操作.
private void separate() { mFile = new File(getExternalCacheDir(), "demo.mp4"); if (!mFile.exists()) { Log.e(TAG, "mp4文件不存在"); return; } MediaExtractor extractor = new MediaExtractor();//實例一個MediaExtractor try { extractor.setDataSource(mFile.getAbsolutePath());//設置添加MP4文件路徑 } catch (IOException e) { e.printStackTrace(); } int trackCount = extractor.getTrackCount();//獲得通道數量 int videoTrackIndex = 0;//視頻軌道索引 MediaFormat videoMediaFormat = null;//視頻格式 int audioTrackIndex = 0;//音頻軌道索引 MediaFormat audioMediaFormat = null; /** * 查找需要的視頻軌道與音頻軌道index */ for (int i = 0; i < trackCount; i++) { //遍歷所以軌道 MediaFormat itemMediaFormat = extractor.getTrackFormat(i); String itemMime = itemMediaFormat.getString(MediaFormat.KEY_MIME); if (itemMime.startsWith("video")) { //獲取視頻軌道位置 videoTrackIndex = i; videoMediaFormat = itemMediaFormat; continue; } if (itemMime.startsWith("audio")) { //獲取音頻軌道位置 audioTrackIndex = i; audioMediaFormat = itemMediaFormat; continue; } } File videoFile = new File(getExternalCacheDir(), "video.h264"); File audioFile = new File(getExternalCacheDir(), "audio.acc"); if (videoFile.exists()) { videoFile.delete(); } if (audioFile.exists()) { audioFile.delete(); } try { FileOutputStream videoOutputStream = new FileOutputStream(videoFile); FileOutputStream audioOutputStream = new FileOutputStream(audioFile); /** * 分離視頻 */ int maxVideoBufferCount = videoMediaFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);//獲取視頻的輸出緩存的最大大小 ByteBuffer videoByteBuffer = ByteBuffer.allocate(maxVideoBufferCount); extractor.selectTrack(videoTrackIndex);//選擇到視頻軌道 int len = 0; while ((len = extractor.readSampleData(videoByteBuffer, 0)) != -1) { byte[] bytes = new byte[len]; videoByteBuffer.get(bytes);//獲取字節 videoOutputStream.write(bytes);//寫入字節 videoByteBuffer.clear(); extractor.advance();//預先加載后面的數據 } videoOutputStream.flush(); videoOutputStream.close(); extractor.unselectTrack(videoTrackIndex);//取消選擇視頻軌道 /** * 分離音頻 */ int maxAudioBufferCount = audioMediaFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);//獲取音頻的輸出緩存的最大大小 ByteBuffer audioByteBuffer = ByteBuffer.allocate(maxAudioBufferCount); extractor.selectTrack(audioTrackIndex);//選擇音頻軌道 len = 0; while ((len = extractor.readSampleData(audioByteBuffer, 0)) != -1) { byte[] bytes = new byte[len]; audioByteBuffer.get(bytes); /** * 添加adts頭 */ byte[] adtsData = new byte[len + 7]; addADTStoPacket(adtsData, len+7); System.arraycopy(bytes, 0, adtsData, 7, len); audioOutputStream.write(bytes); audioByteBuffer.clear(); extractor.advance(); } audioOutputStream.flush(); audioOutputStream.close(); } catch (FileNotFoundException e) { Log.e(TAG, "separate: 錯誤原因=" + e); e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } extractor.release();//釋放資源 } private static void addADTStoPacket(byte[] packet, int packetLen) { /* 標識使用AAC級別 當前選擇的是LC 一共有1: AAC Main 2:AAC LC (Low Complexity) 3:AAC SSR (Scalable Sample Rate) 4:AAC LTP (Long Term Prediction) */ int profile = 2; int frequencyIndex = 0x04; //設置采樣率 int channelConfiguration = 2; //設置頻道,其實就是聲道 // fill in ADTS data packet[0] = (byte) 0xFF; packet[1] = (byte) 0xF9; packet[2] = (byte) (((profile - 1) << 6) + (frequencyIndex << 2) + (channelConfiguration >> 2)); packet[3] = (byte) (((channelConfiguration & 3) << 6) + (packetLen >> 11)); packet[4] = (byte) ((packetLen & 0x7FF) >> 3); packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F); packet[6] = (byte) 0xFC; }
注意! 在分離音頻后並沒有adts頭. 所以這需要我們手動導入. 如果不太了解什么是adts可以參考https://www.cnblogs.com/guanxinjing/p/11438181.html
結果:
一些你可能會碰到的坑
坑1.
在上面的代碼中,有下面2行代碼會坑...ByteBuffer.allocate();的值,不是跟創建byte[] 一樣隨便輸一個固定值.... 因為視頻或者音頻流的一幀字節大小是強制輸出固定大小的.. 如果你ByteBuffer.allocate(1*1024);寫成這樣肯定會報錯,因為極有可能在執行extractor.readSampleData()時放不下輸出的一幀流字節而報錯... 所以最正確的方式是使用videoMediaFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);獲取當前輸出一幀的流的字節大小.. 這樣既不會因為申請過量內存而浪費也不會因為申請內存太小而報錯
int maxVideoBufferCount = videoMediaFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);//獲取視頻的輸出緩存的最大大小 ByteBuffer videoByteBuffer = ByteBuffer.allocate(maxVideoBufferCount);
坑2.
請無視extractor.readSampleData(videoByteBuffer, 0)這個方法里的第二個參數,因為只需要輸入0,根本不需要設置.注釋里也沒有這個參數的說明,真實情況是使用extractor.advance();方法來跳到下一幀的數據.. 簡直莫名其妙....
end