Android 播放視頻並獲取指定時間的幀畫面


最近做的項目要求既能播放視頻(類似於視頻播放器),又能每隔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

http://jcodec.org/

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;
        }
    }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM