Android手機直播(四)Android Media API

一、文章說明

最近工作實在太忙,很久沒有更新文章了,收到很多小伙伴催更的消息,心中實在慚愧,趁著今天有空趕緊更新。

第一篇文章從總體上介紹了Android手機直播,之后兩篇文章分別介紹了視頻和音頻采集,這篇文章便開始介紹編解碼相關的知識。Android提供很多和視音頻處理相關的類,熟練使用這些相關的類,其實是能實現很強大的功能。

視音頻編解碼一般分為兩種,一種是硬編實現,一種是軟編實現。這兩種方式各有優缺點,硬編性能好,但是需要對兼容性進行相應處理;軟編兼容性好,可以進行一些參數設置,但是軟編一般性能較差,引入相關的編解碼庫往往會增大app的整體體積,而且還需要寫相應的jni接口。

這篇文章主要講述使用Android原生提供的各種類來實現對視音頻進行處理,相信各位看完整篇文章后會感受到這幾個類配合使用的強大。

直播項目已經開源,開源地址:SopCastComponent
Github地址:https://github.com/SuperJim123

二、幾個類

很多時候我們往往會忽略很多事情,就比如說Android系統已經給我們提供了對視音頻的強大支持,我們往往還不知道,沒有專心去研究。這篇文章先介紹幾個和視音頻相關的類,通過這幾個類的組合使用,其實是能變換出許多視音頻處理的相關功能,下面就對這幾個類進行簡單介紹。

MediaMetadataRetriever::用來獲取視頻的相關信息,例如視頻寬高、時長、旋轉角度、碼率等等。
MediaExtractor::視音頻分離器,將一些格式的視頻分離出視頻軌道和音頻軌道。
MediaCodec:視音頻相應的編解碼類。
MediaMuxer:視音頻合成器,將視頻和音頻合成相應的格式。
MediaFormat:視音頻相應的格式信息。
MediaCodec.BufferInfo:存放ByteBuffer相應信息的類。
MediaCrypto:視音頻加密解密處理的類。
MediaCodecInfo:視音頻編解碼相關信息的類。

MediaFormat和MediaCodec.BufferInfo是串起上面幾個類的橋梁,上面幾個視音頻處理的類通過這兩個橋梁建立起聯系,從而變化出相應的功能,認真分析的話會感覺到Google設計的精妙。

三、MediaMetadataRetriever

MediaMetadataRetriever用來獲取視音頻的相關信息,MediaMetadataRetriever的使用十分簡單,傳入相應的文件路徑創建MediaMetadataRetriever,之后便可以得到視頻的相關參數。

MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever();
metadataRetriever.setDataSource(file.getAbsolutePath());
String widthString = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
if(!TextUtils.isEmpty(widthString)) {
    width = Integer.valueOf(widthString);
}
String heightString = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
if(!TextUtils.isEmpty(heightString)) {
    height = Integer.valueOf(heightString);
}
String durationString = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
if(!TextUtils.isEmpty(durationString)) {
    duration = Long.valueOf(durationString);
}
String bitrateString = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE);
if(!TextUtils.isEmpty(bitrateString)) {
    bitrate = Integer.valueOf(bitrateString);
}
String degreeStr = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
if (!TextUtils.isEmpty(degreeStr)) {
    degree = Integer.valueOf(degreeStr);
}
metadataRetriever.release();

四、MediaExtractor

MediaExtractor用來對視音頻進行分離,對文件中的視頻音頻軌道進行分離能做大量的事情,比如說要寫一個播放器,那么首先的第一個步驟是分離出視頻音頻軌道,然后進行相應的處理。MediaExtractor的創建和MediaMetadataRetriever一樣十分簡單,只需要傳入相應的文件路徑。通過getTrackCount()可以得到相應的軌道數量,一般情況下視音頻軌道都有,有些時候可能只有視頻,有些時候可能只有音頻。軌道的序號從0開始,通過getTrackFormat(int index)方法可以得到相應的MediaFormat,而通過MediaFormat可以判斷出軌道是視頻還是音頻。通過selectTrack(int index)方法選擇相應序號的軌道。

public static MediaExtractor createExtractor(String path) throws IOException {
    MediaExtractor extractor;
    File inputFile = new File(path);   // must be an absolute path
    if (!inputFile.canRead()) {
        throw new FileNotFoundException("Unable to read " + inputFile);
    }
    extractor = new MediaExtractor();
    extractor.setDataSource(inputFile.toString());
    return extractor;
}

