Android:如何把 BitMap 存到 SdCard

Published on:

本文寫作當時最新版本為 Android 7.0,執行版本為 Android 6.0

這次的需求是這樣子的:小明懷疑小美帶小王回家,所以偷偷買了一隻 Webcam 藏在天花板準備透過即時串流來看看到底發生什麼事;但裝隨附的 App 在手機上又太明顯了,所以決定自立自強來學怎麼寫可以讀取 WebCam 又能截圖存證的 App。

啊,你說小明是誰?哎,反正不是我,問這麼多幹什麼?

換句話說就是要把正在串流的影片截圖之後存起來。所以會有以下幾個主題:

  • 如何找到 SdCard 路徑與如何把 BitMap 存成 .Jpg
  • 如何播放並把 RTSP 轉成 BitMap

感謝 StackOverflow 與 Google,還好這都不太坑。本文將著重於介紹如何串連起以上三個部分,至於如何打磨細節則不是本文著墨的重點。

如何找到 SdCard 路徑與如何儲存 BitMap

感謝 StackOverflow 的先人智慧,參照 How to save a bitmap on internal storage 一文可以得到如下兩個神秘寶箱:

* 如何轉存 BitMap

private void storeImage(Bitmap image) {
    File pictureFile = getOutputMediaFile();
    if (pictureFile == null) {
        Log.d(TAG,
                "Error creating media file, check storage permissions: ");// e.getMessage());

        return;
    } 
    try {
        FileOutputStream fos = new FileOutputStream(pictureFile);
        image.compress(Bitmap.CompressFormat.PNG, 90, fos);
        fos.close();
    } catch (FileNotFoundException e) {
        Log.d(TAG, "File not found: " + e.getMessage());
    } catch (IOException e) {
        Log.d(TAG, "Error accessing file: " + e.getMessage());
    }  
}

* 如何取得 SdCard 目錄位置

這邊要注意 mediaStorageDir 的定義,這邊關係到檔案會出現在你 SdCard 的哪個部分;例如,依照範例的路徑來看:

Environment.getExternalStorageDirectory()
            + "/Android/data/"
            + getApplicationContext().getPackageName()
            + "/Files"

檔案就會出現在 sdcard/Android/data/${YOUR_APP_PACKAGE_NAME}/Files 底下。

/** Create a File for saving an image or video */
private  File getOutputMediaFile(){
    // To be safe, you should check that the SDCard is mounted

    // using Environment.getExternalStorageState() before doing this.

    
    // p.s: MODIFY THE PATH YOU WANT YOUR IMAGE TO STORE HERE

    // 嗨嗨如果你想改儲存路徑記得改這邊

    // for example, you can just using this way to save image into the root of sdcard

    // File mediaStorageDir = new File(Environment.getExternalStorageDirectory().toString());

    
    File mediaStorageDir = new File(Environment.getExternalStorageDirectory()
            + "/Android/data/"
            + getApplicationContext().getPackageName()
            + "/Files"); 

    // This location works best if you want the created images to be shared

    // between applications and persist after your app has been uninstalled.


    // Create the storage directory if it does not exist

    if (! mediaStorageDir.exists()){
        if (! mediaStorageDir.mkdirs()){
            return null;
        }
    } 
    // Create a media file name

    String timeStamp = new SimpleDateFormat("ddMMyyyy_HHmm").format(new Date());
    File mediaFile;
        String mImageName="MI_"+ timeStamp +".jpg";
        mediaFile = new File(mediaStorageDir.getPath() + File.separator + mImageName);  
    return mediaFile;
} 

如何播放並把 RTSP 轉成 BitMap

查了下 Android Developer 的 Supported Media Formats,發現其原生就支援以下格式:

  • RTSP (RTP, SDP)
  • HTTP/HTTPS progressive streaming
  • HTTP/HTTPS live streaming (draft protocol):
    • MPEG-2 TS media files only
    • Protocol version 3 (Android 4.0 and above)
    • Protocol version 2 (Android 3.x)
    • Not supported before Android 3.0

根據研究,Android 上能夠拿來當成播放畫面載體的元件至少有三個,分別如下:

本文將不會介紹全部三種元件的使用方式。

* 使用 VideoView 播放 RTSP:DEMO

內建元件 VideoView 只要正確套上就能播放,以下來看一段簡單的範例:

AndroidManifest.xml

一定要加入存取網路的權限,否則會黑畫面一陣子之後出現 can't play this video.

<uses-permission android:name="android.permission.INTERNET" />

activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/LinearLayout1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <EditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ems="10" >

        <requestFocus />
    </EditText>

    <Button
        android:id="@+id/playButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Play" />

    <VideoView
        android:id="@+id/rtspVideo"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>

MainActivity.java

package rtsp.example.com.rtspsample;

import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.VideoView;

public class MainActivity extends Activity implements OnClickListener {

    EditText rtspUrl;
    Button playButton;
    VideoView videoView;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        rtspUrl = (EditText) this.findViewById(R.id.editText);
        videoView = (VideoView) this.findViewById(R.id.rtspVideo);
        playButton = (Button) this.findViewById(R.id.playButton);
        playButton.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.playButton:
                RtspStream(rtspUrl.getEditableText().toString());
                break;
        }
    }

    private void RtspStream(String rtspUrl) {
        videoView.setVideoURI(Uri.parse(rtspUrl));
        videoView.requestFocus();
        videoView.start();
    }
}

