一、文章說明
最近工作實在太忙,很久沒有更新文章了,收到很多小伙伴催更的消息,心中實在慚愧,趁著今天有空趕緊更新。
第一篇文章從總體上介紹了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釋放這個輸出緩沖區。
在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
十、結束語
終于寫完了,各位看官覺得文章不錯的話不妨點個喜歡~