Android视频数据的采集(上)

​ Android 平台视频数据的采集,需要用到设备的前置或者后置摄像头。若想使用摄像头采集视频数据,可以调用系统预装,或者已经安装的其它第三方相机应用拍照或者拍摄视频,这种方法比较简便,可以满足基本的视频采集需求,但是没法对照片或者视频的拍摄以及后续处理做过多控制。如果想在视频采集过程中进行图像的自定义处理,就需要使用系统提供的 Camera API, 开发自己的相机应用。目前想到的采集视频数据的方法有以下三种:

  1. 使用系统预装、或者已经安装的相机应用(例如美图等)采集视频数据;

  2. 使用 Jetpack CameraX 工具包采集视频数据;

  3. 使用 android 系统提供的 Camera API 开发自己的相机应用。

本篇介绍第一种方法,使用系统预装或者安装的其它相机应用采集视频数据。

​ 无论使用哪种方式,都需要在清单文件中声明需要的权限,包括使用相机的权限、保存已拍摄数据需要的写数据的权限,以及在代码中动态请求需要的权限。如果你的 app 在 google play store 上发布,并且没有相机就无法工作,还需要使用 <user-feature…> 标签声明app 要求的相机功能,方便 google play store 对不符合要求的设备隐藏该 app。

1
<manifest ... >
2
  			<uses-permission android:name="android.permission.CAMERA" />
3
    		<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
4
    		<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
5
        <uses-feature android:name="android.hardware.camera"
6
                      android:required="true" />
7
        ...
8
</manifest>

​ 几乎全部 Android 手机出厂时都预装有相机应用,使用预装相机拍摄照片大致分为两个步骤:在 Activity 里启用 Intent 打开相机,以及返回该 Activity 后对拿到的图片进行处理,包括压缩、保存、或者预览图片等。这里在 Activity 放置一个 ImageView,用来展示拍摄的照片:

1
class CameraActivity :AppCompatActivity(){
2
  private val REQUEST_CODE_IMAGE_CAPTURE = 1
3
  private lateinit var mImageView: ImageView
4
	
5
  override fun onCreate(savedInstanceState: Bundle?) {
6
        super.onCreate(savedInstanceState)
7
        setContentView(R.layout.activity_camera_intent)
8
        if (!hasCameraPermission(this)) {
9
            requestCameraPermission(this, true)
10
        }
11
12
        findViewById<Button>(R.id.image_capture).setOnClickListener {
13
            dispatchImageCaptureIntent()
14
        }
15
        mImageView = findViewById(R.id.image_over_view)
16
				//...
17
  }
18
  
19
  private fun dispatchImageCaptureIntent() {
20
        Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
21
            // Ensure that there's a camera activity to handle the intent
22
            startActivityForResult(takePictureIntent, REQUEST_CODE_IMAGE_CAPTURE)
23
        }
24
  }
25
26
  override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
27
    super.onActivityResult(requestCode, resultCode, data)
28
    if (requestCode == REQUEST_CODE_IMAGE_CAPTURE && resultCode == Activity.RESULT_OK) {
29
      // get the thumbnail
30
      val bitmap = data?.extras?.get("data")?.let { it as Bitmap }
31
      bitmap?.let {
32
        mPictureView.setImageBitmap(bitmap)
33
      }
34
    }
35
  }
36
37
  override fun onRequestPermissionsResult(
38
    requestCode: Int,
39
    permissions: Array<out String>,
40
    grantResults: IntArray
41
  ) {
42
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
43
    if (!hasCameraPermission(this)) {
44
      Toast.makeText(
45
        this,
46
        "Camera permission is needed to run this application", Toast.LENGTH_LONG
47
      ).show()
48
      launchPermissionSettings(this)
49
      finish()
50
    }
51
  }  
52
}

​ 上述代码在函数onActivityResult(...) 中拿到的 bitmap 只是一张缩略图,并不是完整的图片,如果想获取到完整的图片,需要启动 Intent的时候,传递给 Intent 一个保存图片的 uri 地址,后续通过这个 uri 地址获取到完整的图片。这里利用 FileProvider 获取保存图片的 uri 地址。

