Android多媒体功能开发(14)——Camera2框架

Posted nanoage

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android多媒体功能开发(14)——Camera2框架相关的知识,希望对你有一定的参考价值。

android5.0开始,引入了一套Camera2框架控制相机实现拍照和录像等功能,相关的类定义在android.hardware.camera2包中。原有的android.hardware包中的Camera类降级使用,因为其功能少,灵活性差,满足不了日益复杂的相机功能需求。

Camera2框架的相机模型被设计成一个管道,使用相机时需要先和相机设备建立一个会话,通过该会话向相机发送请求,相机将图像数据保存到配置好的Surface,Surface就是存放图像数据的缓冲区。请求分为单次请求、重复请求和多次请求三种。例如,实现预览功能需要发送一个重复请求,相当于不断向相机发送预览请求,相机就会不断把预览图像数据存入预览组件的图像缓冲区。而实现拍照功能可以发送一个单次请求,相机会将图像数据存入一个缓冲区,应用再将图像数据保存到图片文件即可。这些请求是被放在一个队列中顺序执行的,因此不会发生两个请求同时执行的冲突。

这种管道模型的请求和响应都是异步的,所以Camera2框架大量采用回调方法。这样使得代码的执行不够线性,刚开始学习时不容易理解掌握。下面我们就具体介绍Camera2框架的使用方法,以及用到的概念和类。

一、相机权限和特性
为了在安装应用前就确认设备上有相机,需要在配置文件Manifest.xml中配置相机特性。这样,如果设备上没有相机就无法安装应用。使用相机必须在配置文件Manifest.xml中注册相机权限android.permission.CAMERA,声明应用需要相机权限。代码是:

<uses-feature android:name="android.hardware.camera" android:required="true"/>
<uses-permission android:name="android.permission.CAMERA" />

Android6.0以上系统需要应用运行时进行动态权限申请,所以在使用相机前需要检查权限,如果没有许可就进行询问。主要代码是:

if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) 
      ActivityCompat.requestPermissions(activity, new String[]Manifest.permission.CAMERA, 101);
      return;

还可以配置更多特性要求,例如必须有支持自动对焦的相机等。

二、开关相机
Camera2框架使用流程的起点是CameraManager,这是一个负责管理相机的系统服务,通过它来获取相机设备和信息。获取CameraManager的代码如下:

CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);

接着需要获取相机列表,每个相机用一个String类型的id表示,后面通过id可以获取对应的相机设备。所有相机id的列表保存在一个String数组中,设备上有几个相机就在数组中几个元素。代码如下:

String[] cameraIdList = cameraManager.getCameraIdList();

接下来可以根据相机id获取相机特性,相机特性保存在一个CameraCharacteristics对象中。一项属性表示为一个键值对,所以该对象是一个键值对的集合。下面的代码通过遍历所有相机的特性找到后置相机的id,再通过CameraCharacteristics对象获取相机支持的拍照分辨率大小:

String backCameraId = null;
for(String cameraId:cameraIdList)
      CameraCharacteristics cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId); 
      if(cameraCharacteristics.get(CameraCharacteristics.LENS_FACING)==CameraCharacteristics.LENS_FACING_BACK) 
                mCameraId = cameraId;
                supportedSizes = cameraCharacteristics .get( CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) .getOutputSizes(ImageReader.class);
                break;
       

接下来就可以调用 CameraManager.openCamera() 方法开启相机,代码如下: 

cameraManager.openCamera(mCameraId, stateCallback, mBackgroundHandler);

openCamera()方法有三个参数,第一个参数是cameraId,这里使用上一步获取到的后置相机的id。第二个参数是处理结果的回调对象,和事件处理机制中的监听器一样。openCamera()方法是异步执行的,调用以后方法马上返回,继续执行后续代码,打开相机是否成功的结果要过一段时间才返回,返回后调用回调对象的对应方法。第三个参数是执行回调方法的线程,可以是主线程,也可以是后台线程。回调对象需要在调用openCamera()方法之前创建,示例代码如下:

CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() 
      @Override
      public void onOpened(@NonNull CameraDevice cameraDevice) 
          mCameraDevice = cameraDevice;
          //  回调函数的代码在子线程中执行,所以不能直接发出Toast消息,只能通过主线程发出
          runOnUiThread(new Runnable() 
              @Override
              public void run() 
                  Toast.makeText(getBaseContext(), "Camera opened", Toast.LENGTH_SHORT).show();
              
          );
      

      @Override
      public void onDisconnected(@NonNull CameraDevice cameraDevice) 
          mCameraDevice = null;
      

      @Override
      public void onError(@NonNull CameraDevice cameraDevice, int i) 
          cameraDevice.close();
          mCameraDevice = null;
      

;

后台线程也需要在调用openCamera()方法之前调用,启动和停止后台线程的代码如下:

private void startBackgroundThread() 
    mBackgroundThread = new HandlerThread("CameraBackground");
    mBackgroundThread.start();
    mBackgroundHandler = new Handler(mBackgroundThread.getLooper());


private void stopBackgroundThread() 
    mBackgroundThread.quitSafely();
    try 
        mBackgroundThread.join();
        mBackgroundThread = null;
        mBackgroundHandler = null;
     catch (InterruptedException e) 
        e.printStackTrace();
    

相机不再使用以后需要关闭,否则会一直占用相机资源,使得其他使用相机的应用无法使用。关闭相机比较合适的时机是在Activity的onPause()方法中。关闭相机的代码如下:

mCameraDevice.close();


三、创建CaptureSession会话
打开相机设备以后,要控制相机还要建立CaptureSession会话。代码如下:

mCameraDevice.createCaptureSession(surfaceList, sessionStateCallback, mBackgroundHandler);

mCameraDevice是上一步打开相机的结果回调中传回的相机对象,创建CameraSession会话需要调用它的createCaptureSession()方法,方法有三个参数。第一个参数是Surface列表,即用于接收图像数据的内存缓冲区,比如用于预览的Surface,用于拍照的Surface,这些Surface必须在这里准备好,否则后面预览或拍照时无法使用。创建预览和拍照两个Surface的列表的代码如下:

//    用Texture组件预览,其Surface在监听器中获得
TextureView  previewTexture = new TextureView(this);
previewTexture.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() 
     @Override
     public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surfaceTexture, int i, int i1) 
         mPreviewSurface = new Surface(surfaceTexture);  //  得到用于预览的Surface
     
);

//    用ImageReader保存拍照数据,其Surface通过getSurface()方法获得
int width = supportedSizes[0].getWidth();
int height = supportedSizes[0].getHeight();
ImageReader  mImageReader = ImageReader.newInstance(width, height, ImageFormat.JPEG, 5);

//    创建Surface列表
List<Surface> surfaceList = new ArrayList<Surface>();
surfaceList.add(mPreviewSurface);
surfaceList.add(mImageReader.getSurface());

createCaptureSession()方法的第二个参数是结果回调对象,是否创建成功的结果要过一段时间才返回,返回后调用回调对象的对应方法。创建成功后,返回CameraCaptureSession对象,通过这个对象向相机发送各种操作请求。回调对象需要在调用createCaptureSession()方法之前创建,代码如下:

CameraCaptureSession.StateCallback sessionStateCallback = new CameraCaptureSession.StateCallback() 
      @Override
      public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) 
          mCameraCaptureSession = cameraCaptureSession;
      
;

createCaptureSession()方法的第三个参数是执行回调方法的线程,可以是主线程,也可以是后台线程,和前面openCamera()方法相同。

相机用完以后需要关闭CaptureSession会话,代码如下:

mCameraCaptureSession.close();

四、开启和停止预览
通过CaptureSession会话,可以向相机发送请求,例如开启和停止预览。预览本质上是不断重复执行的Capture操作,每一次Capture都会把相机数据输出到对应的Surface上,显示出预览画面。使用的是CameraCaptureSession对象的setRepeatingRequest方法,具体代码如下:

