【问题标题】:Background Service Video Recorder in Android 10 (Q)Android 10 (Q) 中的后台服务录像机
【发布时间】:2020-08-28 10:18:35
【问题描述】:

我正在开发一个仅适用于 android 服务的应用程序,无需用户操作。

我想创建一个只使用服务的后台录像机。 我发现了几个项目,但它们太旧了(每个都大约 5 岁),就像这样:https://github.com/pickerweng/CameraRecorder

Android 文档不是很温和。 SurfaceView 似乎是一个解决方案,但不幸的是它只能在活动中创建。

谁有任何可能使用 Camera2 的线索?

【问题讨论】:

    标签: android service background camera recording


    【解决方案1】:

    除非您是前台应用程序或前台服务(带有持久通知),否则您无法在 Android Q 或更高版本上使用相机。

    也就是说,如果您是其中一种情况,您可以使用已弃用的相机 API 或更新的 camera2 API,而无需进行绘图预览。

    对于旧 API,您可以只使用 SurfaceTexture 作为预览输出,否则永远不要对 SurfaceTexture 做任何事情;对于camera2,只设置一个录制输出Surface,没有预览输出Surface。

    【讨论】:

    • 有通知没关系...我只是不想使用任何Activity。你知道我可以在前台服务和持久通知中使用的代码吗?
    • 我不知道实现我建议的示例代码。我可能会尝试通过删除预览输出目标并将其移动为前台服务而不是活动来调整 camera2video:github.com/android/camera-samples/tree/master/Camera2Video
    【解决方案2】:

    这是使用 CameraX 的后台录制应用程序的完整工作实现:

    1) 这里是后台服务

        class MediaRecordingService : LifecycleService() {
    
        companion object {
            const val CHANNEL_ID: String = "media_recorder_service"
            private val TAG = MediaRecordingService::class.simpleName
            private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
            const val CHANNEL_NAME: String = "Media recording service"
            const val ONGOING_NOTIFICATION_ID: Int = 2345
            const val ACTION_START_WITH_PREVIEW: String = "start_recording"
            const val BIND_USECASE: String = "bind_usecase"
        }
    
        enum class RecordingState {
            RECORDING, PAUSED, STOPPED
        }
    
        class RecordingServiceBinder(private val service: MediaRecordingService) : Binder() {
            fun getService(): MediaRecordingService {
                return service
            }
        }
        private var preview: Preview? = null
        private lateinit var timer: Timer
        private var cameraProvider: ProcessCameraProvider? = null
        private lateinit var recordingServiceBinder: RecordingServiceBinder
        private var activeRecording: ActiveRecording? = null
        private var videoCapture: androidx.camera.video.VideoCapture<Recorder>? = null
        private val listeners = HashSet<DataListener>(1)
        private val pendingActions: HashMap<String, Runnable> = hashMapOf()
        private var recordingState: RecordingState = RecordingState.STOPPED
        private var duration: Int = 0
        private var timerTask: TimerTask? = null
        private var isSoundEnabled: Boolean = true
    
        override fun onCreate() {
            super.onCreate()
            recordingServiceBinder = RecordingServiceBinder(this)
            timer = Timer()
        }
    
        override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
            super.onStartCommand(intent, flags, startId)
            when(intent?.action) {
                ACTION_START_WITH_PREVIEW -> {
                    if (cameraProvider == null) {
                        initializeCamera()
                    }
                }
            }
            return START_NOT_STICKY
        }
    
        private fun initializeCamera() {
            val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
            cameraProviderFuture.addListener({
                // Used to bind the lifecycle of cameras to the lifecycle owner
                cameraProvider = cameraProviderFuture.get()
                val qualitySelector = getQualitySelector()
                val recorder = Recorder.Builder()
                    .setQualitySelector(qualitySelector)
                    .build()
                videoCapture = withOutput(recorder)
                            // Select back camera as a default
                val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
    
                try {
                    // Unbind use cases before rebinding
                    cameraProvider?.unbindAll()
                    // Bind use cases to camera
                    cameraProvider?.bindToLifecycle(this, cameraSelector, videoCapture)
                } catch(exc: Exception) {
                    Log.e(MediaRecordingService::class.simpleName, "Use case binding failed", exc)
                }
                val action = pendingActions[BIND_USECASE]
                action?.run()
                pendingActions.remove(BIND_USECASE)
            }, ContextCompat.getMainExecutor(this))
        }
    
        private fun getQualitySelector(): QualitySelector {
            return QualitySelector
                .firstTry(QualitySelector.QUALITY_UHD)
                .thenTry(QualitySelector.QUALITY_FHD)
                .thenTry(QualitySelector.QUALITY_HD)
                .finallyTry(
                    QualitySelector.QUALITY_SD,
                    QualitySelector.FALLBACK_STRATEGY_LOWER
                )
        }
    
        fun startRecording() {
            val mediaStoreOutputOptions = createMediaStoreOutputOptions()
            if (ActivityCompat.checkSelfPermission(
                    this,
                    Manifest.permission.RECORD_AUDIO
                ) != PackageManager.PERMISSION_GRANTED
            ) {
                return
            }
    
            var pendingRecording = videoCapture?.output?.prepareRecording(this, mediaStoreOutputOptions)
            if (isSoundEnabled) {
                pendingRecording = pendingRecording?.withAudioEnabled()
            }
            activeRecording = pendingRecording?.withEventListener(ContextCompat.getMainExecutor(this),
                    {
                        when (it) {
                            is VideoRecordEvent.Start -> {
                                startTrackingTime()
                                recordingState = RecordingState.RECORDING
                            }
    
                            is VideoRecordEvent.Finalize -> {
                                recordingState = RecordingState.STOPPED
                                duration = 0
                                timerTask?.cancel()
                            }
                        }
                        for (listener in listeners) {
                            listener.onRecordingEvent(it)
                        }
                    })
                ?.start()
            recordingState = RecordingState.RECORDING
        }
    
        private fun startTrackingTime() {
            timerTask = object: TimerTask() {
                override fun run() {
                    if (recordingState == RecordingState.RECORDING) {
                        duration += 1
                        for (listener in listeners) {
                            listener.onNewData(duration)
                        }
                    }
                }
            }
            timer.scheduleAtFixedRate(timerTask, 1000, 1000)
        }
    
        fun stopRecording() {
            activeRecording?.stop()
            activeRecording = null
        }
    
        private fun createMediaStoreOutputOptions(): MediaStoreOutputOptions {
            val name = "CameraX-recording-" +
                    SimpleDateFormat(FILENAME_FORMAT, Locale.getDefault())
                        .format(System.currentTimeMillis()) + ".mp4"
            val contentValues = ContentValues().apply {
                put(MediaStore.Video.Media.DISPLAY_NAME, name)
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/Recorded Videos")
                }
            }
            return MediaStoreOutputOptions.Builder(
                contentResolver,
                MediaStore.Video.Media.EXTERNAL_CONTENT_URI
            )
                .setContentValues(contentValues)
                .build()
        }
    
        fun bindPreviewUseCase(surfaceProvider: Preview.SurfaceProvider?) {
            activeRecording?.pause()
            if (cameraProvider != null) {
                bindInternal(surfaceProvider)
            } else {
                pendingActions[BIND_USECASE] = Runnable {
                    bindInternal(surfaceProvider)
                }
            }
        }
    
        private fun bindInternal(surfaceProvider: Preview.SurfaceProvider?) {
            if (preview != null) {
                cameraProvider?.unbind(preview)
            }
            initPreviewUseCase()
            preview?.setSurfaceProvider(surfaceProvider)
            val cameraInfo: CameraInfo? = cameraProvider?.bindToLifecycle(
                this@MediaRecordingService,
                CameraSelector.DEFAULT_BACK_CAMERA,
                preview
            )?.cameraInfo
            observeCameraState(cameraInfo, this)
        }
    
        private fun initPreviewUseCase() {
            preview?.setSurfaceProvider(null)
            preview = Preview.Builder()
                .build()
        }
    
        fun unbindPreview() {
            // Just remove the surface provider. I discovered that for some reason if you unbind the Preview usecase the camera willl stop recording the video.
            preview?.setSurfaceProvider(null)
        }
    
        fun startRunningInForeground() {
            val parentStack = TaskStackBuilder.create(this)
                .addNextIntentWithParentStack(Intent(this, MainActivity::class.java))
    
            val pendingIntent1 = parentStack.getPendingIntent(0, 0)
    
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT)
                val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
                nm.createNotificationChannel(channel)
            }
    
            val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
                .setContentTitle(getText(R.string.video_recording))
                .setContentText(getText(R.string.video_recording_in_background))
                .setSmallIcon(R.drawable.ic_record)
                .setContentIntent(pendingIntent1)
                .build()
            startForeground(ONGOING_NOTIFICATION_ID, notification)
        }
    
        fun isSoundEnabled(): Boolean {
            return isSoundEnabled
        }
    
        fun setSoundEnabled(enabled: Boolean) {
            isSoundEnabled = enabled
        }
    
        // Stop recording and remove SurfaceView
        override fun onDestroy() {
            super.onDestroy()
            activeRecording?.stop()
            timerTask?.cancel()
        }
    
        override fun onBind(intent: Intent): IBinder {
            super.onBind(intent)
            return recordingServiceBinder
        }
    
        fun addListener(listener: DataListener) {
            listeners.add(listener)
        }
    
        fun removeListener(listener: DataListener) {
            listeners.remove(listener)
        }
    
        fun getRecordingState(): RecordingState {
            return recordingState
        }
    
        private fun observeCameraState(cameraInfo: androidx.camera.core.CameraInfo?, context: Context) {
            cameraInfo?.cameraState?.observe(this) { cameraState ->
                run {
                    when (cameraState.type) {
                        CameraState.Type.PENDING_OPEN -> {
                            // Ask the user to close other camera apps
                        }
                        CameraState.Type.OPENING -> {
                            // Show the Camera UI
                            for (listener in listeners) {
                                listener.onCameraOpened()
                            }
                        }
                        CameraState.Type.OPEN -> {
                            // Setup Camera resources and begin processing
                        }
                        CameraState.Type.CLOSING -> {
                            // Close camera UI
                        }
                        CameraState.Type.CLOSED -> {
                            // Free camera resources
                        }
                    }
                }
    
                cameraState.error?.let { error ->
                    when (error.code) {
                        // Open errors
                        CameraState.ERROR_STREAM_CONFIG -> {
                            // Make sure to setup the use cases properly
                            Toast.makeText(context,
                                "Stream config error. Restart application",
                                Toast.LENGTH_SHORT).show()
                        }
                        // Opening errors
                        CameraState.ERROR_CAMERA_IN_USE -> {
                            // Close the camera or ask user to close another camera app that's using the
                            // camera
                            Toast.makeText(context,
                                "Camera in use. Close any apps that are using the camera",
                                Toast.LENGTH_SHORT).show()
                        }
                        CameraState.ERROR_MAX_CAMERAS_IN_USE -> {
                            // Close another open camera in the app, or ask the user to close another
                            // camera app that's using the camera
                        }
                        CameraState.ERROR_OTHER_RECOVERABLE_ERROR -> {
    
                        }
                        // Closing errors
                        CameraState.ERROR_CAMERA_DISABLED -> {
                             // Ask the user to enable the device's cameras
                            Toast.makeText(context,
                                "Camera disabled",
                                Toast.LENGTH_SHORT).show()
                        }
                        CameraState.ERROR_CAMERA_FATAL_ERROR -> {
                            // Ask the user to reboot the device to restore camera function
                            Toast.makeText(context,
                                "Fatal error",
                                Toast.LENGTH_SHORT).show()
                        }
                        // Closed errors
                        CameraState.ERROR_DO_NOT_DISTURB_MODE_ENABLED -> {
                            // Ask the user to disable the "Do Not Disturb" mode, then reopen the camera
                            Toast.makeText(context,
                                "Do not disturb mode enabled",
                                Toast.LENGTH_SHORT).show()
                        }
                    }
                }
            }
        }
    
        interface DataListener {
            fun onNewData(duration: Int)
            fun onCameraOpened()
            fun onRecordingEvent(it: VideoRecordEvent?)
        }
    
        }
    

    添加这些依赖项

    implementation "androidx.camera:camera-video:1.1.0-alpha11"
        implementation "androidx.camera:camera-camera2:1.1.0-alpha11"
        implementation "androidx.camera:camera-lifecycle:1.1.0-alpha11"
        implementation "androidx.camera:camera-view:1.0.0-alpha31"
        implementation "androidx.camera:camera-extensions:1.0.0-alpha31"
        implementation "androidx.lifecycle:lifecycle-service:2.4.0"
    

    2) 这是活动

    class MainActivity : AppCompatActivity(), MediaRecordingService.DataListener {
        private var recordingService: MediaRecordingService? = null
        private lateinit var viewBinding: ActivityMainBinding
        private var isReverseLandscape: Boolean = false
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            viewBinding = ActivityMainBinding.inflate(layoutInflater)
            setContentView(viewBinding.root)
            viewBinding.btnRecord.setOnClickListener {
                onPauseRecordClicked()
            }
            viewBinding.btnMute.setOnClickListener { onMuteRecordingClicked() }
            viewBinding.btnRotate.setOnClickListener {
                requestedOrientation = if (isReverseLandscape) {
                    ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
                } else {
                    ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
                }
                isReverseLandscape = !isReverseLandscape
            }
            viewBinding.btnBack.setOnClickListener {
                onBackPressedDispatcher.onBackPressed()
            }
        }
    
        private fun onMuteRecordingClicked() {
            if(recordingService == null) return
            var soundEnabled = recordingService?.isSoundEnabled()
            soundEnabled = !soundEnabled!!
            recordingService?.setSoundEnabled(soundEnabled)
            setSoundState(soundEnabled)
        }
    
        private fun setSoundState(soundEnabled: Boolean) {
            if (soundEnabled){
                viewBinding.viewMute.setBackgroundResource(R.drawable.ic_volume_up_24)
            } else {
                viewBinding.viewMute.setBackgroundResource(R.drawable.ic_volume_off_24)
            }
        }
    
        private fun bindService() {
            val intent = Intent(this, MediaRecordingService::class.java)
            intent.action = MediaRecordingService.ACTION_START_WITH_PREVIEW
            startService(intent)
            bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
        }
    
        override fun onStart() {
            super.onStart()
            bindService()
        }
    
        private val serviceConnection: ServiceConnection = object : ServiceConnection {
            override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
                recordingService = (service as MediaRecordingService.RecordingServiceBinder).getService()
                onServiceBound(recordingService)
            }
    
            override fun onServiceDisconnected(name: ComponentName?) {
    
            }
        }
    
        private fun onServiceBound(recordingService: MediaRecordingService?) {
            when(recordingService?.getRecordingState()){
                MediaRecordingService.RecordingState.RECORDING -> {
                    viewBinding.viewRecordPause.setBackgroundResource(R.drawable.ic_baseline_stop_24)
                    viewBinding.btnMute.visibility = View.INVISIBLE
                }
                MediaRecordingService.RecordingState.STOPPED -> {
                    viewBinding.viewRecordPause.setBackgroundResource(R.drawable.ic_videocam_24)
                    viewBinding.txtDuration.text = "00:00:00"
                    viewBinding.btnMute.visibility = View.VISIBLE
                    setSoundState(recordingService.isSoundEnabled())
                }
                else -> {
                    // no-op
                }
            }
    
            recordingService?.addListener(this)
            recordingService?.bindPreviewUseCase(viewBinding.previewContainer.surfaceProvider)
        }
    
        private fun onPauseRecordClicked() {
            when(recordingService?.getRecordingState()){
                MediaRecordingService.RecordingState.RECORDING -> {
                    recordingService?.stopRecording()
                    viewBinding.viewRecordPause.setBackgroundResource(R.drawable.ic_videocam_24)
                    viewBinding.txtDuration.text = "00:00:00"
                }
                MediaRecordingService.RecordingState.STOPPED -> {
                    viewBinding.viewRecordPause.setBackgroundResource(R.drawable.ic_baseline_stop_24)
                    recordingService?.startRecording()
                }
                else -> {
                    // no-op
                }
            }
        }
    
        @SuppressLint("SetTextI18n")
        override fun onNewData(duration: Int) {
            runOnUiThread {
                var seconds = duration
                var minutes = seconds / MINUTE
                seconds %= MINUTE
                val hours = minutes / HOUR
                minutes %= HOUR
    
                val hoursString = if (hours >= 10) hours.toString() else "0$hours"
                val minutesString = if (minutes >= 10) minutes.toString() else "0$minutes"
                val secondsString = if (seconds >= 10) seconds.toString() else "0$seconds"
                viewBinding.txtDuration.text = "$hoursString:$minutesString:$secondsString"
            }
        }
    
        override fun onCameraOpened() {
    
        }
    
        override fun onRecordingEvent(it: VideoRecordEvent?) {
            when (it) {
                is VideoRecordEvent.Start -> {
                    viewBinding.btnMute.visibility = View.INVISIBLE
                    viewBinding.viewRecordPause.setBackgroundResource(R.drawable.ic_baseline_stop_24)
                }
    
                is VideoRecordEvent.Finalize -> {
                    recordingService?.isSoundEnabled()?.let { it1 -> setSoundState(it1) }
                    viewBinding.btnMute.visibility = View.VISIBLE
                    viewBinding.viewRecordPause.setBackgroundResource(R.drawable.ic_videocam_24)
                    onNewData(0)
                    val intent = Intent(Intent.ACTION_VIEW, it.outputResults.outputUri)
                    intent.setDataAndType(it.outputResults.outputUri, "video/mp4")
                    startActivity(Intent.createChooser(intent, "Open recorded video"))
                }
            }
        }
    
        override fun onStop() {
            super.onStop()
            if (recordingService?.getRecordingState() == MediaRecordingService.RecordingState.STOPPED) {
                recordingService?.let {
                    ServiceCompat.stopForeground(it, ServiceCompat.STOP_FOREGROUND_REMOVE)
                    recordingService?.stopSelf()
                }
            } else {
                recordingService?.startRunningInForeground()
            }
            recordingService?.unbindPreview()
            recordingService?.removeListener(this)
        }
    
        companion object {
            private const val MINUTE: Int = 60
            private const val HOUR: Int = MINUTE * 60
        }
    }
    
    1. 在清单中这些权限

        <uses-feature android:name="android.hardware.camera.any" />
        <uses-permission android:name="android.permission.CAMERA" />
        <uses-permission android:name="android.permission.RECORD_AUDIO" />
        <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> 
        <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
            android:maxSdkVersion="28" />

    这是活动布局:

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/root_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.theorbapp.MainActivity">
    
        <androidx.camera.view.PreviewView
            android:id="@+id/preview_container"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.0" />
    
        <FrameLayout
            android:id="@+id/frameLayout"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginTop="16dp"
            android:background="@color/transparent_black"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">
    
            <TextView
                android:id="@+id/txt_duration"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:paddingStart="4dp"
                android:paddingEnd="4dp"
                android:textAppearance="@style/TextAppearance.AppCompat.Medium"
                android:textColor="@color/white"
                tools:text="00:01:25" />
        </FrameLayout>
    
        <FrameLayout
            android:id="@+id/btn_rotate"
            android:layout_width="@dimen/button_radius"
            android:layout_height="@dimen/button_radius"
            android:layout_marginEnd="16dp"
            android:animateLayoutChanges="true"
            android:background="@drawable/circle_drawable"
            app:layout_constraintBottom_toBottomOf="@+id/btn_back"
            app:layout_constraintEnd_toStartOf="@+id/btn_back"
            app:layout_constraintTop_toTopOf="@+id/btn_back">
    
            <View
                android:id="@+id/view"
                android:layout_width="@dimen/button_inner_radius"
                android:layout_height="@dimen/button_inner_radius"
                android:layout_gravity="center"
                android:background="@drawable/ic_screen_rotation_24" />
        </FrameLayout>
    
        <FrameLayout
            android:id="@+id/btn_back"
            android:layout_width="@dimen/button_radius"
            android:layout_height="@dimen/button_radius"
            android:layout_marginTop="16dp"
            android:layout_marginEnd="16dp"
            android:animateLayoutChanges="true"
            android:background="@drawable/circle_drawable"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent">
    
            <View
                android:id="@+id/view2"
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:layout_gravity="center"
                android:background="@drawable/ic_baseline_navigate_before_24" />
        </FrameLayout>
    
        <FrameLayout
            android:id="@+id/btn_record"
            android:layout_width="56dp"
            android:layout_height="56dp"
            android:background="@drawable/circle_drawable"
            app:layout_constraintBottom_toTopOf="@+id/btn_mute"
            app:layout_constraintEnd_toEndOf="@+id/btn_back"
            app:layout_constraintTop_toBottomOf="@+id/btn_back">
    
            <View
                android:id="@+id/view_record_pause"
                android:layout_width="36dp"
                android:layout_height="36dp"
                android:layout_gravity="center"
                android:background="@drawable/ic_videocam_24" />
        </FrameLayout>
    
        <FrameLayout
            android:id="@+id/btn_mute"
            android:layout_width="@dimen/button_radius"
            android:layout_height="@dimen/button_radius"
            android:layout_marginEnd="16dp"
            android:layout_marginBottom="16dp"
            android:animateLayoutChanges="true"
            android:background="@drawable/circle_drawable"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent">
    
            <View
                android:id="@+id/view_mute"
                android:layout_width="@dimen/button_inner_radius"
                android:layout_height="@dimen/button_inner_radius"
                android:layout_gravity="center"
                android:background="@drawable/ic_volume_up_24" />
        </FrameLayout>
    
    </androidx.constraintlayout.widget.ConstraintLayout>

    此实现在后台录制视频。但是我发现的错误是,有时当您打开从后台录制返回的应用程序时,预览会变黑。我尝试修复此错误无济于事

    【讨论】:

      猜你喜欢
      • 2021-06-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-04-11
      相关资源
      最近更新 更多