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