了解了音視頻的編解碼過程,我們接下來使用一下經(jīng)常跟MediaCodec一起搭配的MediaExtractor和MediaMuxer。最后會使用一個簡單的demo來了解具體了解這兩個工具類的使用過程。這一節(jié)我們就先不講MediaCodec了,放到下節(jié)的demo。
一、MediaExtractor
Android提供了一個MediaExtractor類,可以用來分離容器中的視頻track和音頻track。
主要API介紹:
- setDataSource(String path):即可以設置本地文件又可以設置網(wǎng)絡文件
- getTrackCount():得到源文件通道數(shù)?
- getTrackFormat(int index):獲取指定(index)的通道格式
- getSampleTime():返回當前的時間戳?
- readSampleData(ByteBuffer byteBuf, int offset):把指定通道中的數(shù)據(jù)按偏移量讀取到ByteBuffer中;
- advance():讀取下一幀數(shù)據(jù)
- release(): 讀取結束后釋放資源
MediaExtractor 的使用主要有這么幾步:
- 設置數(shù)據(jù)源
- 獲取通道數(shù),切換到想要的軌道
- 循環(huán)讀取每幀的樣本數(shù)據(jù)
- 完成后釋放資源
二、MediaMuxer
MediaMuxer的作用是生成音頻或視頻文件;還可以把音頻與視頻混合成一個音視頻文件。
相關API介紹:
- MediaMuxer(String path, int format):path:輸出文件的名稱 ?format:輸出文件的格式;當前只支持MP4格式;
- addTrack(MediaFormat format):添加通道;我們更多的是使用MediaCodec.getOutpurForma()或Extractor.getTrackFormat(int index)來獲取MediaFormat;也可以自己創(chuàng)建;
- start():開始合成文件
- writeSampleData(int trackIndex, ByteBuffer byteBuf, MediaCodec.BufferInfo bufferInfo):把ByteBuffer中的數(shù)據(jù)寫入到在構造器設置的文件中;
- stop():停止合成文件
- release():釋放資源
參數(shù) | |
---|---|
int |
MUXER_OUTPUT_3GPP 3GPP媒體文件格式 |
int |
MUXER_OUTPUT_HEIF HEIF媒體文件格式 |
int |
MUXER_OUTPUT_MPEG_4 MPEG4媒體文件格式 |
int |
MUXER_OUTPUT_OGG Ogg媒體文件格式 |
int |
MUXER_OUTPUT_WEBM WEBM媒體文件格式 |
MediaMuxer的使用步驟:
- 設置目標文件路徑和音視頻格式
- 添加要合成的軌道,包括音軌和視軌
- 開始合成,循環(huán)寫入每幀樣本數(shù)據(jù)
- 完成后釋放
三、MediaFormat
????????用MediaCodec來進行編解碼,在創(chuàng)建MediaCodec時需要調用configure方法進行配置,Mediaformat則是configure需要傳入的一個參數(shù)。
?3.1 視頻類型的Mediaformat
可以通過如下代碼創(chuàng)建視頻類型Mediaformat:
MediaFormat videoFormat = MediaFormat.createVideoFormat(videoType, width, height);
方法的參數(shù)類型:
- videoType常用的有兩種:
????????MediaFormat.MIMETYPE_VIDEO_AVC(H.264)
????????MediaFormat.MIMETYPE_VIDEO_HEVC(H.265)
- width和height需要根據(jù)底層支持的分辨率來設置,如果width和height設置的不符合要求會出現(xiàn)如下錯誤:
E/CameraCaptureSession: Session 1: Failed to create capture session; configuration failed
對于視頻類型而言有下列四個配置是必須指定的:手動配置和直接獲取原視頻的配置
// 指定編碼器顏色格式
videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
// 指定幀率
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
// 指定比特率
videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, 10000000);
//指定關鍵幀時間間隔,一般設置為每秒關鍵幀
videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
?3.2 音頻類型的Mediaformat
可以通過如下代碼創(chuàng)建音頻類型Mediaformat:
MediaFormat audioFormat = MediaFormat.createAudioFormat(audioType, sampleRate, channelCount);
方法的參數(shù)類型:
- audioType:常用的是MediaFormat.MIMETYPE_AUDIO_AAC
- sampleRate:采樣率
- channelCount:聲道數(shù)量
單聲道 channelCount=1 , 雙聲道 channelCount=2
對于音頻類型而言有一個配置是必須指定的:
//音頻比特率(碼率)
audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
四、MediaCodec.BufferInfo?
????????用于描述解碼得到的byte[]數(shù)據(jù)的相關信息,每緩沖區(qū)元數(shù)據(jù)包括指定相關編解碼器(輸出)緩沖區(qū)中有效數(shù)據(jù)范圍的偏移量和大小。
主要有四個屬性:
- int?flags :與緩沖區(qū)關聯(lián)的緩沖區(qū)標志。
- int?offset :緩沖區(qū)中數(shù)據(jù)的起始偏移量。
- long?presentationTimeUs :緩沖區(qū)的顯示時間戳,以微秒計。這是從相應的輸入緩沖區(qū)傳入的表示時間戳中獲得的。對于大小為0的緩沖區(qū),應該忽略這一點。
- int?size :緩沖區(qū)中的數(shù)據(jù)量(以字節(jié)為單位)。如果這是
0
緩沖區(qū)中沒有數(shù)據(jù),可以丟棄。大小為0的緩沖區(qū)的唯一用途是攜帶流結束標記。
flags詳解:
- 與緩沖區(qū)關聯(lián)的緩沖區(qū)標志。...的結合
MediaCodec.BUFFER_FLAG_KEY_FRAME
和MediaCodec.BUFFER_FLAG_END_OF_STREAM
.- 作為關鍵幀的編碼緩沖區(qū)標有
MediaCodec.BUFFER_FLAG_KEY_FRAME
.- 對應于輸入緩沖區(qū)的最后一個輸出緩沖區(qū)用
MediaCodec.BUFFER_FLAG_END_OF_STREAM
也將標有MediaCodec.BUFFER_FLAG_END_OF_STREAM
。在某些情況下,這可能是一個空緩沖區(qū),其唯一目的是攜帶流結束標記。值是
0
或以下各項的組合MediaCodec.BUFFER_FLAG_SYNC_FRAME
,?MediaCodec.BUFFER_FLAG_KEY_FRAME
,?MediaCodec.BUFFER_FLAG_CODEC_CONFIG
,?MediaCodec.BUFFER_FLAG_END_OF_STREAM
,?MediaCodec.BUFFER_FLAG_PARTIAL_FRAME
、以及Android . media . media codec . buffer _ FLAG _ MUXER _ DATA
- ?BUFFER_FLAG_CODEC_CONFIG?? 常數(shù)值:2:這表明如此標記的緩沖區(qū)包含編解碼器初始化/編解碼器特定數(shù)據(jù),而不是媒體數(shù)據(jù)。
- BUFFER_FLAG_END_OF_STREAM 常數(shù)值:4:這表示流的結束,即在此之后將沒有緩沖器可用,當然,除非,flush()如下。
- BUFFER_FLAG_KEY_FRAME 常數(shù)值:1:這表明如此標記的(編碼的)緩沖區(qū)包含關鍵幀的數(shù)據(jù)。
- BUFFER_FLAG_PARTIAL_FRAME 常數(shù)值:8:這表示緩沖區(qū)只包含一幀的一部分,解碼器應該對數(shù)據(jù)進行批處理,直到在解碼該幀之前出現(xiàn)一個沒有該標志的緩沖區(qū)。
- BUFFER_FLAG_SYNC_FRAME 常數(shù)值:1:這表明如此標記的(編碼的)緩沖區(qū)包含關鍵幀的數(shù)據(jù)。API 21中不贊成使用此常量。 使用BUFFER_FLAG_KEY_FRAME相反,都是關鍵幀。
五、MediaExtractor和MediaMuxer結合的demo
實現(xiàn)音視頻的解封裝和封裝的過程:
//實現(xiàn)音視頻的解封裝和封裝的過程
public class MediaCodecDemo extends Activity {
//顯示解封裝后的視頻和音頻在SD卡保存的位置
private TextView tv_out;
private final String mVideoPath = Environment.getExternalStorageDirectory()
+ "/Pictures/送孟浩然之廣陵.mp4";
//解封裝和封裝在本地使用文件名
private final String inputAudio = "audio1.aac";
private final String outPutVideo = "video1.mp4";
private static final String TAG1 ="解封裝MediaExtractor:" ;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_media_codec);
initView();
//提取視頻分離出純音頻和純視頻文件
extractorAndMuxerMP4(mVideoPath);
//重新合成成音視頻文件
muxerMp4(inputAudio,outPutVideo);
}
private void initView() {
tv_out = findViewById(R.id.tv_out);
}
}
//提取視頻分離出純音頻和純視頻文件
private void extractorAndMuxerMP4(String url){
//提取數(shù)據(jù)(解封裝)
//1. 構造MediaExtractor
MediaExtractor mediaExtractor = new MediaExtractor();
try {
//2.設置數(shù)據(jù)源,數(shù)據(jù)源可以是本地文件地址,也可以是網(wǎng)絡地址:
mediaExtractor.setDataSource(url);
//3.獲取軌道數(shù)
int trackCount = mediaExtractor.getTrackCount();
//遍歷軌道,查看音頻軌或者視頻軌道信息
for (int i = 0; i < trackCount; i++) {
//4. 獲取某一軌道的媒體格式
MediaFormat trackFormat = mediaExtractor.getTrackFormat(i);
String keyMime = trackFormat.getString(MediaFormat.KEY_MIME);
if (TextUtils.isEmpty(keyMime)) {
continue;
}
//5.通過mime信息識別音軌或視頻軌道,打印相關信息
//(默認的是先掃描到視頻,在掃描到音頻)
if (keyMime.startsWith("video/")) {
File outputFile = extractorAndMuxer(mediaExtractor, i, "/video.mp4");
tv_out.setText("純視頻文件路徑:" + outputFile.getAbsolutePath());
} else if (keyMime.startsWith("audio/")) {
File outputFile = extractorAndMuxer(mediaExtractor, i, "/audio.aac");
tv_out.setText(tv_out.getText().toString() + "\n純音頻路徑:"
+ outputFile.getAbsolutePath());
tv_out.setVisibility(View.VISIBLE);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
????????通過getTrackFormat(int index)來獲取各個track的MediaFormat,通過MediaFormat來獲取track的詳細信息,如:MimeType、分辨率、采樣頻率、幀率等等
//確定是音軌或視頻軌道后,文件輸出
private File extractorAndMuxer(MediaExtractor mediaExtractor, int i, String outputName) throws IOException{
//獲取傳過來的MediaExtractor對應軌道的trackFormat
MediaFormat trackFormat = mediaExtractor.getTrackFormat(i);
MediaMuxer mediaMuxer;
//選擇軌道
mediaExtractor.selectTrack(i);
File outputFile = new File(getExternalFilesDir(Environment.DIRECTORY_MUSIC).getAbsolutePath() + outputName);
if (outputFile.exists()) {
//如果文件存在,就刪除
outputFile.delete();
}
//1. 構造MediaMuxer
mediaMuxer = new MediaMuxer(outputFile.getAbsolutePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
//2. 添加軌道信息 參數(shù)為MediaFormat
mediaMuxer.addTrack(trackFormat);
//3. 開始合成
mediaMuxer.start();
//4. 設置buffer
ByteBuffer buffer = ByteBuffer.allocate(500 * 1024);//設置每一幀的大小
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
//5.通過mediaExtractor.readSampleData讀取數(shù)據(jù)流
int sampleSize = 0;
//循環(huán)讀取每幀的樣本數(shù)據(jù)
//mediaExtractor.readSampleData(buffer, 0)把指定通道中的數(shù)據(jù)按偏移量讀取到ByteBuffer中
while ((sampleSize = mediaExtractor.readSampleData(buffer, 0)) > 0) {
bufferInfo.flags = mediaExtractor.getSampleFlags();
bufferInfo.offset = 0;
bufferInfo.size = sampleSize;
bufferInfo.presentationTimeUs = mediaExtractor.getSampleTime();
//所有解碼的幀都已渲染,我們現(xiàn)在可以停止播放了,雖然這里沒有用到
//一般的使用方法是判斷 isEOS是否等于0;
//int isEOS = bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM;
//判斷輸出數(shù)據(jù)是否為關鍵幀的方法:
//boolean keyFrame = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0;
//6. 把通過mediaExtractor解封裝的數(shù)據(jù)通過writeSampleData寫入到對應的軌道
mediaMuxer.writeSampleData(0, buffer, bufferInfo);
//讀取下一幀數(shù)據(jù)
mediaExtractor.advance();
}
Log.i(TAG1, "extractorAndMuxer: " + outputName + "提取封裝完成");
mediaExtractor.unselectTrack(i);
//6.關閉
mediaMuxer.stop();
mediaMuxer.release();
return outputFile;
}
這里需要科普一下兩個正數(shù)進行&運算:兩個正數(shù)進行&運算的值永遠小于或等于最小的數(shù)。
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { Log.i(TAG, "OutputBuffer BUFFER_FLAG_END_OF_STREAM"); break; }
????????我們知道BUFFER_FLAG_END_OF_STREAM代表的是2^2,也就是0100。因為是&運算,我們只用關注info.flags二進制第三位即可:為0即上面判斷為false,為1即上面判斷為true。
- 正數(shù)與負數(shù)的與運算:負數(shù)的異或操作需要先把數(shù)轉換成補碼才行(頭不變取反+1)
兩個數(shù)互為相反數(shù)位與操作可有特殊用途,位與所剩恰為最低位。
兩個數(shù)互為相反數(shù)異或可能有特殊用途,異或后,所剩最低位左移一位。
到此我們就將音視頻解封裝成了音頻和視頻,并且保存在了指定文件當中,我們分析一下流程:
- 構造MediaExtractor(不需要參數(shù)) —> 之后的操作使用try/catch包圍 —> setDateSource(url)設置本地或者網(wǎng)絡資源?—> getTrackConut()獲取該資源的通道數(shù) —> for循環(huán)通道數(shù) —>?獲取某一軌道的媒體格式:getTrackFormat(i)返回一個MediaFormat?—>?判斷是什么通道根據(jù)trackFormat.getString(MediaFormat.KEY_MIME)返回ketMime的startsWith("?")?。
- 接下來的操作就確定了音軌和視頻軌道,同時確定文件的輸出地點。
- 構造MediaMuxer (需要指定文件和格式)—>?addTrack(trackFormat)添加軌道信息 參數(shù)為MediaFormat,注意這里的MediaFormat要是對應的軌道?—> start()開始合成?—>?設置ByteBuffer,用于緩存一幀數(shù)據(jù)?—>?MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo()獲取bufferInfo?—>?通過mediaExtractor.readSampleData讀取數(shù)據(jù)流,同時也作為一個while循環(huán)的判斷條件?—> 配置bufferInfo的四個屬性?—> 把通過mediaExtractor解封裝的數(shù)據(jù)通過mediaMuxer.writeSampleData寫入到對應的軌道?—> 讀取下一幀audioExtractor.advance()。
- 循環(huán)結束之后mediaExtractor.unselectTrack(i)釋放選擇?—>?mediaMuxer.stop()停止?—> 最后釋放mediaMuxer和mediaExtractor。
接下來我們開始合成操作。
//把音軌和視頻軌再合成新的視頻
private String muxerMp4(String inputAudio , String outPutVideo){
File videoFile = new File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), "video.mp4");
File audioFile = new File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), inputAudio);
File outputFile = new File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), outPutVideo);
if (outputFile.exists()) {
outputFile.delete();
}
if (!videoFile.exists()) {
Toast.makeText(this, "視頻源文件不存在", Toast.LENGTH_SHORT).show();
return "";
}
if (!audioFile.exists()) {
Toast.makeText(this, "音頻源文件不存在", Toast.LENGTH_SHORT).show();
return "";
}
MediaExtractor videoExtractor = new MediaExtractor();
MediaExtractor audioExtractor = new MediaExtractor();
try {
MediaMuxer mediaMuxer = new MediaMuxer(outputFile.getAbsolutePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
int videoTrackIndex = 0;
int audioTrackIndex = 0;
//先添加視頻軌道
videoExtractor.setDataSource(videoFile.getAbsolutePath());
int trackCount = videoExtractor.getTrackCount();
for (int i = 0; i < trackCount; i++) {
MediaFormat trackFormat = videoExtractor.getTrackFormat(i);
String mimeType = trackFormat.getString(MediaFormat.KEY_MIME);
if (TextUtils.isEmpty(mimeType)) {
continue;
}
if (mimeType.startsWith("video/")) {
videoExtractor.selectTrack(i);
videoTrackIndex = mediaMuxer.addTrack(trackFormat);
break;
}
}
//再添加音頻軌道
audioExtractor.setDataSource(audioFile.getAbsolutePath());
int trackCountAduio = audioExtractor.getTrackCount();
for (int i = 0; i < trackCountAduio; i++) {
MediaFormat trackFormat = audioExtractor.getTrackFormat(i);
String mimeType = trackFormat.getString(MediaFormat.KEY_MIME);
if (TextUtils.isEmpty(mimeType)) {
continue;
}
if (mimeType.startsWith("audio/")) {
audioExtractor.selectTrack(i);
audioTrackIndex = mediaMuxer.addTrack(trackFormat);
Log.i(TAG1, "muxerToMp4: audioTrackIndex=" + audioTrackIndex);
break;
}
}
//再進行合成
mediaMuxer.start();
ByteBuffer byteBuffer = ByteBuffer.allocate(500 * 1024);
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int sampleSize = 0;
while ((sampleSize = videoExtractor.readSampleData(byteBuffer, 0)) > 0) {
bufferInfo.flags = videoExtractor.getSampleFlags();
bufferInfo.offset = 0;
bufferInfo.size = sampleSize;
bufferInfo.presentationTimeUs = videoExtractor.getSampleTime();
mediaMuxer.writeSampleData(videoTrackIndex, byteBuffer, bufferInfo);
videoExtractor.advance();
}
int audioSampleSize = 0;
MediaCodec.BufferInfo audioBufferInfo = new MediaCodec.BufferInfo();
while ((audioSampleSize = audioExtractor.readSampleData(byteBuffer, 0)) > 0) {
audioBufferInfo.flags = audioExtractor.getSampleFlags();
audioBufferInfo.offset = 0;
audioBufferInfo.size = audioSampleSize;
audioBufferInfo.presentationTimeUs = audioExtractor.getSampleTime();
mediaMuxer.writeSampleData(audioTrackIndex, byteBuffer, audioBufferInfo);
audioExtractor.advance();
}
//最后釋放資源
videoExtractor.release();
audioExtractor.release();
mediaMuxer.stop();
mediaMuxer.release();
} catch (IOException e) {
e.printStackTrace();
return "";
}
return outputFile.getAbsolutePath();
}
????????因為這個與上面例子的流程大致相同,上面看懂了,下面基本上沒什么問題,所以注釋相對比較少。至于過程也就懶得分析了。
????????我們在解封裝的過程中同時使用到了MediaExtractor和MediaMuxer,包括合成的時候也用了這兩個。不要想當然的認為MediaExtractor解封裝出來兩文件,兩文件根據(jù)MediaMuxer就可以合成!??!
????????最后遺留兩個問題:
????????1.解封裝出來的是不同軌道的資源,可是當做文件輸出時,除了文件名不同其他的操作都是一模一樣,就連mediaMuxer的參數(shù)格式都是MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4,那音頻文件是怎樣合成成功的???
????????AAC代表Advanced Audio Coding(高級音頻編碼),是一種由MPEG-4標準定義的有損音頻壓縮格式。而且解封裝出來的音頻acc和視頻MP4改了后綴都可以正常播放。
????????
? ? ? ? 音頻文件同樣可以理解為一幀幀的說法,之后我回使用實時AAC音頻幀并通過AudioTrack來播放,盡情期待。文章來源:http://www.zghlxwxcb.cn/news/detail-429527.html
? ? ? ? 2.分解出來的軌道是固定的嗎?還是根據(jù)自定義來的?他的個數(shù)只能是一個音頻一個視頻嗎?文章來源地址http://www.zghlxwxcb.cn/news/detail-429527.html
- 分解出來的軌道不是固定的但一般是兩個軌道(一個音頻一個視頻)
E/測試Demo: 軌道數(shù)量 = 2 E/測試Demo: 0編號通道格式 = video/avc E/測試Demo: 1編號通道格式 = audio/mp4a-latm
- 這個具體的順序就是根據(jù)你使用mediaMuxer添加合成的順序
- 當然也可能有多個音頻和視頻在一個盒子里
到了這里,關于Android音視頻開發(fā)(三)——MediaExtractor和MediaMuxer的使用的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!