​ FileProvider 是 ContentProvider 的一个特殊子类,它通过创建 content:// Uri 格式的地址而不是 file:/// Uri格式的地址使文件间的共享变得更加安全,有关 FileProvider 的详细用法,请参考开发者文档

dispatchImageCaptureIntent() 函数做一些改动后,就可以获取到完整尺寸的照片:

1
private var mCurrentPhotoPath = ""
2
//...
3
4
private fun dispatchImageCaptureIntent() {
5
        Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
6
            // Ensure that there's a camera activity to handle the intent
7
            takePictureIntent.resolveActivity(packageManager)?.also {
8
                val photoFile = try {
9
                    createImageFile()
10
                } catch (ioException: IOException) {
11
                    Log.e(TAG, ioException.toString())
12
                    null
13
                }
14
15
                photoFile?.also {
16
                    // storage/emulated/0/Android/data/com.shaowei.streaming/files/Pictures/JPEG_...
17
                    val photoUri = FileProvider.getUriForFile(this, FILE_PROVIDER_AUTHORITY, it)
18
                    takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
19
                    startActivityForResult(takePictureIntent, REQUEST_CODE_IMAGE_CAPTURE)
20
                }
21
            }
22
        }
23
}
24
25
private fun createImageFile(): File {
26
        // Create an image file name
27
        val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
28
        val storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
29
        return File.createTempFile(
30
            "JPEG_${timeStamp}_", /* prefix */
31
            ".jpg", /* suffix */
32
            storageDir /* directory */
33
        ).apply {
34
            // Save a file: path for use with ACTION_VIEW intents
35
            mCurrentPhotoPath = absolutePath
36
            Log.e(TAG, mCurrentPhotoPath)
37
        }
38
}

​ 需要注意的是,如果保存了完整尺寸的图片,之前通过 Intent 返回的缩略图数据就没有了,想要了解为什么系统这么设计,还需要进一步探究,或许 Android 系统认为已经拿到了完成尺寸的图片,开发者就可以制作任何尺寸的缩略图。

​ 以上介绍了使用系统相机拍摄图片、保存图片的方法,如果想要录制视频该怎么办呢?

​ 方法和拍摄照片一致,只需要把 Intent 的 action 从 MediaStore.ACTION_IMAGE_CAPTURE 替换为 MediaStore.ACTION_VIDEO_CAPTURE, 在函数onActivityResult() 里接收到的就是已录制的视频 uri, 这里我们使用 VideoView 播放刚刚录制的视频:

1
//...
2
Private val REQUEST_CODE_VIDEO_CAPTURE = 2
3
private lateinit var mVideoView: VideoView
4
//...
5
6
override fun onCreate(savedInstanceState: Bundle?) {
7
        super.onCreate(savedInstanceState)
8
        setContentView(R.layout.activity_camera_intent)     
9
        mVideoView = findViewById(R.id.video_view)
10
  			//...
11
}
12
13
private fun dispatchVideoCaptureIntent() {
14
        Intent(MediaStore.ACTION_VIDEO_CAPTURE).also { takeVideoIntent ->
15
            takeVideoIntent.resolveActivity(packageManager)?.also {
16
                startActivityForResult(takeVideoIntent, REQUEST_CODE_VIDEO_CAPTURE)
17
            }
18
        }
19
}
20
21
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
22
        super.onActivityResult(requestCode, resultCode, data)
23
        if (requestCode == REQUEST_CODE_VIDEO_CAPTURE && resultCode == Activity.RESULT_OK) {
24
            val videoUri = intent?.data
25
            Log.e(TAG, videoUri.toString())
26
            mVideoView.setVideoURI(videoUri)
27
            mVideoView.start()
28
        }
29
}
30
31
override fun onStop() {
32
        super.onStop()
33
        mVideoView.stopPlayback()
34
}

​ 今天的内容介绍到这里,希望这部分内容,可以让你对使用系统预装相机应用拍照和录制视频有一个清晰的了解,完整示例代码请移步Github-StreamingTour