public static String getMimeTypeFor(MediaFormat format) {
    return format.getString(MediaFormat.KEY_MIME);
}

public static int getAndSelectVideoTrackIndex(MediaExtractor extractor) {
    for (int index = 0; index < extractor.getTrackCount(); ++index) {
        if (isVideoFormat(extractor.getTrackFormat(index))) {
            extractor.selectTrack(index);
            return index;
        }
    }
    return -1;
}

public static int getAndSelectAudioTrackIndex(MediaExtractor extractor) {
    for (int index = 0; index < extractor.getTrackCount(); ++index) {
        if (isAudioFormat(extractor.getTrackFormat(index))) {
            extractor.selectTrack(index);
            return index;
        }
    }
    return -1;
}

public static boolean isVideoFormat(MediaFormat format) {
    return getMimeTypeFor(format).startsWith("video/");
}

public static boolean isAudioFormat(MediaFormat format) {
    return getMimeTypeFor(format).startsWith("audio/");
}

選擇好一個軌道后,便可以通過相應方法提取出相應軌道的數據。extractor.seekTo(startTime, SEEK_TO_PREVIOUS_SYNC)方法可以直接跳轉到開始解析的位置。extractor.readSampleData(byteBuffer, 0)方法則可以將數據解析到byteBuffer中。extractor.advance()方法則將解析位置進行前移,準備下一次解析。

下面是MediaExtractor一般的使用方法。

MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(...);
int numTracks = extractor.getTrackCount();
for (int i = 0; i < numTracks; ++i) {
    MediaFormat format = extractor.getTrackFormat(i);
    String mime = format.getString(MediaFormat.KEY_MIME);
    if (weAreInterestedInThisTrack) {
        extractor.selectTrack(i);
    }
}
ByteBuffer inputBuffer = ByteBuffer.allocate(...)
while (extractor.readSampleData(inputBuffer, ...) >= 0) {
    int trackIndex = extractor.getSampleTrackIndex();
    long presentationTimeUs = extractor.getSampleTime();
    ...
    extractor.advance();
}

extractor.release();
extractor = null;

五、MediaCodec

MediaCodec是Android視音頻里面最為重要的類,它主要實現的功能是對視音頻進行編解碼處理。在編碼方面,可以對采集的視音頻數據進行編碼處理,這樣的話可以對數據進行壓縮,從而實現以較少的數據量存儲視音頻信息。在解碼方面,可以解碼相應格式的視音頻數據,從而得到原始的可以渲染的數據,從而實現視音頻的播放。

一般場景下音頻使用的是AAC-LC的格式,而視頻使用的是H264格式。這兩種格式在MediaCodec支持的版本(Api 16)也都得到了很好的支持。在直播過程中,先采集視頻和音頻數據,然后將原始的數據塞給編碼器進行硬編,然后得到相應的編碼后的AAC-LC和H264數據。

在Android系統中,MediaCodec支持的格式有限,在使用MediaCodec之前需要對硬編類型的支持進行檢測,如果MediaCodec支持再進行使用。

1、檢查

在使用硬編編碼器之前需要對編碼器支持的格式進行檢查,在Android中可以使用MediaCodecInfo這個類來獲取系統對視音頻硬編的支持情況。

下面的代碼是判斷MediaCodec是否支持某個MIME:

private static MediaCodecInfo selectCodec(String mimeType) {
     int numCodecs = MediaCodecList.getCodecCount();
     for (int i = 0; i < numCodecs; i++) {
         MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
         if (!codecInfo.isEncoder()) {
             continue;
         }
         String[] types = codecInfo.getSupportedTypes();
         for (int j = 0; j < types.length; j++) {
             if (types[j].equalsIgnoreCase(mimeType)) {
                 return codecInfo;
             }
         }
     }
     return null;
 }

根據之前的講述,在Android系統中有著不同的顏色格式,有著各種類型的YUV顏色格式和RGB顏色格式。在攝像頭采集的文章中已經講述,需要設置攝像頭采集的圖像顏色格式,一般來說設置為ImageFormat.NV21,之后在攝像頭PreView的回調中得到相應的圖像數據。

在Android系統中不同手機中的編碼器支持著不同的顏色格式,一般情況下并不直接支持NV21的格式,這時候需要將NV21格式轉換成為編碼器支持的顏色格式。在攝像頭采集的文章中已經詳細講述YUV圖像格式和相應的存儲規則,YUV圖像格式的轉換可以使用LibYuv。

這里說一下MediaCodec支持的圖像格式。一般來說Android MediaCodec支持如下幾種格式:

