在 Android 上擷取 Video Frame 的方法

之前有需要很精準的在 Android 上擷取 video 中的 frames,並且轉成 Bitmap 來使用。一開始覺得是個很簡單的工作,因為 Android SDK 有個 class 叫做 MediaMetadataRetriever,裡面就有 getFrameAtTime 的 API 啊。真好。結果不是這麼一回事耶!想不到在 Multimedia 應用這麼盛行的時代了,Android Media 的 API 還這麼不堪用啊?
好吧,來簡單說說我測試的幾個方法:
Use MediaMetadataRetriever getFrameAtTime
這個 API 乍看之下超棒的,實際用起來其實滿令人挫折的,一開始的確很容易可以拿到一張 Bitmap 但是後來就會發現不管丟進去的時間參數是什麼其實它也只會吐給你同一張。有時候更糟的是吐回來的 Bitmap 直接就是 null 了,但至少 null 是死的比較乾脆一點。所以如果去找出 Android Framework 中對應的 code 來看就會發現: (ref: gitorious)
getFrameAtTime 最終是 call 到 stagefrighMedatadataRetriever 的 extractVideoFrameWithCodecFlags,而它根本不管你丟的時間啊。有 thumbnail time 的話就一直 decode 同一張給你,沒有的話就給你空的,完全符合結果,這 API 只能讓 Gallery 拿來展示 video thumbnail 用而已。這 API 從 Level 10 之後就開出來了,一直是這樣的 implementation 實在是 …
Use MediaPlayer
上述方法不 work ,於是轉向 MediaPlayer,方法是 create 一個 TextureView 傳給 MediaPlayer 畫,然後對 MediaPlayer seek 到我想要的時間點之後去 call TextureView 的 gitBitmap API 取得 Bitmap。這個方法基本上都沒問題,而且也都是走 hardware solution(除了 getBitmap 需要把 GPU memory copy 到 CPU memory 之外),唯一(也是致命)的問題是無法精準的控制時間! 設定一個 seek complete 的 listener 給 MediaPlayer 然後在收到 seek complete 之後去 getBitmap 發現 frame 沒動。如果讓 MP 開始播放然後定時去拿 frame 的話可以 work,但完全無法控制中間 delay 的時間,所以也是個不堪用的方法啊。
Use MediaCodec
Goolge 自從 JellyBean 之後在 Java 層開放了一個很低階的 decoder/encoder API,叫做 MediaCodec,去年的 Google I/O 2012 有一個 session 在介紹這個 API。大致上 android media 相關的都還是架構在 OpenMX 上,所以這個 MediaCodec API 底層實作還是走 OpenMX protocol,因此只要 device 有支援硬體解碼或壓縮的話,透過這個 API 都可以得到硬體解碼或壓縮的高效能好處。而這個 API 就用處而言都很好,唯一比較不好的一點是設計得很難用。我大致參考了這篇文章然後寫了一個可以播出影片的完整 Android Project 放在 GitHub上,有興趣可以參考看看。 這裡比較關鍵的是比較瞭解一下這邊這兩行,MediaCodec 完全的保護住 input / output buffer,而且這邊的 buffer 是 GPU memory,因此 MediaCodec 都期望我們透過 index 來 access 這些 buffer。
然後由於這一組 API 真的很低階,所以如果要用這個寫 Player 的話,Application 必須自己負責做 A/V sync … 這要做的好的話是要花一點功夫的,還好我只是要拿 video frame 而已 XD
由於我想要拿的是 RGB buffer,但通常 video frame decode 出來一定是 YUV 的 format,而且 YUV 還分超多種。不過基本上只要是公開的 YUV format 的話都可以找到方法自己寫 code 轉換。但還有一種 case 网络游戏 是硬體廠商自己訂的 YUV format,這種就要去翻一下 android framework source code,如前面提到的 MediaMetaDataretriever 也是透過 OMX decoder 拿到 video raw buffer 然後轉換成 RGB buffer,所以底層一定有 code 做這件事情。stagrfright 裡面有一個 ColorConverter 就是做這件事情的,但這邊的話是透果軟體作轉換所以效率會差一些(其實應該是很多)。有興趣的話可以參考我寫的一個 jni code 裡面會直接去使用 ColorConverter 去做 color conversion。
至於如果有人想要挑戰拿 MediaCodec 來做 transcoding 的話第一個必須要解決的問題是可能必須自己寫一個 MP4 muxer (或者是你 target 的 format 的 muxer),這一般人要做好像有點難度。如果有做出來的話記得分享一下 🙂
希望 Google 可以改善一下這個 API 讓它好用一點,然後也 release 一些真實的 Sample Code 放在 API demo project 裡面,不然這組 API 實在不怎麼 Google。
我是 K,以前幻想著可以做些偉大的事。但發現把一件簡單的事情做到很好就是一件很難又很厲害的事。所以現在正處於一個把簡單的事情做好的狀態。
請問如何將 mediaplayer 撥放影片時, 將視訊畫面秀在TextureView, 看 api 沒有實際的範例可參考, 能否提供簡單的範例? 謝謝
您好,可以看一下這個片段 https://gist.github.com/kswlee/5627632#file-android-mediaplayer-use-textureview,是我之前寫來測試用的。
K
您好
我下載你在github上的範例:https://github.com/kswlee/MediaCodecApp
我試著想要自己build 你提供的 colorconvert-jni.cpp
可是 build 不起來~~看Android.mk中有寫到:
LOCAL_C_INCLUDES +=
/android_source/frameworks/av/include/media/stagefright
/android_source/frameworks/native/include/media/openmax
/android_source/customize/native/include/utils
/android_source/customize/native/include
以及
LOCAL_LDLIBS += -lstagefright
LOCAL_LDLIBS += -landroid
LOCAL_LDLIBS += -lutils
感覺有少 .h 和 .so 檔
不知道可否指導一下呢?謝謝你啊~
您好,我已經更新 code 把 JNI 的部分拿掉了。
你好 我下載FrameGrabber,在HTC NEW ONE 4.3上面, 兩種方式
getFrameAtTimeByMMDR(video, 33333 * captureFrame) : getFrameAtTimeByFrameGrabber(video, 33333 * captureFrame);
都得到相同的圖片畫面, 可能是Android 4.3 已經修復MMDR的BUG了
謝謝你詳細的分享
另外,想請問, 如何在ANDROID下, 得知一個mp4 , 每秒的frame rate數目呢?
hi 感謝您的分享。
framerate 的話可以透過跟 MediaExtracor 拿 track 的 MediaFormat。之後再跟 MediaFormat 拿 framerate 的值。
MediaFormat mf = getFormat();
if (mf.containsKey(MediaFormat.KEY_FRAME_RATE))
float frameRate = mf.getFloat(MediaFormat.KEY_FRAME_RATE);
你好,若我想在decode完之後讀取或修改output buffer的內容,請問有怎樣的方法比較好? 只能透過getbitmap 從surface獲得資料嗎,還是可以根據OutputBufferIndex 中直接去access記憶體內容?
建議您直接透過 GL 的方式去做 frame 的改動。
您好:
感謝您的回應,我剛接觸openGL ES,對於其能做到的功能相當不熟悉,能請問一些關鍵函數的名稱嗎? 目前我是用類似您的流程,將frame decode 到 pbuffer的off-screen surface上,然後用Glreadpixel 讀出,因為我需要分析畫面內容並做一些對應處理,然後再render上去,但glreadpixel的速度十分慢,想請問您有沒有其他的解決方案。
比較好的做法應該是使用 shader 去做 frame 渲染的動作。
您好:
請問一下,如果我要將Frame 的資料轉成圖的話,我應該要從哪一部分把資料轉存?
您好,您可參考我之前的一個 Open Source Project: https://github.com/kswlee/FrameGrabber
您好:
謝謝您的回覆,不過想有些問題想再請教一下
在擷取的過程中,我發現只能針對指定的時間擷取Frame
但若想要將整部影片的每一個Frame都轉成圖片的話
我使用一個for迴圈包住主程式中轉換的副函式
但會造成程式沒有回應,且不會在螢幕顯示目前處理的Frame
或許是用錯方法了,還請協助一下
謝謝
你好,我看到你说到使用TextView就可以通过getBitmap方法来获取Bitmap对象,但是我试用后发现得到的是一个完全黑色的图像(compress到本地文件),能告诉我是什么原因吗?
注:通过View的isHardwareAccelerated方法知道,当前View是支持硬件加速的。
你好,想請教你一下,由於剛剛學習android video 技術很多不是很清楚.想請問一下在我們整個播放流程裡面,video 會有好幾個threads, 請問一下這些threads 在輸出buffer 之間的各個關係是如何,我不是很清楚可否指引一條路. 還有另一個問題就是我的想要在手機IC裡面增加 DTV這個function (siano solution),我詢問過台灣有做過的IC設計公司都是透過由JNI下去實現.如果我們現在要去整個android framework 實現的話該怎樣去實現呢? 請你指導一下謝謝