CaptureRequest.Builder builder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
builder.addTarget(mPreviewSurface);
CameraCaptureSession.CaptureCallback captureCallback = new CameraCaptureSession.CaptureCallback();
mCameraCaptureSession.setRepeatingRequest(builder.build(), captureCallback, mBackgroundHandler);

方法的第一个参数是CaptureRequest请求对象,保存预览的各种设置参数,通过Builder模式创建并使用预览模板。因为相机的参数非常多,所以提供了模板以简化代码,预览使用预览模板生成请求,拍照使用拍照模板生成请求。然后把预览Surface加入,这样相机就会把图像数据保存到Surface中。

方法的第二个参数是结果回调对象,请求的结果要过一段时间才返回,返回后调用回调对象的对应方法。这个回调对象没有做任何处理。

方法的第三个参数是执行回调方法的线程,可以是主线程,也可以是后台线程,和前面openCamera()、createCaptureSession()方法相同。

关闭预览只要停止该重复请求就行,代码如下:

mCameraCaptureSession.stopRepeating();

五、拍照
拍照通过单次Capture实现,可以用ImageReader对象作为接收照片的Surface。使用CameraCaptureSession对象的capture()方法,代码如下:

CaptureRequest.Builder builder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
builder.addTarget(mPreviewSurface);
builder.addTarget(mImageReader.getSurface());
CameraCaptureSession.CaptureCallback captureCallback = new CameraCaptureSession.CaptureCallback() ;
mCameraCaptureSession.capture(builder.build(), captureCallback, mBackgroundHandler);

方法的第一个参数是CaptureRequest请求对象,保存拍照的各种设置参数,通过Builder模式创建并使用拍照模板。然后把预览Surface和ImageReader的Surface都加入,这样相机会同时把图像数据保存到两个Surface,拍照的同时不中断预览。

方法的第二个参数是结果回调对象,请求的结果要过一段时间才返回,返回后调用回调对象的对应方法。这个回调对象没有做任何处理。

方法的第三个参数是执行回调方法的线程,可以是主线程,也可以是后台线程,和前面openCamera()、createCaptureSession()、setRepeatingRequest()方法相同。

ImageReader接收到图像数据后,其OnImageAvailableListener监听器的onImageAvailable方法被调用,在此方法中可以将图像数据保存到文件,代码如下:

ImageReader.OnImageAvailableListener listener = new ImageReader.OnImageAvailableListener() 
    @Override
    public void onImageAvailable(ImageReader imageReader) 
        Image image = imageReader.acquireNextImage();   //  取出一个图像
        saveImage(image, picFile);    //  将图像保存到文件
    
;
mImageReader.setOnImageAvailableListener(listener, mBackgroundHandler); //  设置监听器,拍照完成后会执行上面的方法

将图像保存到文件的代码如下:

void saveImage(Image mImage, File mFile) 
    ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
    byte[] bytes = new byte[buffer.remaining()];
    buffer.get(bytes);
    FileOutputStream output = null;
    try 
        output = new FileOutputStream(mFile);
        output.write(bytes);
     catch (IOException e) 
        e.printStackTrace();
     finally 
        mImage.close();
        if (null != output) 
            try 
                output.close();
             catch (IOException e) 
                e.printStackTrace();
            
        
    


六、示例
根据Camera2框架的使用方法,我们编写了一个例子,将每一步操作用一个按钮来控制,以便演示Camera2框架的使用步骤。

运行时,首先点击"OPEN CAMERA"按钮,执行打开相机代码,并在回调方法中保存打开的CameraDevice对象。

然后点击"CREATE SESSION"按钮,执行创建CaptureSession会话的代码,并在回调方法中保存创建的CaptureSession。

接着点击"START PREVIEW"按钮,执行发送重复Capture请求开启预览的代码。

