安卓kotlin JetPack Compose 实现摄像头监控画面变化并录制视频

简介: 在这个示例中,开发者正在使用Kotlin和Jetpack Compose构建一个Android应用程序,该程序 能够通过手机后置主摄像头录制视频、检测画面差异、实时预览并将视频上传至FTP服务器的Android应用

kotlin开发安卓app,Jetpack Compose框架。 通过手机后置主摄像头数据,要求能自动获取摄像头的最佳帧率和最大画面尺寸。  循环读取每一帧,通过两帧之间差异,来判断画面是否有变化,如果有变化,则开始录制视频,最少录制5秒。如果一直有差异,则一直录制,直到前后两帧之间没有差异。录制的视频文件名使用日期和时间命名,输出的视频为h264格式,。 使用viewModel控制摄像头, UI层提供一个开始和停止按钮。 同时在UI界面上,能实时预览摄像头的画面。 视频录制完上传到远程ftp中, ftp地址 192.168.10.100:21  用户名 xxx 密码 xxx,上传时,按照日期存放




为了在 Jetpack Compose 环境下开发一个使用后置摄像头采集视频,并在帧差异检测的基础上自动录制的 Android 应用,我们可以使用 CameraX 和 ViewModel,同时结合 FTP 上传功能。

下面这个完整示例展示了如何使用 Jetpack Compose 和 CameraX 编写这个应用。

1. 添加依赖

在 build.gradle 中添加所需的依赖:


dependencies {
    implementation "androidx.compose.ui:ui:1.3.0"
    implementation "androidx.compose.material:material:1.3.0"
    implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1"
    implementation "androidx.camera:camera-camera2:1.1.0"
    implementation "androidx.camera:camera-lifecycle:1.1.0"
    implementation "androidx.camera:camera-video:1.0.0-alpha28"
    implementation "androidx.camera:camera-view:1.1.0"
    implementation 'commons-net:commons-net:3.6'
    implementation 'androidx.activity:activity-compose:1.3.1'
}



2. 权限配置

在 AndroidManifest.xml 中添加所需的权限:



<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />



3. 创建 ViewModel

我们需要一个 ViewModel 来管理摄像头的操作、视频帧分析和录像控制。

创建 CameraViewModel.kt:



import android.content.Context
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.video.*
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

class CameraViewModel : ViewModel() {
    var isRecording = mutableStateOf(false)
    var previewUri = mutableStateOf<Uri?>(null)
    private var videoCapture: VideoCapture<Recorder>? = null
    private var recording: Recording? = null
    private lateinit var cameraExecutor: ExecutorService

    fun startCamera(context: Context, previewView: PreviewView) {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(context)

        cameraProviderFuture.addListener({
            val cameraProvider = cameraProviderFuture.get()
            val preview = Preview.Builder().build().also {
                it.setSurfaceProvider(previewView.surfaceProvider)
            }

            val recorder = Recorder.Builder()
                .setQualitySelector(QualitySelector.from(Quality.HIGHEST))
                .build()

            videoCapture = VideoCapture.withOutput(recorder)
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            try {
                cameraProvider.unbindAll()
                cameraProvider.bindToLifecycle(
                    context as androidx.lifecycle.LifecycleOwner,
                    cameraSelector,
                    preview,
                    videoCapture
                )
            } catch (exc: Exception) {
                Log.e("CameraViewModel", "Use case binding failed", exc)
            }
            
            detectMotionAndRecord(context) // Start detecting motion and recording
        }, ContextCompat.getMainExecutor(context))

        cameraExecutor = Executors.newSingleThreadExecutor()
    }

    private fun detectMotionAndRecord(context: Context) {
        var prevFrame: ImageProxy? = null
        val handler = Handler(Looper.getMainLooper())
        val delayMillis = 100L

        val captureRunnable = object : Runnable {
            override fun run() {
                if (isRecording.value) {
                    recording?.stop()
                    isRecording.value = false
                }

                // Capture frame
                videoCapture?.takePicture(cameraExecutor, ContextCompat.getMainExecutor(context), object : ImageCapture.OnImageCapturedCallback() {
                    override fun onCaptureSuccess(image: ImageProxy) {
                        if (prevFrame == null) {
                            prevFrame = image
                        } else {
                            // Compare frames
                            val isDifferent = compareFrames(prevFrame!!, image)
                            prevFrame?.close()
                            prevFrame = image

                            if (isDifferent) {
                                if (!isRecording.value) {
                                    startRecording(context)
                                    isRecording.value = true
                                }
                            } else {
                                if (System.currentTimeMillis() - recordingStartTime > 5000) {
                                    recording?.stop()
                                    isRecording.value = false
                                }
                            }
                        }
                        handler.postDelayed(this, delayMillis)
                    }

                    override fun onError(exception: ImageCaptureException) {
                        Log.e("CameraViewModel", "Image capture failed", exception)
                    }
                })
            }
        }
        handler.post(captureRunnable)
    }

    private var recordingStartTime = 0L

