安卓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月前
|
编解码 安全 Android开发
如何修复 Android 和 Windows 不支持视频编解码器的问题?
视频播放时遇到“编解码器不支持”错误(如0xc00d36c4或0xc00d5212)是常见问题,即使文件格式为MP4或MKV。编解码器是编码和解码数据的工具,不同设备和版本支持不同的编解码器。解决方法包括:1) 安装所需编解码器,如K-Lite Codec Pack;2) 使用自带编解码器的第三方播放器,如VLC、KMPlayer等。这些方法能帮助你顺利播放视频。
|
3月前
|
Android开发 开发者 Kotlin
Android实战经验之Kotlin中快速实现MVI架构
MVI架构通过单向数据流和不可变状态,提供了一种清晰、可预测的状态管理方式。在Kotlin中实现MVI架构,不仅提高了代码的可维护性和可测试性,还能更好地应对复杂的UI交互和状态管理。通过本文的介绍,希望开发者能够掌握MVI架构的核心思想,并在实际项目中灵活应用。
92 8
|
4月前
|
编译器 Android开发 开发者
Android经典面试题之Kotlin中Lambda表达式和匿名函数的区别
Lambda表达式和匿名函数都是Kotlin中强大的特性,帮助开发者编写简洁而高效的代码。理解它们的区别和适用场景,有助于选择最合适的方式来解决问题。希望本文的详细讲解和示例能够帮助你在Kotlin开发中更好地运用这些特性。
70 9
|
8月前
|
Android开发 Kotlin
Android经典面试题之Kotlin的==和===有什么区别?
本文介绍了 Kotlin 中 `==` 和 `===` 操作符的区别:`==` 用于比较值是否相等,而 `===` 用于检查对象身份。对于基本类型,两者行为相似;对于对象引用,`==` 比较值相等性,`===` 检查引用是否指向同一实例。此外,还列举了其他常用比较操作符及其应用场景。
234 93
|
7月前
|
存储 前端开发 测试技术
Android kotlin MVVM 架构简单示例入门
Android kotlin MVVM 架构简单示例入门
98 1
|
7月前
|
调度 Android开发 开发者
构建高效Android应用:探究Kotlin多线程优化策略
【10月更文挑战第11天】本文探讨了如何在Kotlin中实现高效的多线程方案,特别是在Android应用开发中。通过介绍Kotlin协程的基础知识、异步数据加载的实际案例,以及合理使用不同调度器的方法,帮助开发者提升应用性能和用户体验。
124 4
|
7月前
|
JSON 调度 数据库
Android面试之5个Kotlin深度面试题:协程、密封类和高阶函数
本文首发于公众号“AntDream”,欢迎微信搜索“AntDream”或扫描文章底部二维码关注,和我一起每天进步一点点。文章详细解析了Kotlin中的协程、扩展函数、高阶函数、密封类及`inline`和`reified`关键字在Android开发中的应用,帮助读者更好地理解和使用这些特性。
136 1
|
8月前
|
监控 安全 Java
Kotlin 在公司上网监控中的安卓开发应用
在数字化办公环境中,公司对员工上网行为的监控日益重要。Kotlin 作为一种基于 JVM 的编程语言,具备简洁、安全、高效的特性,已成为安卓开发的首选语言之一。通过网络请求拦截,Kotlin 可实现网址监控、访问时间记录等功能,满足公司上网监控需求。其简洁性有助于快速构建强大的监控应用,并便于后续维护与扩展。因此,Kotlin 在安卓上网监控应用开发中展现出广阔前景。
59 1
|
8月前
|
安全 Java Android开发
探索安卓应用开发的新趋势:Kotlin和Jetpack Compose
在安卓应用开发领域,随着技术的不断进步,新的编程语言和框架层出不穷。Kotlin作为一种现代的编程语言,因其简洁性和高效性正逐渐取代Java成为安卓开发的首选语言。同时,Jetpack Compose作为一个新的UI工具包,提供了一种声明式的UI设计方法,使得界面编写更加直观和灵活。本文将深入探讨Kotlin和Jetpack Compose的特点、优势以及如何结合使用它们来构建现代化的安卓应用。
235 11
|
8月前
|
安全 Android开发 开发者
探索安卓开发的未来:Kotlin的崛起与Flutter的挑战
在移动开发的广阔天地中,安卓平台始终占据着举足轻重的地位。随着技术的不断进步和开发者需求的多样化,Kotlin和Flutter成为了改变游戏规则的新玩家。本文将深入探讨Kotlin如何以其现代化的特性赢得开发者的青睐,以及Flutter凭借跨平台的能力如何挑战传统的安卓开发模式。通过实际案例分析,我们将揭示这两种技术如何塑造未来的安卓应用开发。
131 6