这三步须依次完成,每一步都需要等待上一步的结果在回调方法中成功返回。这个例子中是分在三个按钮中手动完成,可以等待上一步结果返回后再点击按钮,代码看起来分步骤执行,比较线性,容易理解。如果想让这三步自动依次执行,可以将每一步的调用放到上一步的回调方法中执行,但是这样代码是多层回调嵌套,不容易理解。

预览画面出现后可以点击"CAPTURE"按钮,执行发送单次Capture请求拍照的代码。完成后可以再依次点击"STOP PREVIEW"、"CLOSE SESSION"、"CLOSE CAMERA"按钮释放资源。

"VIEW"按钮调用可以查看图片的界面查看拍摄的照片,以验证拍摄是否正确完成。"CAMERAVIEW"按钮启动另一个Activity,该Activity中使用了一个封装好的控件,可以实现Camera2框架预览和拍照功能。使用封装好的控件代码非常简单,而且该控件的代码相对比较完善,解决了类似于照片方向、自动闪光灯等设置问题。我们的示例代码中为了突出主要代码,便于理解,将这些细节问题都忽略了。

例子的完整代码如下:

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.FileProvider;

import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.ImageFormat;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CaptureRequest;
import android.media.Image;
import android.media.ImageReader;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import android.util.Size;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.Toast;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity 
    File picFile;   //  保存照片的文件
    Uri picUri;     //  照片文件的uri,查看图片时使用

    TextureView previewTexture; //  预览组件
    Surface mPreviewSurface;    //  预览Surface

    private String mCameraId;   //  后置相机id
    private HandlerThread mBackgroundThread;    //  处理回调结果的后台线程
    private Handler mBackgroundHandler;         //  处理回调结果的handler
    private CameraDevice mCameraDevice;         //  相机设备对象
    private Size[] supportedSizes;              //  相机支持的拍照分辨率大小
    private CameraCaptureSession mCameraCaptureSession; //  相机会话
    private ImageReader mImageReader;   //  拍照时接收图像数据并保存到文件

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        LinearLayout ll = new LinearLayout(this);
        ll.setOrientation(LinearLayout.VERTICAL);
        setContentView(ll);

        LinearLayout line1 = new LinearLayout(this);
        line1.setOrientation(LinearLayout.HORIZONTAL);
        ll.addView(line1);

        LinearLayout line2 = new LinearLayout(this);
        line2.setOrientation(LinearLayout.HORIZONTAL);
        ll.addView(line2);

        LinearLayout line3 = new LinearLayout(this);
        line3.setOrientation(LinearLayout.HORIZONTAL);
        ll.addView(line3);

        LinearLayout line4 = new LinearLayout(this);
        line4.setOrientation(LinearLayout.HORIZONTAL);
        ll.addView(line4);

        previewTexture = new TextureView(this);
        ll.addView(previewTexture);
        previewTexture.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() 
            @Override
            public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surfaceTexture, int i, int i1) 
                mPreviewSurface = new Surface(surfaceTexture);  //  得到用于预览的Surface
            
            @Override
            public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surfaceTexture, int i, int i1) 
            
            @Override
            public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surfaceTexture) 
                return false;
            
            @Override
            public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surfaceTexture) 
            
        );

        picFile = new File(getExternalFilesDir(null), "picture.jpg");
        //  定义多媒体文件的uri,在应用之间传递文件时需要用uri
        if (Build.VERSION.SDK_INT >= 24)   //  Android 7 以后不能直接用file uri分享文件,要使用FileProvider
            String fileProviderAuthority = getPackageName() + ".fileProvider"; //  FileProvider的名字,FileProvider在应用配置文件AndroidManifest中定义
            //  格式为:content://com.zzk.a1510camera2.fileProvider/testdir/picture.jpg, testdir是res/file_paths/file_paths.xml中定义的目录别名
            picUri = FileProvider.getUriForFile(MainActivity.this, fileProviderAuthority, picFile);
         else     //  Android 7 以前可以直接用file uri分享文件
            //  格式为:file:///storage/emulated/0/Android/data/com.zzk.a1510camera2/files/picture.jpg
            picUri = Uri.fromFile(picFile);
        

        Button btnOpenCamera = new Button(this);
        btnOpenCamera.setText("Open Camera");
        line1.addView(btnOpenCamera);
        btnOpenCamera.setOnClickListener(new View.OnClickListener() 
            @Override
            public void onClick(View view) 
                try 
                    openCamera();
                 catch (CameraAccessException e) 
                    e.printStackTrace();
                
            
        );

        Button btnCloseCamera = new Button(this);
        btnCloseCamera.setText("Close Camera");
        line1.addView(btnCloseCamera);
        btnCloseCamera.setOnClickListener(new View.OnClickListener() 
            @Override
            public void onClick(View view) 
                closeCamera();
            
        );

        Button btnCreateSession = new Button(this);
        btnCreateSession.setText("Create Session");
        line2.addView(btnCreateSession);
        btnCreateSession.setOnClickListener(new View.OnClickListener() 
            @Override
            public void onClick(View view) 
                try 
                    createCaptureSession();
                 catch (CameraAccessException e) 
                    e.printStackTrace();
                
            
        );

        Button btnCloseSession = new Button(this);
        btnCloseSession.setText("Close Session");
        line2.addView(btnCloseSession);
        btnCloseSession.setOnClickListener(new View.OnClickListener() 
            @Override
            public void onClick(View view) 
                closeCaptureSession();
            
        );

        Button btnStartPreview = new Button(this);
        btnStartPreview.setText("Start Preview");
        line3.addView(btnStartPreview);
        btnStartPreview.setOnClickListener(new View.OnClickListener() 
            @Override
            public void onClick(View view) 
                try 
                    startPreview();
                 catch (CameraAccessException e) 
                    e.printStackTrace();
                
            
        );

        Button btnStopPreview = new Button(this);
        btnStopPreview.setText("Stop Preview");
        line3.addView(btnStopPreview);
        btnStopPreview.setOnClickListener(new View.OnClickListener() 
            @Override
            public void onClick(View view) 
                try 
                    stopPreview();
                 catch (CameraAccessException e) 
                    e.printStackTrace();
                
            
        );

        Button btnCapture = new Button(this);
        btnCapture.setText("Capture");
        line4.addView(btnCapture);
        btnCapture.setOnClickListener(new View.OnClickListener() 
            @Override
            public void onClick(View view) 
                try 
                    capture();
                 catch (CameraAccessException e) 
                    e.printStackTrace();
                
            
        );

        Button btnView = new Button(this);
        btnView.setText("View");
        line4.addView(btnView);
        btnView.setOnClickListener(new View.OnClickListener() 
            @Override
            public void onClick(View arg0) 
                if (picFile.exists()) 
                    Intent intent = new Intent(Intent.ACTION_VIEW);
                    intent.setDataAndType(picUri, "image/*");
                    intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); //  授予对方读取该文件的权限
                    startActivity(intent);
                
            
        );

        Button btnCameraView = new Button(this);
        btnCameraView.setText("CameraView");
        line4.addView(btnCameraView);
        btnCameraView.setOnClickListener(new View.OnClickListener() 
            @Override
            public void onClick(View arg0) 
                Intent intent = new Intent(MainActivity.this, CameraActivity.class);
                startActivity(intent);
            
        );

    

    /**
     * 打开后置相机,将设备保存到mCameraDevice中
     * @throws CameraAccessException
     */
    private void openCamera() throws CameraAccessException 
        //  获取CameraManager
        CameraManager cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
        //  获取相机列表
        String[] cameraIdList = cameraManager.getCameraIdList();
        //  获取后置相机id
        mCameraId = null;
        for(String cameraId:cameraIdList)
            CameraCharacteristics cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId); //  获取相机特性
            if(cameraCharacteristics.get(CameraCharacteristics.LENS_FACING)==CameraCharacteristics.LENS_FACING_BACK)   //  后置相机
                mCameraId = cameraId;
                //  拍照支持的分辨率
                supportedSizes = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP).getOutputSizes(ImageReader.class);
                break;
            
        
        //  打开相机结果的回调函数(监听器)
        CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() 
            @Override
            public void onOpened(@NonNull CameraDevice cameraDevice) 
                mCameraDevice = cameraDevice;
                //  回调函数的代码在子线程中执行,所以不能直接发出Toast消息,只能通过主线程发出
                runOnUiThread(new Runnable() 
                    @Override
                    public void run() 
                        Toast.makeText(getBaseContext(), "Camera opened", Toast.LENGTH_SHORT).show();
                    
                );
            
            @Override
            public void onDisconnected(@NonNull CameraDevice cameraDevice) 
                mCameraDevice = null;
            

            @Override
            public void onError(@NonNull CameraDevice cameraDevice, int i) 
                cameraDevice.close();
                mCameraDevice = null;
            
        ;
        //  启动处理返回结果的后台线程
        if(mBackgroundHandler==null) startBackgroundThread();
        //  打开相机需要的权限检查
        if (ActivityCompat.checkSelfPermission(getBaseContext(), Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) 
            ActivityCompat.requestPermissions(MainActivity.this, new String[]Manifest.permission.CAMERA, 101);
            return;
        
        //  打开相机
        cameraManager.openCamera(mCameraId, stateCallback, mBackgroundHandler);
    

    /**
     * 关闭相机
     */
    private void closeCamera()
        if(mCameraDevice!=null) 
            mCameraDevice.close();
            mCameraDevice = null;
            Toast.makeText(getBaseContext(), "Camera closed", Toast.LENGTH_SHORT).show();
        
    

    private void startBackgroundThread() 
        mBackgroundThread = new HandlerThread("CameraBackground");
        mBackgroundThread.start();
        mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
    

    private void stopBackgroundThread() 
        mBackgroundThread.quitSafely();
        try 
            mBackgroundThread.join();
            mBackgroundThread = null;
            mBackgroundHandler = null;
         catch (InterruptedException e) 
            e.printStackTrace();
        
    

    /**
     *  创建CaptureSession
     */
    private void createCaptureSession() throws CameraAccessException 
        //  创建ImageReader
        createImageReader();
        //  接收图像数据的Surface
        List<Surface> surfaceList = new ArrayList<Surface>();
        surfaceList.add(mPreviewSurface);
        surfaceList.add(mImageReader.getSurface());
        //  处理创建结果的回调函数(监听器)
        CameraCaptureSession.StateCallback sessionStateCallback = new CameraCaptureSession.StateCallback() 
            @Override
            public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) 
                mCameraCaptureSession = cameraCaptureSession;
                //  回调函数的代码在子线程中执行,所以不能直接发出Toast消息,只能通过主线程发出
                runOnUiThread(new Runnable() 
                    @Override
                    public void run() 
                        Toast.makeText(getBaseContext(), "CaptureSession created", Toast.LENGTH_SHORT).show();
                    
                );
            
            @Override
            public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) 
            
        ;
        //  创建CameSession
        mCameraDevice.createCaptureSession(surfaceList, sessionStateCallback, mBackgroundHandler);
    

    /**
     * 创建ImageReader
     */
    private void createImageReader()
        if(mImageReader==null) 
            //  创建ImageReader,最多保存5张图片
            int width = supportedSizes[0].getWidth();
            int height = supportedSizes[0].getHeight();
            Log.i("zzk", "capture: size=" + width + ", " + height);
            mImageReader = ImageReader.newInstance(width, height, ImageFormat.JPEG, 5);
            ImageReader.OnImageAvailableListener listener = new ImageReader.OnImageAvailableListener() 
                @Override
                public void onImageAvailable(ImageReader imageReader) 
                    //  回调函数的代码在子线程中执行,所以不能直接发出Toast消息,只能通过主线程发出
                    runOnUiThread(new Runnable() 
                        @Override
                        public void run() 
                            Toast.makeText(getBaseContext(), "Image available", Toast.LENGTH_SHORT).show();
                        
                    );
                    Image image = imageReader.acquireNextImage();   //  取出一个图像
                    mBackgroundHandler.post(new ImageSaver(image, picFile));    //  在后台线程中将图像保存到文件
                
            ;
            mImageReader.setOnImageAvailableListener(listener, mBackgroundHandler); //  设置监听器,拍照完成后会执行上面的方法
        
    

    /**
     * 关闭CaptureSession
     */
    private void closeCaptureSession()
        if(mCameraCaptureSession!=null)
            mCameraCaptureSession.close();
            mCameraCaptureSession = null;
            Toast.makeText(getBaseContext(), "CaptureSession closed", Toast.LENGTH_SHORT).show();
        
    

    /**
     * 开始预览
     * @throws CameraAccessException
     */
    private void startPreview() throws CameraAccessException 
        //  通过Builder模式创建CaptureRequest,创建时使用预览模板
        CaptureRequest.Builder builder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
        //  加入接收图像数据的Surface
        builder.addTarget(mPreviewSurface);
        //  创建处理结果的回调函数(监听器)
        CameraCaptureSession.CaptureCallback captureCallback = new CameraCaptureSession.CaptureCallback();
        //  发送重复请求,让相机不断向预览组件发送图像数据
        mCameraCaptureSession.setRepeatingRequest(builder.build(), captureCallback, mBackgroundHandler);
    

    /**
     * 停止预览
     * @throws CameraAccessException
     */
    private void stopPreview() throws CameraAccessException 
        mCameraCaptureSession.stopRepeating();  //  取消重复请求
    

    /**
     * 拍照
     * @throws CameraAccessException
     */
    private  void capture() throws CameraAccessException 
        //  通过Builder模式创建CaptureRequest,创建时使用拍照模板
        CaptureRequest.Builder builder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
        //  加入接收图像数据的Surface,包括预览用的Surface和保存图片文件用的ImageReader,拍照的同时预览也不停顿
        builder.addTarget(mPreviewSurface);
        builder.addTarget(mImageReader.getSurface());
        //  处理结果的回调函数(监听器)
        CameraCaptureSession.CaptureCallback captureCallback = new CameraCaptureSession.CaptureCallback() ;
        //  发送单次请求,让相机向ImageReader发送一次图像数据,拍照完成后调用ImageReader的OnImageAvailableListener监听器的onImageAvailable方法
        mCameraCaptureSession.capture(builder.build(), captureCallback, mBackgroundHandler);
    


