最近做的項目要求既能播放視頻(類似於視頻播放器),又能每隔1s左右獲取一幀視頻畫面,然后對圖片進行處理,調查了一周,也被折磨了一周,總算找到了大致符合要求的方法。首先對調查過程中涉及到的方法進行簡單介紹,再重點介紹最終所采用的方法,話不多說,進入正題。
一.MediaMetadataRetriever
播放視頻並取得畫面的一幀,大家最先想到應該都是這個,我同樣也最先對它進行了測試,這里使用MediaPlayer進行播放,視頻播放界面使用SurfaceView來實現。
public class PlayerMainActivity extends Activity implements OnClickListener, SurfaceHolder.Callback, Runnable { private static final String TAG = "Movie"; private MediaPlayer mediaPlayer; private SurfaceView surfaceView; private SurfaceHolder surfaceHolder; private Button play_btn;private int currentPosition = 0; private Bitmap bitmap = null; private String dataPath = Environment.getExternalStorageDirectory() + "/Video/Test_movie.AVI"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); surfaceView = (SurfaceView) findViewById(R.id.surfaceView); play_btn = (Button) findViewById(R.id.play_btn); play_btn.setOnClickListener(this); screen_cut_btn.setOnClickListener(this); surfaceHolder = surfaceView.getHolder(); surfaceHolder.addCallback(this); } @Override public void run() { mediaPlayer = new MediaPlayer(); mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mediaPlayer.setDisplay(surfaceHolder); try { mediaPlayer.setDataSource(dataPath); mediaPlayer.prepare(); MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); mediaMetadataRetriever.setDataSource(dataPath); int millis = mediaPlayer.getDuration(); Log.i(TAG, "millis: " + millis/1000); for (int i = 10000*1000; i < 20*1000*1000; i+=500*1000) { bitmap = mediaMetadataRetriever.getFrameAtTime(i, MediaMetadataRetriever.OPTION_CLOSEST); String path = Environment.getExternalStorageDirectory() + "/bitmap/" + i + ".png"; FileOutputStream fileOutputStream = null; try { fileOutputStream = new FileOutputStream(path); bitmap.compress(CompressFormat.PNG, 100, fileOutputStream); Log.i(TAG, "i: " + i/1000/1000); } catch (Exception e) { Log.i(TAG, "Error: " + i/1000/1000); e.printStackTrace(); } finally { if (fileOutputStream != null) { fileOutputStream.close(); } } bitmap.recycle(); } } catch (Exception e) { e.printStackTrace(); } } @Override public void surfaceCreated(SurfaceHolder holder) { Thread t = new Thread(this); t.start(); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { } @Override public void onClick(View view) { switch (view.getId()) { case R.id.play_btn: if (mediaPlayer.isPlaying()) { mediaPlayer.pause(); play_btn.setText(getResources().getText(R.string.play)); } else { mediaPlayer.start(); play_btn.setText(getResources().getText(R.string.pause)); } break;default: break; } } @Override protected void onDestroy() { super.onDestroy(); if (mediaPlayer.isPlaying()) { mediaPlayer.stop(); } mediaPlayer.release(); } }
獲取一幀的關鍵代碼為:
Bitmap bitmap = mediaMetadataRetriever.getFrameAtTime(timeMs * 1000, MediaMetadataRetriever.OPTION_CLOSEST);
public Bitmap getFrameAtTime(long timeUs, int option)
第一個參數是傳入時間,只能是us(微秒)
第二個參數:
- OPTION_CLOSEST 在給定的時間,檢索最近一個幀,這個幀不一定是關鍵幀。
- OPTION_CLOSEST_SYNC 在給定的時間,檢索最近一個同步與數據源相關聯的的幀(關鍵幀)。
- OPTION_NEXT_SYNC 在給定時間之后檢索一個同步與數據源相關聯的關鍵幀。
- OPTION_PREVIOUS_SYNC 顧名思義,同上
這里為了提取我們想要的幀,不使用關鍵幀,所以用 OPTION_CLOSEST .
最終的測試結果並不理想,連續取20幀畫面,其中真正有效的只有7張,其余都是重復的,原因為即使是使用參數OPTION_CLOSEST,程序仍然會去取指定時間臨近的關鍵幀,如10s-15s總是取同一幀,因此這種方法不可用。
提高視頻的質量或許有效,未嘗試。
補充MediaMetadataRetriever的其他知識
// 取得視頻的總長度(單位為毫秒)
String time = mediaMetadataRetriever. extractMetadata( MediaMetadataRetriever. METADATA_KEY_DURATION);
MediaMetadataRetriever主要用來取縮略圖。
二.ThumbnailUtils
同樣主要是用來獲取視頻的縮略圖,不可靠,因此並未深入研究。
從以上兩種方法可以看出,Android API 所提供的獲取視頻幀的方法大都只能獲取視頻的縮略圖,沒辦法獲取視頻每一幀的圖片。因此,調查方向應當轉向對視頻進行解碼,然后獲取任意一幀。
三.MediaCodec
硬件解碼,嘗試從inputBuffers、outputBuffers中獲取幀畫面,失敗,bitmap中的數據大小始終為0 KB。
public class MoviePlayerActivity extends Activity implements OnTouchListener, OnClickListener, SurfaceHolder.Callback { private static final String TAG = "Image"; private String file_path; private Button movie_play; private boolean playButtonVisible; private boolean playPause; private SurfaceView surfaceView; private SurfaceHolder surfaceHolder; private PlayerThread playerThread = null; private ByteBuffer mPixelBuf; @Override protected void onCreate(Bundle savedInstanceState) { getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); requestWindowFeature(Window.FEATURE_NO_TITLE); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); super.onCreate(savedInstanceState); setContentView(R.layout.movie_player_activity); movie_play = (Button) findViewById(R.id.movie_videoview_play); movie_play.setOnClickListener(this); movie_play.setText("Play"); Intent intent = getIntent(); file_path = intent.getStringExtra("file_path"); //此Activity是從其他頁面跳轉來的,file_path為要播放的視頻地址 surfaceView = (SurfaceView) findViewById(R.id.surfaceView); surfaceHolder = surfaceView.getHolder(); surfaceHolder.addCallback(this); mPixelBuf = ByteBuffer.allocateDirect(640*480*4); mPixelBuf.order(ByteOrder.LITTLE_ENDIAN); } @Override public void surfaceCreated(SurfaceHolder holder) { if (playerThread == null) { playerThread = new PlayerThread(holder.getSurface()); playerThread.start(); } } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { if (playerThread != null) { playerThread.interrupt(); } } @Override public boolean onTouch(View v, MotionEvent event) { if (!playButtonVisible) { movie_play.setVisibility(View.VISIBLE); movie_play.setEnabled(true); } else { movie_play.setVisibility(View.INVISIBLE); } playButtonVisible = !playButtonVisible; return false; } @Override public void onClick(View view) { switch (view.getId()) { case R.id.movie_videoview_play: if (!playPause) { movie_play.setText("Pause"); } else { movie_play.setText("Play"); } playPause = !playPause; break; default: break; } } private void writeFrameToSDCard(byte[] bytes, int i, int sampleSize) { i++; if (i%10 == 0) { try { Bitmap bmp = BitmapFactory.decodeByteArray(bytes, 0, sampleSize); mPixelBuf.rewind(); bmp.copyPixelsFromBuffer(mPixelBuf); String path = Environment.getExternalStorageDirectory() + "/bitmap/" + i + ".png"; FileOutputStream fileOutputStream = null; try { fileOutputStream = new FileOutputStream(path); bmp.compress(CompressFormat.PNG, 90, fileOutputStream); bmp.recycle(); Log.i(TAG, "i: " + i); } catch (Exception e) { Log.i(TAG, "Error: " + i); e.printStackTrace(); } finally { if (fileOutputStream != null) { fileOutputStream.close(); } } } catch (Exception e) { e.printStackTrace(); } } } private class PlayerThread extends Thread { private MediaExtractor extractor; private MediaCodec mediaCodec; private Surface surface; public PlayerThread(Surface surface) { this.surface = surface; } @Override public void run() { extractor = new MediaExtractor(); try { extractor.setDataSource(file_path); } catch (IOException e1) { Log.i(TAG, "Error"); e1.printStackTrace(); } for (int i = 0; i < extractor.getTrackCount(); i++) { MediaFormat format = extractor.getTrackFormat(i); String mime = format.getString(MediaFormat.KEY_MIME); if (mime.startsWith("video/")) { extractor.selectTrack(i); mediaCodec = MediaCodec.createDecoderByType(mime); mediaCodec.configure(format, surface, null, 0); break; } } if (mediaCodec == null) { Log.e(TAG, "Can't find video info!"); return; } mediaCodec.start(); ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers(); ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers(); BufferInfo info = new BufferInfo(); boolean isEOS = false; long startMs = System.currentTimeMillis(); int i = 0; while (!Thread.interrupted()) { if (!isEOS) { int inIndex = mediaCodec.dequeueInputBuffer(10000); if (inIndex >= 0) { ByteBuffer buffer = inputBuffers[inIndex]; int sampleSize = extractor.readSampleData(buffer, 0); if (sampleSize < 0) { // We shouldn't stop the playback at this point, just pass the EOS // flag to mediaCodec, we will get it again from the dequeueOutputBuffer Log.d(TAG, "InputBuffer BUFFER_FLAG_END_OF_STREAM"); mediaCodec.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); isEOS = true; } else { mediaCodec.queueInputBuffer(inIndex, 0, sampleSize, extractor.getSampleTime(), 0); extractor.advance(); } } } int outIndex = mediaCodec.dequeueOutputBuffer(info, 100000); switch (outIndex) { case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED: Log.d(TAG, "INFO_OUTPUT_BUFFERS_CHANGED"); outputBuffers = mediaCodec.getOutputBuffers(); break; case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED: Log.d(TAG,"New format " + mediaCodec.getOutputFormat()); break; case MediaCodec.INFO_TRY_AGAIN_LATER: Log.d(TAG, "dequeueOutputBuffer timed out!"); break; default: ByteBuffer buffer = outputBuffers[outIndex]; Log.v(TAG,"We can't use this buffer but render it due to the API limit, " + buffer); // We use a very simple clock to keep the video FPS, or the video // playback will be too fast while (info.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) { try { sleep(10); } catch (InterruptedException e) { e.printStackTrace(); break; } } mediaCodec.releaseOutputBuffer(outIndex, true); /* saves frame to SDcard */ mPixelBuf.rewind(); GLES20.glReadPixels(0, 0, 640, 480, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, mPixelBuf); try { ByteBuffer outByteBuffer = outputBuffers[outIndex]; outByteBuffer.position(info.offset); outByteBuffer.limit(info.offset + info.size); //info的兩個參數值始終為0,所保存的.png也都是0KB。 outByteBuffer.limit(2); byte[] dst = new byte[outByteBuffer.capacity()]; outByteBuffer.get(dst); writeFrameToSDCard(dst, i, dst.length); i++; } catch (Exception e) { Log.d(TAG, "Error while creating bitmap with: " + e.getMessage()); } break; } // All decoded frames have been rendered, we can stop playing now if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { Log.d(TAG, "OutputBuffer BUFFER_FLAG_END_OF_STREAM"); break; } } mediaCodec.stop(); mediaCodec.release(); extractor.release(); } } }
所保存的圖片大小始終為0的原因大概和下面的方法五類似。
想要深入研究此方法可以參考:
http://stackoverflow.com/questions/19754547/mediacodec-get-all-frames-from-video
http://stackoverflow.com/questions/23321880/how-to-get-bitmap-frames-from-video-using-mediacodec
四.JCodec
jcodec-samples-0.1.7.apk
解碼視頻文件耗時較長,性能較差,平均1.5s取一幀圖片,達不到要求。
五.使用VideoView播放視頻,使用getDrawingCache獲取View視圖
VideoView是Android提供的播放視頻組件,上手很容易。這里使用getDrawingCache獲取控件的View。
但是DrawingCache只能截取非視頻部分的畫面,播放視頻的那個小窗口一直是黑色的。原因為Activity畫面走的是framebuffer,視頻是硬解碼推送過來的,所有讀取/dev/graphics/fb0 視頻播放的那一塊就是黑色的,硬件解碼不會推送到buffer的,而是直接推送到硬件輸出了。
public class MoviePlayerActivity extends Activity implements OnTouchListener, OnClickListener, Runnable { private static final String TAG = "Image"; private String file_path; private VideoView videoView; private Button movie_play; private boolean playButtonVisible; private boolean playPause; @Override protected void onCreate(Bundle savedInstanceState) { // full screen getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); // no title requestWindowFeature(Window.FEATURE_NO_TITLE); // landscape or horizontal screen setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); super.onCreate(savedInstanceState); setContentView(R.layout.movie_player_activity); movie_play = (Button) findViewById(R.id.movie_videoview_play); movie_play.setOnClickListener(this); movie_play.setText("Play"); Intent intent = getIntent(); file_path = intent.getStringExtra("file_path"); videoView = (VideoView) findViewById(R.id.movie_palyer_videoview); videoView.setMediaController(null); // videoView.setMediaController(new MediaController(this)); videoView.setVideoPath(file_path); videoView.start(); videoView.requestFocus(); Thread screenShootThread = new Thread(this); screenShootThread.start(); videoView.setOnTouchListener(this); } @Override public void run() { //播放視頻時后台自動截圖,注意參數為videoView for (int i = 10000 * 1000; i < 20 * 1000 * 1000; i += 500 * 1000) { int nowTime = videoView.getCurrentPosition(); try { screenShot(videoView, i); } catch (Exception e1) { Log.i(TAG, "Error: screenShot. "); e1.printStackTrace(); } try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } @Override public boolean onTouch(View v, MotionEvent event) { if (!playButtonVisible) { movie_play.setVisibility(View.VISIBLE); movie_play.setEnabled(true); } else { movie_play.setVisibility(View.INVISIBLE); } playButtonVisible = !playButtonVisible; return false; } @Override public void onClick(View view) { switch (view.getId()) { case R.id.movie_videoview_play: if (!playPause) { movie_play.setText("Pause"); } else { movie_play.setText("Play"); } playPause = !playPause; int nowTime = videoView.getCurrentPosition(); Log.i(TAG, "nowTime: " + nowTime); try { screenShot(videoView, nowTime); //點擊按鈕截圖,注意參數為videoView } catch (Exception e) { e.printStackTrace(); } break; default: break; } } public void screenShot(View view, int nowTime) throws Exception { view.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); view.setDrawingCacheEnabled(true); view.buildDrawingCache(); Bitmap bitmap = Bitmap.createBitmap(view.getDrawingCache()); String path = Environment.getExternalStorageDirectory() + "/bitmap/" + nowTime + ".png"; FileOutputStream fileOutputStream = null; try { fileOutputStream = new FileOutputStream(path); bitmap.compress(Bitmap.CompressFormat.PNG, 90, fileOutputStream); Log.i(TAG, "i: " + nowTime); } catch (Exception e) { Log.i(TAG, "Error: " + nowTime); e.printStackTrace(); } finally { if (fileOutputStream != null) { fileOutputStream.close(); } } bitmap.recycle(); view.setDrawingCacheEnabled(false); } }
DrawingCache 獲取其他控件如Button等View時正常。
六.VideoView播放視頻,MediaMetadataRetriever獲取幀畫面(就是你了!)
能夠正常獲取幀畫面,並且畫面之間不重復,即不是只取關鍵幀,而是去取相應時間點的幀畫面。若不保存為圖片(.png/.jpg),耗時最多為0.4s,基本達到要求。
參考:http://yashirocc.blog.sohu.com/175636801.html
public class MoviePlayerActivity extends Activity implements OnTouchListener, OnClickListener, Runnable { private static final String TAG = "ImageLight"; private String file_path; private VideoView videoView; private Button movie_play; private boolean playButtonVisible; private boolean playPause; @Override protected void onCreate(Bundle savedInstanceState) { getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); // full screen requestWindowFeature(Window.FEATURE_NO_TITLE); // no title setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); // landscape or horizontal screen super.onCreate(savedInstanceState); setContentView(R.layout.movie_player_activity); movie_play = (Button) findViewById(R.id.movie_videoview_play); movie_play.setOnClickListener(this); movie_play.setText("Play"); Intent intent = getIntent(); file_path = intent.getStringExtra("file_path"); videoView = (VideoView) findViewById(R.id.movie_palyer_videoview); videoView.setMediaController(null); // videoView.setMediaController(new MediaController(this)); videoView.setVideoPath(file_path); videoView.start(); videoView.requestFocus(); Thread screenShootThread = new Thread(this); screenShootThread.start(); videoView.setOnTouchListener(this); } @Override public void run() { MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever(); metadataRetriever.setDataSource(file_path); for (int i = 40000 * 1000; i < 50 * 1000 * 1000; i += 500 * 1000) { // try { // Thread.sleep(500); // } catch (InterruptedException e) { // e.printStackTrace(); // } Bitmap bitmap = metadataRetriever. getFrameAtTime(videoView.getCurrentPosition()*1000, MediaMetadataRetriever.OPTION_CLOSEST); Log.i(TAG, "bitmap---i: " + i/1000); String path = Environment.getExternalStorageDirectory() + "/bitmap/" + i + ".png"; FileOutputStream fileOutputStream = null; try { fileOutputStream = new FileOutputStream(path); bitmap.compress(Bitmap.CompressFormat.PNG, 90, fileOutputStream); Log.i(TAG, "i: " + i/1000); } catch (Exception e) { Log.i(TAG, "Error: " + i/1000); e.printStackTrace(); } finally { if (fileOutputStream != null) { try { fileOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } bitmap.recycle(); } } @Override public boolean onTouch(View v, MotionEvent event) { if (!playButtonVisible) { movie_play.setVisibility(View.VISIBLE); movie_play.setEnabled(true); } else { movie_play.setVisibility(View.INVISIBLE); } playButtonVisible = !playButtonVisible; return false; } @Override public void onClick(View view) { switch (view.getId()) { case R.id.movie_videoview_play: if (!playPause) { movie_play.setText("Pause"); } else { movie_play.setText("Play"); } playPause = !playPause; break; default: break; } } }