Android 10 (Q) 中的后台服务录像机
Posted
技术标签:
【中文标题】Android 10 (Q) 中的后台服务录像机【英文标题】:Background Service Video Recorder in Android 10 (Q) 【发布时间】:2020-08-28 10:18:35 【问题描述】:我正在开发一个仅适用于 android 服务的应用程序,无需用户操作。
我想创建一个只使用服务的后台录像机。 我发现了几个项目,但它们太旧了(每个都大约 5 岁),就像这样:https://github.com/pickerweng/CameraRecorder
Android 文档不是很温和。 SurfaceView 似乎是一个解决方案,但不幸的是它只能在活动中创建。
谁有任何可能使用 Camera2 的线索?
【问题讨论】:
【参考方案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
-
在清单中这些权限
<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_
android:layout_
tools:context="com.theorbapp.MainActivity">
<androidx.camera.view.PreviewView
android:id="@+id/preview_container"
android:layout_
android:layout_
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_
android:layout_
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_
android:layout_
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_
android:layout_
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_
android:layout_
android:layout_gravity="center"
android:background="@drawable/ic_screen_rotation_24" />
</FrameLayout>
<FrameLayout
android:id="@+id/btn_back"
android:layout_
android:layout_
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_
android:layout_
android:layout_gravity="center"
android:background="@drawable/ic_baseline_navigate_before_24" />
</FrameLayout>
<FrameLayout
android:id="@+id/btn_record"
android:layout_
android:layout_
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_
android:layout_
android:layout_gravity="center"
android:background="@drawable/ic_videocam_24" />
</FrameLayout>
<FrameLayout
android:id="@+id/btn_mute"
android:layout_
android:layout_
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_
android:layout_
android:layout_gravity="center"
android:background="@drawable/ic_volume_up_24" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
此实现在后台录制视频。但是我发现的错误是,有时当您打开从后台录制返回的应用程序时,预览会变黑。我尝试修复此错误无济于事
【讨论】:
以上是关于Android 10 (Q) 中的后台服务录像机的主要内容,如果未能解决你的问题,请参考以下文章