上兩期咱們已經學習了關於視頻播放的基礎知識,還有容器格式文件的結構。那麼今天終於能夠開始學習安卓平臺的視頻播放知識了!相信你們早就已經等不及了。 java
可是千萬不要小看以前兩章的基礎知識,理解他們對咱們接下來學習安卓平臺的Codec API大有益處。若是沒有理解以前的章節最好仍是仔細再複習一遍。。。。:smile:git
這一章咱們會如此安排github
1.Android 平臺視頻播放API的變遷歷史數組
2.Android 新 Media API的使用app
3.一個使用新Media API播放視頻的例子ide
在好久好久之前。。。。oop
咳咳咳,言重了。。。。post
在2012年之前,安卓平臺的視頻播放,一直都是很是簡單的事情(對於大部分開發者來講,由於大部分開發者都不須要深刻底層MediaPlayer Service),學習
很是簡單,建立播放器對象,注入URL,播放,播放完畢以後release。。。。。google
這給安卓開發者帶來了很是大的便利,應用代碼也很是少。能夠說,在2011年以前(尤爲是直播業務還沒爆火以前),這款Native Player仍是很好用的。
可是這款播放器的缺點也很是顯而易見。
1.不少格式的容器文件不支持,也不支持自適應視頻播放(Adaptive Streaming)
2.應用開發者很難debug播放器,MediaPlayer的代碼不少都是Native Method。並不在Java層。
3.很難作自定義的拓展和設置,好比緩衝的大小,下載進度等等。
正是由於MediaPlayer自己的實現對開發者是徹底透明的,因此它也愈來愈神祕,也逐漸跟不上如今的業務對播放器的需求了。
因此谷歌也意識到了這一點,在2012年的Google IO大會上,谷歌宣佈了Android Jelly Bean,也就是4.3以後,安卓平臺release新的Media Codec API組。這些API再也不像以前傻瓜式的MediaPlayer同樣,而是把API組件設計的面向視頻播放的更底層概念。好比,編解碼API,容器文件讀取器Extractor API等等。
以上的圖都是從Google IO大會的視頻進行截圖而來的。咱們能夠從結構圖裏看出,原來的MediaPlayer把Extractor,和Codec API所有封鎖在了Framework層,應用層徹底接觸不到。在新的API設計裏面,這些都挪到了應用層(其實雖然MediaCodec API,就是編解碼API還在Framework,可是應用層能夠調用他們)
在全新的Media API裏面,最最最重要的就是MediaExtractor和MediaCodec這兩個類,第一個能夠對容器文件進行讀取控制,第二個就是對數據進行編解碼的API。
MediaExtractor
MediaExtractor能夠同一個URL,獲取容器文件的軌道數量,軌道信息(Track)。在肯定了軌道信息以後,能夠選擇想要解碼的軌道(只能選擇一個,因此音軌和視頻軌道須要兩個不一樣MediaExtractor給兩個不一樣MediaCodec解碼),再從該軌道不停的讀取數據放入MediaCodec API進行解碼。
MediaCodec
MediaCodec API則是建立的時候就須要選擇Codec的類型。而後編碼的時候須要安卓平臺顯示視頻的Surface,MediaCrypto對象(若是視頻被加密的話,這個細節我會在DRM章節介紹)。
一個MediaCodec在建立以後會在內部維護兩個對列(Queue),一個是InputQueue,一個是OutputQueue。相似生產者消費者的模式,MediaCodec會不停的從InputQueue獲取數據(InputQueue的數據又是又MediaExtractor提供),解碼,再把解碼以後的數據放入OutputQueue,再提供給Surface讓其視頻內容。
這兩個類協做的方式以下圖
那麼咱們是時候看看源代碼了!咱們此次使用的是谷歌一個非官方維護的開源項目,叫grafika。這個項目實際上是一個Demo app,裏面使用新的Media API作了不少有意思的小實例。其中就包括咱們此次要看的,使用MediaAPI播放視頻的例子。這裏只有三個方法,調用順序也是依次進行。
public void playWithUrl() throws IOException {
MediaExtractor extractor = null;
MediaCodec decoder = null;
try {
/** * 建立一個MediaExtractor對象 */
extractor = new MediaExtractor();
/** * 設置Extractor的source,這裏能夠把mp4的url傳進來, */
extractor.setDataSource(context, Uri.parse(url),new HashMap<String, String>());
/** * 這裏咱們須要選擇咱們要解析的軌道,咱們在這個例子裏面只解析視頻軌道 */
int trackIndex = selectTrack(extractor);
if (trackIndex < 0) {
throw new RuntimeException("No video track found in " + url);
}
/** * 選擇視頻軌道的索引 */
extractor.selectTrack(trackIndex);
/** * 獲取軌道的音視頻格式,這個格式和Codec有關,能夠點擊MediaFormat類看看有哪些 */
MediaFormat format = extractor.getTrackFormat(trackIndex);
String mime = format.getString(MediaFormat.KEY_MIME);
/** * 建立一個MediaCodec對象 */
decoder = MediaCodec.createDecoderByType(mime);
/** * 設置格式,和視頻輸出的Surface,開始解碼 */
decoder.configure(format, mOutputSurface, null, 0);
decoder.start();
doExtract(extractor, trackIndex, decoder, mFrameCallback);
}
catch ( Exception e ){
e.printStackTrace();
}
finally {
// release everything we grabbed
if (decoder != null) {
decoder.stop();
decoder.release();
decoder = null;
}
if (extractor != null) {
extractor.release();
extractor = null;
}
}
}
複製代碼
/** * 咱們用Extractor獲取軌道數量,而後遍歷他們,只要找到第一個軌道是Video的就返回 */
private static int selectTrack(MediaExtractor extractor) {
// Select the first video track we find, ignore the rest.
int numTracks = extractor.getTrackCount();
for (int i = 0; i < numTracks; i++) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("video/")) {
if (VERBOSE) {
Log.d(TAG, "Extractor selected track " + i + " (" + mime + "): " + format);
}
return i;
}
}
return -1;
}
複製代碼
private void doExtract(MediaExtractor extractor, int trackIndex, MediaCodec decoder, FrameCallback frameCallback) {
final int TIMEOUT_USEC = 10000;
/** * 獲取MediaCodec的輸入隊列,是一個數組 */
ByteBuffer[] decoderInputBuffers = decoder.getInputBuffers();
int inputChunk = 0;
long firstInputTimeNsec = -1;
boolean outputDone = false;
boolean inputDone = false;
/** * 用while作循環 */
while (!outputDone) {
if (VERBOSE) Log.d(TAG, "loop");
if (mIsStopRequested) {
Log.d(TAG, "Stop requested");
return;
}
// Feed more data to the decoder.
/** * 不停的輸入數據知道輸入隊列滿爲止 */
if (!inputDone) {
/** * 這個方法返回輸入隊列數組能夠放數據的位置,即一個索引 */
int inputBufIndex = decoder.dequeueInputBuffer(TIMEOUT_USEC);
/** * 若是輸入隊列還有位置 */
if (inputBufIndex >= 0) {
if (firstInputTimeNsec == -1) {
firstInputTimeNsec = System.nanoTime();
}
ByteBuffer inputBuf = decoderInputBuffers[inputBufIndex];
// Read the sample data into the ByteBuffer. This neither respects nor
// updates inputBuf's position, limit, etc.
/** * 用Extractor讀取一個sample的數據,而且放入輸入隊列 */
int chunkSize = extractor.readSampleData(inputBuf, 0);
/** * 若是chunk size是小於0,證實咱們已經讀取完畢這個軌道的數據了。 */
if (chunkSize < 0) {
// End of stream -- send empty frame with EOS flag set.
decoder.queueInputBuffer(inputBufIndex, 0, 0, 0L,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
inputDone = true;
if (VERBOSE) Log.d(TAG, "sent input EOS");
}
else {
if (extractor.getSampleTrackIndex() != trackIndex) {
Log.w(TAG, "WEIRD: got sample from track " +
extractor.getSampleTrackIndex() + ", expected " + trackIndex);
}
long presentationTimeUs = extractor.getSampleTime();
decoder.queueInputBuffer(inputBufIndex, 0, chunkSize,
presentationTimeUs, 0 /*flags*/);
if (VERBOSE) {
Log.d(TAG, "submitted frame " + inputChunk + " to dec, size=" +
chunkSize);
}
inputChunk++;
/** * Extractor移動一個sample的位置,下一次再調用extractor.readSampleData()就會讀取下一個sample */
extractor.advance();
}
} else {
if (VERBOSE) Log.d(TAG, "input buffer not available");
}
}
if (!outputDone) {
/** * 開始把輸出隊列的數據拿出來,decodeStatus只要不是大於零的整數都是異常的現象,須要處理 */
int decoderStatus = decoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
if (decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
// no output available yet
if (VERBOSE) Log.d(TAG, "no output from decoder available");
} else if (decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
// not important for us, since we're using Surface
if (VERBOSE) Log.d(TAG, "decoder output buffers changed");
} else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat newFormat = decoder.getOutputFormat();
if (VERBOSE) Log.d(TAG, "decoder output format changed: " + newFormat);
} else if (decoderStatus < 0) {
throw new RuntimeException(
"unexpected result from decoder.dequeueOutputBuffer: " +
decoderStatus);
} else { // decoderStatus >= 0
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
if (VERBOSE) Log.d(TAG, "output EOS");
outputDone = true;
}
boolean doRender = (mBufferInfo.size != 0);
if (doRender && frameCallback != null) {
frameCallback.preRender(mBufferInfo.presentationTimeUs);
}
/** * 只要咱們調用了decoder.releaseOutputBuffer(), * 就會把輸出隊列的數據所有輸出到Surface上顯示,而且釋放輸出隊列的數據 */
decoder.releaseOutputBuffer(decoderStatus, doRender);
}
}
}
}
複製代碼
固然,你們可能會有不少問題,好比,你說了可拓展性呢?Extractor不仍是隻能讀取指定的格式?等等等等的問題。我會再接下來的幾章慢慢的講解,經過谷歌的開源播放器ExoPlayer,咱們能夠深刻到如何使用,拓展這些API。下一章我會先講解自適應視頻的概念,而後會經過Exoplayer的例子來闡述如何使用Media API播放自適應視頻。
good night!