Android多媒体功能开发——使用VideoView控件播放视频

Android播放视频类主要有两种方式:

  1. VideoView控件
  2. SurfaceView控件+MediaPlayer

VideoView是SurfaceView的子类,实际上VideoView相当于SurfaceView + MediaPlayer。SurfaceView支持的功能VideoView都支持。也可用VideoView+MediaPlayer的方式播放。

视频播放控制可以使用MediaController控件,也可以用代码进行控制。MediaController是一个播放控制面板,可以加到VideoView控件上。同时,SurfaceView和VideoView都提供了播放控制的方法,所以可以用代码进行播放控制。


用VideoView播放视频的基本步骤是:创建或获取VideoView控件、设置视频数据源、开始播放、停止播放、释放资源。播放过程中可以暂停和继续。停止播放后可以恢复,此时会再从头播放。也可以重新设置视频数据源,播放另一个视频。基本流程和对应的代码如下图:

下面用一个简单的例子进行说明,界面和对应的关键代码如下:

视频源采用外部存储上的视频,通过视频选择界面进行选择。若要播放资源ID或assets中的文件,需采用VideoView+MediaPlayer的方式。在界面上用四个按钮对应VideoView对象的相应方法进行播放控制。设置一个播放控制面板,需点击视频才能出现,会自动隐藏。

