1. 程式人生 > >Android 音視訊任務4

Android 音視訊任務4

任務4. 學習 Android 平臺的 MediaExtractor 和 MediaMuxer API,知道如何解析和封裝 mp4 檔案

  • MediaMuxer和MediaCodec算是比較年輕的,它們是JB 4.1和JB 4.3才引入的。前者用於將音訊和視訊進行混合生成多媒體檔案。缺點是目前只能支援一個audio track和一個video track,目前支援mp4,3gp,webm輸出
  • MediaCodec用於將音視訊進行壓縮編碼,它有個比較牛X的地方是可以對Surface內容進行編碼,如KK 4.4中螢幕錄影功能就是用它實現的。
  • MediaFormat用於描述多媒體資料的格式。
  • MediaRecorder用於錄影+壓縮編碼,生成編碼好的檔案如mp4, 3gpp,視訊主要是用於錄製Camera preview。
  • MediaPlayer用於播放壓縮編碼後的音視訊檔案。
  • AudioRecord用於錄製PCM資料。
  • AudioTrack用於播放PCM資料。PCM即原始音訊取樣資料,可以用如vlc播放器播放。

MediaExtractor可以從資料來源中提取解複用的,編碼後的媒體資料。MediaExtractor用於音視訊分路,比如從一個視訊中分別提取音訊(音軌)和視訊(視訊軌)
它既可以從視訊裡提取視訊軌或音訊軌,也可以單從音訊裡提取音訊軌

//如下使用`

MediaExtractor extractor = new MediaExtractor();
 extractor.setDataSource();
int 
 int numTracks = extractor.getTrackCount(); //當前視訊中有幾個軌道
 for (int i = 0; i < numTracks; ++i) {
   MediaFormat format = extractor.getTrackFormat(i);
   String mime = format.getString(MediaFormat.KEY_MIME);
   boolean
isVideoTrack = mime.startsWith("video/); //當前是不是視訊軌,視訊軌的MIME以”video/“開頭,音訊軌的MIME以“audio/”開頭 if (isVideoTrack) { extractor.selectTrack(i); //選擇視軌,當確定感興趣的軌道時,一定要選取! int index = mediaMuxer.addTrack(format) //如果要進行這個視軌的合成合成器除了需要資料,也需要mediaMuxer需要設定當前視軌的format,在寫入的時候也需要視軌的index } } ByteBuffer inputBuffer = ByteBuffer.allocate() //這個容量必須大一點,否則下面readSampleData會崩,實測是1000*1024可以 //下面readSampleData會自動更新buffer的limit和postion,就和read,write一樣 while (extractor.readSampleData(inputBuffer, ...) >= 0) { int trackIndex = extractor.getSampleTrackIndex(); long presentationTimeUs = extractor.getSampleTime(); ... extractor.advance(); //前進到下一個樣本(下一個視訊幀或音訊幀);readSampleData看起來不會自動更新讀過的資料所以需要這個 } extractor.release(); extractor = null;

MediaMuxer可以複用基本流。 目前MediaMuxer支援MP4,Webm和3GP檔案作為輸出。 它還支援自Android Nougat以來在MP4中複用B幀。是extractor的反作用型別,用於把視訊軌和音訊軌進行合成,和MediaExtractor正好是反過程。
不支援mp3,wav音訊源(AAC支援)
只能支援一個audio track和一個video track,

//如下使用
MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4);
mediaMuxer.setOrientationHint(90); //設定混合後視訊的旋轉角度
 // More often, the MediaFormat will be retrieved from MediaCodec.getOutputFormat()
 // or MediaExtractor.getTrackFormat().
 MediaFormat audioFormat = new MediaFormat(...);
 MediaFormat videoFormat = new MediaFormat(...);
 int audioTrackIndex = muxer.addTrack(audioFormat); //返回的是混合器裡的軌道號,也就是新檔案裡的軌道號
 int videoTrackIndex = muxer.addTrack(videoFormat);
 ByteBuffer inputBuffer = ByteBuffer.allocate(bufferSize);
 boolean finished = false;
 BufferInfo bufferInfo = new BufferInfo();

 muxer.start(); //開始混合
 while(!finished) {
   // getInputBuffer() will fill the inputBuffer with one frame of encoded
   /* sample from either MediaCodec or MediaExtractor, set isAudioSample to true when the sample is audio data, set up all the fields of bufferInfo,and return true if there are no more samples.*/
   finished = getInputBuffer(inputBuffer, isAudioSample, bufferInfo);
   if (!finished) {
     int currentTrackIndex = isAudioSample ? audioTrackIndex : videoTrackIndex;
/*
bufferInfo.presentationTimeUs = extractor.getSampleTime(); //直接從extractor獲取
bufferInfo.offset = 0;  //如果沒有特殊需求一般是0
bufferInfo.flags = extractor.getSampleFlags();
bufferInfo.size = size;  //本次寫入的資料量
*/
     muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo); //每次寫入資料都要同時寫入index和info,info要明確如上面幾點,注意這裡的index是新合成的視訊的相應軌道,應該是由addTrack返回的值
   }
 };
 muxer.stop();
 muxer.release();