/**
     * Returns true if this is a color format that this test code understands (i.e. we know how
     * to read and generate frames in this format).
     */
    private static boolean isRecognizedFormat(int colorFormat) {
        switch (colorFormat) {
            // these are the formats we know how to handle for this test
            case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar:
            case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar:
            case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar:
            case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar:
            case MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar:
                return true;
            default:
                return false;
        }
    }

之前有看過一篇文章,里面大致統計了Android各種手機MediaCodec支持的各種顏色格式,上面5個類型是比較常用的類型。

另外MediaCodec支持Surface的方式輸入和輸出,當編碼的時候只需要在Surface上進行繪制就可以輸入到編碼器,而解碼的時候可以將解碼圖像直接輸出到Surface上,使用起來相當方便,需要在Api 18或以上。

2、創建

當需要使用MediaCodec的時候,首先需要根據視音頻的類型創建相應的MediaCodec。在直播項目中視頻使用了H264,而音頻使用了AAC-LC。在Android中創建直播的音頻編碼器需要傳入相應的MIME,AAC-LC對應的是audio/mp4a-latm,而H264對應的是video/avc。如下的代碼展示了兩個編碼器的創建,其中視頻編碼器的輸入設置成為了Surface的方式。

    //Audio
    public static MediaCodec getAudioMediaCodec() throws IOException {
        int size = AudioRecord.getMinBufferSize(44100, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
        MediaFormat format = MediaFormat.createAudioFormat("audio/mp4a-latm", 44100, 1);
        format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
        format.setInteger(MediaFormat.KEY_BIT_RATE, 64 * 1000);
        format.setInteger(MediaFormat.KEY_SAMPLE_RATE, 44100);
        format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, size);
        format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
        MediaCodec mediaCodec = MediaCodec.createEncoderByType("audio/mp4a-latm");
        mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        return mediaCodec;
    }
    //Video
    public static MediaCodec getVideoMediaCodec() throws IOException {
        int videoWidth = getVideoSize(1280);
        int videoHeight = getVideoSize(720);
        MediaFormat format = MediaFormat.createVideoFormat("video/avc", videoWidth, videoHeight);
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        format.setInteger(MediaFormat.KEY_BIT_RATE, 1300* 1000);
        format.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
        format.setInteger(MediaFormat.KEY_BITRATE_MODE,MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
        format.setInteger(MediaFormat.KEY_COMPLEXITY,MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
        MediaCodec mediaCodec = MediaCodec.createEncoderByType("video/avc");
        mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        return mediaCodec;
    }

    // We avoid the device-specific limitations on width and height by using values that
    // are multiples of 16, which all tested devices seem to be able to handle.
    public static int getVideoSize(int size) {
        int multiple = (int) Math.ceil(size/16.0);
        return multiple*16;
    }
3、使用

MediaCodec創建之后,需要通過start()方法進行開啟。MediaCodec有輸入緩沖區隊列和輸出緩沖區隊列,不斷通過往輸入緩沖區隊列傳遞數據,經過MediaCodec處理后就可以得到響應的輸出數據。當在編碼的時候,需要向輸入緩沖區傳入采集到的原始的視音頻數據,然后獲取輸出緩沖區的數據,輸出出來的數據也就是編碼處理后的數據。當在解碼的時候,往輸入緩沖區輸入需要解碼的數據,然后獲取輸出緩沖區的數據,輸出出來的數據也就是解碼后得到的原始的視音頻數據。當需要清空輸入和輸出緩沖區的時候,可以調用MediaCodec的flush()方法。當編碼或者解碼結束時,通過往輸入緩沖區輸入帶結束標記的數據,然后從輸出緩沖區可以得到這個結束標記,從而完成整個編解碼過程。下面一張圖片很好地展示了MediaCodec的狀態變化。


MediaCodec狀態

對于MediaCodec通過處理輸入的數據,從而得到輸出數據。MediaCodec通過一系列的輸入和輸出緩沖區來處理數據。如下圖所示,輸入客戶端通過查詢得到空的輸入緩沖區,然后往里面填充數據,然后將輸入緩沖區傳遞給MediaCodec;輸出客戶端通過查詢得到塞滿的輸出緩沖區,然后得到里面的數據,然后通知MediaCodec釋放這個輸出緩沖區。


MediaCodec過程

在API 21及以后可以通過下面這種異步的方式來使用MediaCodec。

 MediaCodec codec = MediaCodec.createByCodecName(name);
 MediaFormat mOutputFormat; // member variable
 codec.setCallback(new MediaCodec.Callback() {
   @Override
   void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
     ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
     // fill inputBuffer with valid data
     …
     codec.queueInputBuffer(inputBufferId, …);
   }

   @Override
   void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) {
     ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
     MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
     // bufferFormat is equivalent to mOutputFormat
     // outputBuffer is ready to be processed or rendered.
     …
     codec.releaseOutputBuffer(outputBufferId, …);
   }

   @Override
   void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {
     // Subsequent data will conform to new format.
     // Can ignore if using getOutputFormat(outputBufferId)
     mOutputFormat = format; // option B
   }

   @Override
   void onError(…) {
     …
   }
 });
 codec.configure(format, …);
 mOutputFormat = codec.getOutputFormat(); // option B
 codec.start();
 // wait for processing to complete
 codec.stop();
 codec.release();