例子的完整代码如下:

public class MainActivity extends AppCompatActivity 
    VideoView videoView;
    static final int PICK_VIDEO = 1;
    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        LinearLayout ll = new LinearLayout(this);
        ll.setOrientation(LinearLayout.VERTICAL);
        setContentView(ll);

        Button btnSelect = new Button(this);
        btnSelect.setText("Select Video");
        ll.addView(btnSelect);
        btnSelect.setOnClickListener(new View.OnClickListener()
            @Override
            public void onClick(View arg0) 
                Intent intent = new Intent(Intent.ACTION_GET_CONTENT);   //  选择文件动作
                intent.setType("video/*");
                startActivityForResult(intent, PICK_VIDEO); //  启动文件选择应用
            
        );
        Button btnPlay = new Button(this);
        btnPlay.setText("Play");
        ll.addView(btnPlay);
        btnPlay.setOnClickListener(new View.OnClickListener()
            @Override
            public void onClick(View arg0) 
                videoView.start();
            
        );
        Button btnPause = new Button(this);
        btnPause.setText("Pause");
        ll.addView(btnPause);
        btnPause.setOnClickListener(new View.OnClickListener()
            @Override
            public void onClick(View arg0) 
                videoView.pause();
            
        );
        Button btnResume = new Button(this);
        btnResume.setText("Resume");
        ll.addView(btnResume);
        btnResume.setOnClickListener(new View.OnClickListener()
            @Override
            public void onClick(View arg0) 
                videoView.resume();
            
        );
        Button btnStop = new Button(this);
        btnStop.setText("StopPlayback");
        ll.addView(btnStop);
        btnStop.setOnClickListener(new View.OnClickListener()
            @Override
            public void onClick(View arg0) 
                videoView.stopPlayback();
            
        );

        // 创建VideoView对象
        videoView = new VideoView(this);
        // 设置播放控制面板
        MediaController controller = new MediaController(this);
        videoView.setMediaController(controller);
        // 将VideoView控件添加到布局
        ll.addView(videoView);
    
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) 
        super.onActivityResult(requestCode, resultCode, data);
        //  处理选择视频的结果,选中的图片文件的uri保存在Intent的getData()中
        if (requestCode == PICK_VIDEO && resultCode == RESULT_OK) 
            Uri uri = data.getData();   //  得到选中的视频文件的uri
            Log.i("zzk", "选择视频成功, uri=" + uri);
            videoView.setVideoURI(uri);
        
    

    @Override
    protected void onDestroy()
        super.onDestroy();
        videoView.suspend();
    