    private fun compareFrames(frame1: ImageProxy, frame2: ImageProxy): Boolean {
        // Implement frame comparison logic (e.g., by comparing pixel values)
        return true // example return, implement actual comparison
    }

    private fun startRecording(context: Context) {
        val fileName = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US)
            .format(System.currentTimeMillis()) + ".mp4"
        val file = File(context.externalMediaDirs.first(), fileName)
        val outputOptions = VideoCapture.OutputFileOptions.Builder(file).build()

        recordingStartTime = System.currentTimeMillis()
        videoCapture?.startRecording(
            outputOptions,
            ContextCompat.getMainExecutor(context),
            object : VideoCapture.OnVideoSavedCallback {
                override fun onVideoSaved(outputFileResults: VideoCapture.OutputFileResults) {
                    val savedUri = Uri.fromFile(file)
                    previewUri.value = savedUri
                    uploadToFTP(file)
                }

                override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {
                    Log.e("CameraViewModel", "Video capture failed: $message", cause)
                }
            })
    }

    private fun uploadToFTP(file: File) {
        viewModelScope.launch(Dispatchers.IO) {
            FTPUpload.uploadFile(
                file.path,
                "192.168.10.100", 21,
                "yyy", "xxx"
            )
        }
    }
}




4. 创建UI

在MainActivity.kt中设置UI:



import android.Manifest
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.google.accompanist.permissions.shouldShowRationale
import java.util.*