從API 21開始,可以使用下面這種同步的方式來使用MediaCodec。

 MediaCodec codec = MediaCodec.createByCodecName(name);
 codec.configure(format, …);
 MediaFormat outputFormat = codec.getOutputFormat(); // option B
 codec.start();
 for (;;) {
   int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
   if (inputBufferId >= 0) {
     ByteBuffer inputBuffer = codec.getInputBuffer(…);
     // fill inputBuffer with valid data
     …
     codec.queueInputBuffer(inputBufferId, …);
   }
   int outputBufferId = codec.dequeueOutputBuffer(…);
   if (outputBufferId >= 0) {
     ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
     MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
     // bufferFormat is identical to outputFormat
     // outputBuffer is ready to be processed or rendered.
     …
     codec.releaseOutputBuffer(outputBufferId, …);
   } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
     // Subsequent data will conform to new format.
     // Can ignore if using getOutputFormat(outputBufferId)
     outputFormat = codec.getOutputFormat(); // option B
   }
 }
 codec.stop();
 codec.release();

在API版本21之前,獲取緩沖區的方式有所不同,不能直接得到相應的緩沖區,需要根據索引序號從緩沖區列表中得到相應的緩沖區,具體的代碼如下所示:

 MediaCodec codec = MediaCodec.createByCodecName(name);
 codec.configure(format, …);
 codec.start();
 ByteBuffer[] inputBuffers = codec.getInputBuffers();
 ByteBuffer[] outputBuffers = codec.getOutputBuffers();
 for (;;) {
   int inputBufferId = codec.dequeueInputBuffer(…);
   if (inputBufferId >= 0) {
     // fill inputBuffers[inputBufferId] with valid data
     …
     codec.queueInputBuffer(inputBufferId, …);
   }
   int outputBufferId = codec.dequeueOutputBuffer(…);
   if (outputBufferId >= 0) {
     // outputBuffers[outputBufferId] is ready to be processed or rendered.
     …
     codec.releaseOutputBuffer(outputBufferId, …);
   } else if (outputBufferId == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
     outputBuffers = codec.getOutputBuffers();
   } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
     // Subsequent data will conform to new format.
     MediaFormat format = codec.getOutputFormat();
   }
 }
 codec.stop();
 codec.release();

當數據輸入結束的時候,通過queueInputBuffer的輸入標記設置為BUFFER_FLAG_END_OF_STREAM來通知MediaCodec結束編碼。當MediaCodec作為編碼器的時候, dequeueOutputBuffer方法能夠得到當前編碼輸出緩沖區數據的相關信息,這些信息存儲在bufferInfo里面,通過bufferInfo信息能夠得到數據的真實長度,當前數據為關鍵幀或者非關鍵幀等等信息。

當使用Output Surface作為解碼的輸出的時候,可以根據以下情況來設置是否將視頻渲染到Surface上。

releaseOutputBuffer(bufferId, false)  //不渲染buffer里面的數據
releaseOutputBuffer(bufferId, true)  //渲染buffer里面的數據
releaseOutputBuffer(bufferId, timestamp)  //在特定時間渲染buffer里面的數據

當使用Input Surface作為編碼器輸入的時候,不允許使用dequeueInputBuffer。當輸入結束的時候,使用signalEndOfInputStream()來使得編碼器停止。

六、MediaMuxer