到這邊為止,應該只要填入正確的 RTSP 來源就可以透過 VideoView 播放檔案了,以下是一組可用的來源:

rtsp://mpv.cdn3.bigCDN.com:554/bigCDN/definst/mp4:bigbuckbunnyiphone_400.mp4

範例repo

VideoView 雖然簡單易用,但如果要拿來截圖仍力有未逮,所以我們會需要使用能夠操作 getBitmap() 的 TextureView。

* 使用 TextureView 播放 RTSP 並存成 BitMap:DEMO

我們已經有了如何儲存 BitMap 的手段,所以接下來可以直接把這些東西組合起來。要用 TextureView 播放 RTSP 就得多使用 MediaPlayer 元件,完整範例如下:

AndroidManifest.xml

一定要加入存取網路的權限,否則會黑畫面一陣子之後出現 can't play this video.

<uses-permission android:name="android.permission.INTERNET" />

activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/LinearLayout1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <EditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ems="10"
        android:text="rtsp://mpv.cdn3.bigCDN.com:554/bigCDN/definst/mp4:bigbuckbunnyiphone_400.mp4">

        <requestFocus />
    </EditText>

    <Button
        android:id="@+id/playButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Play" />

    <Button
        android:text="save video frame"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/btnCapture" />

    <TextureView
        android:id="@+id/rtspVideo"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:alpha="1"
        tools:foreground="#000" />

</LinearLayout>

MainActivity.java

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.SurfaceTexture;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class MainActivity extends Activity
        implements
        OnClickListener,
        TextureView.SurfaceTextureListener {

    EditText rtspUrl;
    Button playButton;
    Button btnCapture;
    private MediaPlayer mMediaPlayer;
    private TextureView mPreview;
    private Surface surface;
    String TAG = "RTSP";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        rtspUrl = (EditText) this.findViewById(R.id.editText);
        playButton = (Button) this.findViewById(R.id.playButton);
        playButton.setOnClickListener(this);

        btnCapture = (Button) this.findViewById(R.id.btnCapture);
        btnCapture.setOnClickListener(this);

        mPreview = (TextureView) findViewById(R.id.rtspVideo);
        mPreview.setSurfaceTextureListener(this);

    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.playButton:
                RtspStream(rtspUrl.getEditableText().toString());
                break;
            case R.id.btnCapture:
                storeImage(getBitmap());
                break;
        }
    }

    private void storeImage(Bitmap image) {
        File pictureFile = getOutputMediaFile();
        if (pictureFile == null) {
            Log.d(TAG,
                    "Error creating media file, check storage permissions: ");// e.getMessage());

            return;
        }
        try {
            FileOutputStream fos = new FileOutputStream(pictureFile);
            image.compress(Bitmap.CompressFormat.PNG, 90, fos);
            fos.close();
        } catch (FileNotFoundException e) {
            Log.d(TAG, "File not found: " + e.getMessage());
        } catch (IOException e) {
            Log.d(TAG, "Error accessing file: " + e.getMessage());
        }
    }

    private  File getOutputMediaFile(){
        // To be safe, you should check that the SDCard is mounted

        // using Environment.getExternalStorageState() before doing this.

        File mediaStorageDir = new File(Environment.getExternalStorageDirectory().toString());

        // This location works best if you want the created images to be shared

        // between applications and persist after your app has been uninstalled.


        // Create the storage directory if it does not exist

        if (! mediaStorageDir.exists()){
            if (! mediaStorageDir.mkdirs()){
                return null;
            }
        }
        // Create a media file name

        String timeStamp = new SimpleDateFormat("ddMMyyyy_HHmm").format(new Date());
        File mediaFile;
        String mImageName="MI_"+ timeStamp +".jpg";
        mediaFile = new File(mediaStorageDir.getPath() + File.separator + mImageName);
        return mediaFile;
    }


    private void RtspStream(String rtspUrl) {
        if (mPreview.isAvailable()){

            try {
                mMediaPlayer = new MediaPlayer();
                mMediaPlayer.setDataSource(this, Uri.parse(rtspUrl));
                mMediaPlayer.setSurface(surface);
                mMediaPlayer.setLooping(true);

                // don't forget to call MediaPlayer.prepareAsync() method when you use constructor for

                // creating MediaPlayer

                mMediaPlayer.prepareAsync();
                // Play video when the media source is ready for playback.

                mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
                    @Override
                    public void onPrepared(MediaPlayer mediaPlayer) {
                        mediaPlayer.start();
                    }
                });

            } catch (IllegalArgumentException | SecurityException | IllegalStateException | IOException e) {
                Log.d(TAG, e.getMessage());
            }
        }
    }

    public Bitmap getBitmap() {
        return mPreview.getBitmap();
    }

    public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
        surface = new Surface(surfaceTexture);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mMediaPlayer != null) {
            // Make sure we stop video and release resources when activity is destroyed.

            mMediaPlayer.stop();
            mMediaPlayer.release();
            mMediaPlayer = null;
        }
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i2) {

    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
        return false;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {

    }

}

到這邊你應該就能夠播放 RTSP 串流的同時儲存截圖了。

Refs

- How To Stream RTMP live in Android

Comments

comments powered by Disqus