class MainActivity : ComponentActivity() {
    private val cameraViewModel: CameraViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            JetpackComposeCameraTheme {
                Surface(modifier = Modifier.fillMaxSize()) {
                    val context = remember { this }
                    val permissionState = rememberMultiplePermissionsState(
                        permissions = listOf(
                            Manifest.permission.CAMERA,
                            Manifest.permission.RECORD_AUDIO,
                            Manifest.permission.WRITE_EXTERNAL_STORAGE,
                            Manifest.permission.READ_EXTERNAL_STORAGE
                        )
                    )

                    if (permissionState.allPermissionsGranted) {
                        CameraPreview(cameraViewModel)
                    } else {
                        Column(
                            Modifier
                                .fillMaxSize()
                                .padding(16.dp)
                        ) {
                            Text("Permissions need to be granted to use the camera")

                            Spacer(modifier = Modifier.height(8.dp))

                            if (permissionState.shouldShowRationale) {
                                Text("Please allow the permissions.")
                            } else {
                                OutlinedButton(onClick = { permissionState.launchMultiplePermissionRequest() }) {
                                    Text("Allow")
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

@Composable
fun CameraPreview(cameraViewModel: CameraViewModel) {
    val context = LocalContext.current
    val isRecording = cameraViewModel.isRecording.collectAsState()
    
    Column(modifier = Modifier.fillMaxSize()) {
        Box(modifier = Modifier
            .weight(1f)
            .fillMaxSize()) {
            
            AndroidView(
                factory = { ctx ->
                    val previewView = PreviewView(ctx)
                    cameraViewModel.startCamera(ctx, previewView)
                    previewView
                },
                modifier = Modifier.fillMaxSize()
            )
        }

        Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
            Button(onClick = { /* Trigger cameraViewModel logic here */ },
                modifier = Modifier.padding(16.dp)) {
                Text("Start Recording")
            }
        
            Button(onClick = { /* Trigger cameraViewModel logic here */ },
                modifier = Modifier.padding(16.dp)) {
                Text("Stop Recording")
            }
        }
    }
}



5. 实现FTP上传功能

创建FTPUpload.kt以实现FTP上传功能:



import org.apache.commons.net.ftp.FTP
import org.apache.commons.net.ftp.FTPClient
import java.io.File
import java.io.FileInputStream
import java.io.IOException

object FTPUpload {

    fun uploadFile(filePath: String, server: String, port: Int, user: String, pass: String) {
        val ftpClient = FTPClient()
        try {
            ftpClient.connect(server, port)
            ftpClient.login(user, pass)
            ftpClient.enterLocalPassiveMode()
            ftpClient.setFileType(FTP.BINARY_FILE_TYPE)

            val dateFolder = SimpleDateFormat("yyyyMMdd", Locale.US).format(System.currentTimeMillis())
            ftpClient.makeDirectory(dateFolder)
            ftpClient.changeWorkingDirectory(dateFolder)

            val firstRemoteFile = File(filePath).name
            val inputStream = FileInputStream(filePath)

            println("Start uploading first file")
            val done = ftpClient.storeFile(firstRemoteFile, inputStream)
            inputStream.close()

            if (done) {
                println("File is uploaded successfully.")
            }
        } catch (ex: IOException) {
            ex.printStackTrace()
        } finally {
            try {
                if (ftpClient.isConnected) {
                    ftpClient.logout()
                    ftpClient.disconnect()
                }
            } catch (ex: IOException) {
                ex.printStackTrace()
            }
        }
    }
}



通过这些代码片段,可以使用Jetpack Compose开发一个能够通过手机后置主摄像头录制视频、检测画面差异、实时预览并将视频上传至FTP服务器的Android应用。




相关文章
|
4月前
|
安全 Java Android开发
探索安卓开发的未来:Kotlin语言的崛起与挑战
在这篇文章中,我们将深入探讨Kotlin语言在安卓开发领域的应用及其对传统Java开发的颠覆性影响。通过分析Kotlin的特性、社区支持以及在实际项目中的应用案例,我们揭示了这一现代编程语言如何为开发者提供更简洁、更安全的编程体验,并讨论了它在面对性能优化和向后兼容性时所面临的挑战。文章旨在为读者呈现一个全面的视角,评估Kotlin作为未来安卓开发主流语言的可能性。
78 1
|
2月前
|
程序员 开发工具 Android开发
Android|使用阿里云推流 SDK 实现双路推流不同画面
本文记录了一种使用没有原生支持多路推流的阿里云推流 Android SDK,实现同时推送两路不同画面的流的方法。
69 7
|
2月前
|
存储 前端开发 测试技术
Kotlin教程笔记-使用Kotlin + JetPack 对旧项目进行MVVM改造
Kotlin教程笔记-使用Kotlin + JetPack 对旧项目进行MVVM改造
|
3月前
|
监控 安全 Java
Kotlin 在公司上网监控中的安卓开发应用
在数字化办公环境中,公司对员工上网行为的监控日益重要。Kotlin 作为一种基于 JVM 的编程语言,具备简洁、安全、高效的特性,已成为安卓开发的首选语言之一。通过网络请求拦截,Kotlin 可实现网址监控、访问时间记录等功能,满足公司上网监控需求。其简洁性有助于快速构建强大的监控应用,并便于后续维护与扩展。因此,Kotlin 在安卓上网监控应用开发中展现出广阔前景。
30 1
|
3月前
|
安全 Android开发 开发者
探索安卓开发的未来:Kotlin的崛起与Flutter的挑战
在移动开发的广阔天地中,安卓平台始终占据着举足轻重的地位。随着技术的不断进步和开发者需求的多样化,Kotlin和Flutter成为了改变游戏规则的新玩家。本文将深入探讨Kotlin如何以其现代化的特性赢得开发者的青睐,以及Flutter凭借跨平台的能力如何挑战传统的安卓开发模式。通过实际案例分析,我们将揭示这两种技术如何塑造未来的安卓应用开发。
86 6
|
3月前
|
安全 Java Android开发
探索安卓应用开发的新趋势:Kotlin和Jetpack Compose
在安卓应用开发领域,随着技术的不断进步,新的编程语言和框架层出不穷。Kotlin作为一种现代的编程语言,因其简洁性和高效性正逐渐取代Java成为安卓开发的首选语言。同时,Jetpack Compose作为一个新的UI工具包,提供了一种声明式的UI设计方法,使得界面编写更加直观和灵活。本文将深入探讨Kotlin和Jetpack Compose的特点、优势以及如何结合使用它们来构建现代化的安卓应用。
88 4
|
3月前
|
调度 Android开发 开发者
探索安卓开发中的新技术:Kotlin协程
【9月更文挑战第9天】本文将深入探讨Kotlin协程在安卓开发中的应用,揭示其如何优雅地处理异步任务。我们将从基础概念入手,逐步深入到实际开发场景,最后通过代码示例直观展示协程的魔力。无论你是初学者还是有经验的开发者,这篇文章都将为你打开一扇通往更高效、更简洁代码的大门。
|
4月前
|
移动开发 Java Android开发
探索安卓应用开发的新趋势:Kotlin与Coroutines
【8月更文挑战第31天】本文旨在为读者揭示安卓开发中的最新潮流,特别是Kotlin语言及其协程特性如何重塑移动应用的构建方式。我们将通过实际代码示例深入理解Kotlin简洁语法背后的强大功能,并探讨如何利用Coroutines优雅地处理异步任务,从而提升应用性能和用户体验。无论你是安卓开发的新手还是资深开发者,这篇文章都将为你带来新的启示和灵感。
|
5月前
|
Android开发 Kotlin
kotlin开发安卓app,如何让布局自适应系统传统导航和全面屏导航
使用`navigationBarsPadding()`修饰符实现界面自适应,自动处理底部导航栏的内边距,再加上`.padding(bottom = 10.dp)`设定内容与屏幕底部的距离,以完成全面的布局适配。示例代码采用Kotlin。
141 15
|
4月前
|
编解码 监控 API
惊艳登场!揭秘如何在Android平台上轻松玩转GB28181标准,实现多视频通道接入的超实用指南!
【8月更文挑战第14天】GB28181是公共安全视频监控联网的技术标准。本文介绍如何在Android平台上实现该标准下的多视频通道接入。首先准备开发环境,接着引入GB28181 SDK依赖并初始化SDK。实现设备注册与登录后,通过指定不同通道号请求多路视频流。最后,处理接收到的数据并显示给用户。此过程涉及视频解码,需确保应用稳定及良好的用户体验。
103 0