安卓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应用。




相关文章
|
27天前
|
安全 Java Android开发
安卓开发中的新趋势:Kotlin与Jetpack的完美结合
【6月更文挑战第20天】在不断进化的移动应用开发领域,Android平台以其开放性和灵活性赢得了全球开发者的青睐。然而,随着技术的迭代,传统Java语言在Android开发中逐渐显露出局限性。Kotlin,一种现代的静态类型编程语言,以其简洁、安全和高效的特性成为了Android开发中的新宠。同时,Jetpack作为一套支持库、工具和指南,旨在帮助开发者更快地打造优秀的Android应用。本文将探讨Kotlin与Jetpack如何共同推动Android开发进入一个新的时代,以及这对开发者意味着什么。
|
4天前
|
Android开发 Kotlin
kotlin开发安卓app,如何让布局自适应系统传统导航和全面屏导航
使用`navigationBarsPadding()`修饰符实现界面自适应,自动处理底部导航栏的内边距,再加上`.padding(bottom = 10.dp)`设定内容与屏幕底部的距离,以完成全面的布局适配。示例代码采用Kotlin。
38 15
|
4天前
|
存储 API Android开发
kotlin开发安卓app,使用webivew 触发 onShowFileChooser, 但只能触发一次,第二次无法触发,是怎么回事。 如何解决
在Android WebView开发中,`onShowFileChooser`方法用于开启文件选择。当用户只能选择一次文件可能是因为未正确处理选择回调。解决此问题需确保:1) 实现`WebChromeClient`并覆写`onShowFileChooser`;2) 用户选择文件后调用`ValueCallback.onReceiveValue`传递URI;3) 传递结果后将`ValueCallback`设为`null`以允许再次选择。下面是一个Kotlin示例,展示如何处理文件选择和结果回调。别忘了在Android 6.0+动态请求存储权限,以及在Android 10+处理分区存储。
|
5天前
|
存储 移动开发 Android开发
使用kotlin Jetpack Compose框架开发安卓app, webview中h5如何访问手机存储上传文件
在Kotlin和Jetpack Compose中,集成WebView以支持HTML5页面访问手机存储及上传音频文件涉及关键步骤:1) 添加`READ_EXTERNAL_STORAGE`和`WRITE_EXTERNAL_STORAGE`权限,考虑Android 11的分区存储;2) 配置WebView允许JavaScript和文件访问,启用`javaScriptEnabled`、`allowFileAccess`等设置;3) HTML5页面使用`<input type="file">`让用户选择文件,利用File API;
|
10天前
|
安全 Java Android开发
探索安卓应用开发中的Kotlin语言优势
【7月更文挑战第8天】 在安卓开发的广阔天地中,Kotlin以其优雅的语法、现代化的特性和高效的性能成为了开发者的新宠。本文将深入探讨Kotlin在安卓应用开发中所展现的独特魅力,从语言特性到实际应用案例,揭示其如何简化代码编写、提升开发效率,并增强应用性能。通过对比分析,我们将一同见证Kotlin如何在众多编程语言中脱颖而出,成为安卓开发领域的一股清新之风。
196 11
|
10天前
|
Java Android开发 开发者
探索安卓应用开发的未来:Kotlin语言的崛起与挑战
【7月更文挑战第8天】随着移动设备在人们日常生活中扮演着越来越重要的角色,安卓应用开发的技术趋势和编程语言选择对开发者来说变得至关重要。本文将深入探讨Kotlin语言如何成为安卓开发的首选,分析其带来的优势及面临的挑战,并预测其未来的发展方向。
|
1月前
|
安全 JavaScript 前端开发
kotlin开发安卓app,JetPack Compose框架,给webview新增一个按钮,点击刷新网页
在Kotlin中开发Android应用,使用Jetpack Compose框架时,可以通过添加一个按钮到TopAppBar来实现WebView页面的刷新功能。按钮位于右上角,点击后调用`webViewState?.reload()`来刷新网页内容。以下是代码摘要:
|
13天前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的高校后勤网上报修系统安卓app附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的高校后勤网上报修系统安卓app附带文章源码部署视频讲解等
12 0
|
20天前
|
Android开发
Jetpack Compose: Hello Android
Jetpack Compose: Hello Android
12 0
|
3天前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
**Kotlin中的`by lazy`和`lateinit`都是延迟初始化技术。`by lazy`用于只读属性,线程安全,首次访问时初始化;`lateinit`用于可变属性,需手动初始化,非线程安全。`by lazy`支持线程安全模式选择,而`lateinit`适用于构造函数后初始化。选择依赖于属性特性和使用场景。**
14 5
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?

热门文章

最新文章