元資料跟蹤
每幀元資料用於攜帶與視訊或音訊相關的額外資訊以便於離線處理,例如,來自感測器的陀螺儀訊號可以在進行離線處理時幫助穩定視訊。僅在MP4容器中支援元資料跟蹤。新增新元資料軌道時,軌道的mime格式必須以字首“application /”開頭,例如“application/gyro”。元資料的格式/佈局將由應用程式定義。寫入元資料與編寫視訊/音訊資料幾乎相同,只是資料不會來自mediacodec。應用程式只需要將包含元資料的位元組緩衝區以及相關的時間戳傳遞給writeSampleData(int,ByteBuffer,MediaCodec.BufferInfo)api。時間戳必須與視訊和音訊的時間基準相同。生成的MP4檔案使用ISOBMFF的第12.3.3.2節中定義的TextMetaDataSampleEntry來表示元資料的mime格式。當使用MediaExtractor提取具有元資料軌道的檔案時,元資料的mime格式將被提取到MediaFormat中。
//如下例,把陀螺儀資訊也傳給生成的MP4

MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4);

   // SetUp Video/Audio Tracks.

   MediaFormat audioFormat = new MediaFormat(...);

   MediaFormat videoFormat = new MediaFormat(...);

   int audioTrackIndex = muxer.addTrack(audioFormat);

   int videoTrackIndex = muxer.addTrack(videoFormat);



   // Setup Metadata Track

   MediaFormat metadataFormat = new MediaFormat(...);

   metadataFormat.setString(KEY_MIME, "application/gyro");

   int metadataTrackIndex = muxer.addTrack(metadataFormat);



   muxer.start();

   while(..) {

       // Allocate bytebuffer and write gyro data(x,y,z) into it.

       ByteBuffer metaData = ByteBuffer.allocate(bufferSize);

       metaData.putFloat(x);

       metaData.putFloat(y);

       metaData.putFloat(z);

       BufferInfo metaInfo = new BufferInfo();

       // Associate this metadata with the video frame by setting

       // the same timestamp as the video frame.

       metaInfo.presentationTimeUs = currentVideoTrackTimeUs;

       metaInfo.offset = 0;

       metaInfo.flags = 0;

       metaInfo.size = bufferSize;

       muxer.writeSampleData(metadataTrackIndex, metaData, metaInfo);

   };

   muxer.stop();

   muxer.release();

 }

MediaCodec類可用於訪問低階媒體編解碼器,即編碼器/解碼器元件。 它是Android低階多媒體支援基礎架構的一部分(通常與MediaExtractor,MediaSync,MediaMuxer,MediaCrypto,MediaDrm,Image,Surface和AudioTrack一起使用。)

從廣義上講,編解碼器處理輸入資料以生成輸出資料。它非同步處理資料並使用一組輸入和輸出緩衝區。在簡單的級別,您請求(或接收)一個空的輸入緩衝區,用資料填充它並將其傳送到編解碼器進行處理。編解碼器使用資料並將其轉換到它的空輸出緩衝區之一。最後,您請求(或接收)到一個填充了資料的輸出緩衝區,使用其內容並將其釋放回編解碼器。