VideoView类的主要方法包括:

  • void setVideoPath(String path):以文件路径的方式设置视频源
  • void setVideoURI(Uri uri):以Uri的方式设置视频源,网络Uri或本地Uri
  • void start():开始播放
  • void pause():暂停,调用start()可以继续
  • void stopPlayback():停止播放,调用resume()可以从头重新播放
  • void resume():重新播放
  • int getDuration():获取当前播放视频的总长度,单位为毫秒
  • int getCurrentPosition():获取当前播放的位置,单位为毫秒
  • void seekTo(int msec):从第几毫秒开始播放
  • isPlaying():是否在播放视频
  • setMediaController(MediaController controller):设置MediaController控制器
  • setOnCompletionListener(MediaPlayer.onCompletionListener l):设置监听播放完成的事件
  • setOnPreparedListener(MediaPlayer.OnPreparedListener l):设置监听视频装载完成的事件

以上是关于Android多媒体功能开发(14)——Camera2框架的主要内容,如果未能解决你的问题,请参考以下文章

Android多媒体功能开发——使用MediaPlayer类播放音频

Android多媒体功能开发(13)——使用MediaRecorder类录制视频

Android多媒体功能开发(11)——使用AudioRecord类录制音频

Android学习羁绊之多媒体开发

支持 Android Camera Api 和 Camera2 Api 的问题

Android+PHP开发最佳实践