前面講述了MediaExtractor(視音頻分離器),現在講述MediaMuxer(視音頻合成器)。MediaMuxer是Android提供的視音頻合成器,目前只支持mp4和webm兩種格式的視音頻合成。一般來時視音頻媒體都有視頻軌道和音頻軌道,有些時候也還有字母軌道,MediaMuxer將這些軌道糅合在一起存儲在一個文件中。
MediaMuxer在Android中一個最常使用的場景是錄制mp4文件。一般來說當存儲為mp4文件時,視頻軌道一般是經過編碼處理后的h264視頻,音頻軌道一般是經過編碼后處理的aac音頻。前面已經講述了如何對采集的視頻和音頻進行硬編,那么這時候如果對硬編后的視頻和音頻使用MediaMuxer進行合成,那么就可以合成為mp4文件。

下面是MediaMuxer一般的使用方法。

MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4);
// 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;
        muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo);
    }
};
muxer.stop();
muxer.release();

其實上面的注釋很好說明了MediaMuxer的使用場景。視音頻軌道的初始化需要傳入MediaFormat,而MediaFormat可以通過MediaCodec.getOutputFormat()獲?。ú杉筮M行硬編得到MediaFormat),也可以通過MediaExtractor.getTrackFormat()獲取(分離器分離出視音頻得到MediaFormat)。上面包含了兩個應用場景,一個是采集,一個是轉碼。

七、結合

MediaExtractor和MediaCodec結合使用可以實現視頻的播放功能,MediaCodec和MediaMuxer結合使用可以實現視頻的錄制功能,MediaExtractor、MediaCodec和MediaMuxer三者一起使用可以實現視頻的轉碼功能。下面講述一下這幾個功能的實現。

1、視音頻錄制

之前講述了視頻的采集和音頻的采集,將采集到的視音頻通過MediaCodec進行編碼處理,之后將編碼數據傳遞到MediaMuxer進行合成,也就完成了視音頻錄制的功能。


視頻錄制

根據視音頻采集的相關參數創建MediaCodec,當MediaCodec的outputBufferId為INFO_OUTPUT_FORMAT_CHANGED時,可以通過codec.getOutputFormat()得到相應的MediaFormat,之后便可以用這個MediaFormat為MediaMuxer添加相應的視音頻軌道。通過codec.dequeueOutputBuffer(…)可以得到編碼后的數據的bufferInfo信息和相應的數據,之后將這個數據和bufferInfo通過muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo)傳遞給Muxer,也就將整個視音頻數據合成到了mp4中。

2、視音頻播放
視音頻播放

利用Android提供的Media API來實現一個播放器也是可以的,實際上Google著名的開源項目ExoPlayer就是這么做的。

上面的示意圖簡要描述了一個簡單的本地播放器的結構。利用MediaExtractor分離視音頻文件,得到相應的音頻軌道和視頻軌道。之后通過MediaExtractor從相應的軌道中獲取數據,并且將這些數據傳遞給MediaCodec的輸入緩沖區,經過MediaCodec的解碼便可以得到相應的原始數據。音頻解碼后可以得到PCM數據,從而可以傳遞給AudioTrack進行播放。視頻解碼后可以渲染到相應的Surface,這個Surface可以是通過SurfaceTexture創建,而SurfaceTexture是可以通過紋理創建的,從而將解碼后的視頻數據傳遞到紋理上了。

MediaExtractor解析視音頻文件,可以得到相應數據的pts,之后pts可以傳輸到MediaCodec,之后在MediaCodec的輸出里面可以得到相應的pts,之后在根據視音頻的pts來控制視音頻的渲染,從而實現視音頻的同步。

3、視音頻轉碼

視音頻的轉碼,其實就是通過MediaExtractor解析相應的文件,之后得到相應的視頻軌道和音頻軌道,之后將軌道里的數據傳輸到MediaCodec進行解碼,然后將解碼后的數據進行相應的處理(例如音頻變聲、視頻裁剪、視頻濾鏡),之后將處理后的數據傳遞給MediaCodec進行編碼,最后利用MediaMuxer將視頻軌道和音頻軌道進行合成,從而完成了整個轉碼過程。


視音頻轉碼

八、展望

文中講述了,如何使用Media API進行相應的錄制、播放、轉碼,講述了如何將視音頻編解碼和紋理相結合,但是由于篇幅原因并未講述如何通過OpenGL對視頻進行相應的處理,在之后的文章中會進一步講述。
文中講述了很多如何實現的原理和過程,但是并未提供相應的實現源碼,其實這些程序的編寫并不復雜,之后會寫相應的開源代碼,寫完之后會更新到這篇文章上。

九、相關鏈接

Android手機直播(一)總覽
Android手機直播(二)攝像機
Android手機直播(三)聲音采集
Android手機直播(四)Android Media API

十、結束語

終于寫完了,各位看官覺得文章不錯的話不妨點個喜歡~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容