資料型別
編解碼器對三種資料進行操作:壓縮資料,原始音訊資料和原始視訊資料。可以使用ByteBuffers處理所有三種資料,但是您應該使用Surface for raw視訊資料來提高編解碼器效能。 Surface使用本機視訊緩衝區而不對映或將它們複製到ByteBuffers;因此,效率更高。使用Surface時通常無法訪問原始視訊資料,但您可以使用ImageReader類訪問不安全的解碼(原始)視訊幀。這可能仍然比使用ByteBuffers更有效,因為一些本機緩衝區可能會對映到直接ByteBuffers。使用ByteBuffer模式時,可以使用Image類和getInput / OutputImage(int)訪問原始視訊幀。

原始音訊緩衝區
原始音訊緩衝區包含整個PCM音訊資料幀,這是通道順序中每個通道的一個樣本。 每個樣本都是本機位元組順序的16位有符號整數。

 short[] getSamplesForChannel(MediaCodec codec, int bufferId, int channelIx) {

   ByteBuffer outputBuffer = codec.getOutputBuffer(bufferId);

   MediaFormat format = codec.getOutputFormat(bufferId);

   ShortBuffer samples = outputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer();

   int numChannels = formet.getInteger(MediaFormat.KEY_CHANNEL_COUNT);

   if (channelIx < 0 || channelIx >= numChannels) {

     return null;

   }

   short[] res = new short[samples.remaining() / numChannels];

   for (int i = 0; i < res.length; ++i) {

     res[i] = samples.get(i * numChannels + channelIx);

   }

   return res;

 }

//音訊抽取後的format資訊
0 = {[email protected]} "mime" -> "audio/mp4a-latm"
1 = {[email protected]} "aac-profile" -> "2"
2 = {[email protected]} "channel-count" -> "2"
3 = {[email protected]} "track-id" -> "1"
4 = {[email protected]} "durationUs" -> "192911760"
5 = {[email protected]} "csd-0" -> "java.nio.HeapByteBuffer[pos=0 lim=2 cap=2]"
6 = {[email protected]} "sample-rate" -> "44100"

//視訊抽取後的format資訊
0 = {[email protected]} "csd-1" -> "java.nio.HeapByteBuffer[pos=0 lim=8 cap=8]"
1 = {[email protected]} "rotation-degrees" -> "90"
2 = {[email protected]} "track-id" -> "1"
3 = {[email protected]} "height" -> "1200"
4 = {[email protected]} "profile" -> "1"
5 = {[email protected]} "color-standard" -> "1"
6 = {[email protected]} "durationUs" -> "2157877"
7 = {[email protected]} "color-transfer" -> "3"
8 = {[email protected]} "mime" -> "video/avc"
9 = {[email protected]} "frame-rate" -> "30"
10 = {[email protected]} "width" -> "1600"
11 = {[email protected]} "color-range" -> "2"
12 = {[email protected]} "max-input-size" -> "123106"
13 = {[email protected]} "csd-0" -> "java.nio.HeapByteBuffer[pos=0 lim=21 cap=21]"
14 = {[email protected]} "level" -> "2048"

這裡要注意的是,要分清楚原視訊中的視/音軌號和新合成的視訊中的視/音軌號,一般來說前者是為了讓extractor選中相應的軌道,而後者是在合成視訊寫資料的時候需要。這裡犯了一個錯就是提取視訊軌的時候,視訊軌在視訊中的軌道號是0,提取音訊幀時,音軌在音訊中的軌道號也是0,實際給muxer新增軌道的時候,視軌被新增到了新視訊的軌道0,音軌被新增到了新視訊的軌道1。但寫音訊資料的時候仍往0號軌道寫,就崩掉了報錯:stop muxer failed

具體工程程式碼見 https://github.com/lujianyun06/VATask/tree/master/app/src/main/java/com/example/lll/va/task4