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




相关文章
|
22小时前
|
安全 JavaScript 前端开发
kotlin开发安卓app,JetPack Compose框架,给webview新增一个按钮,点击刷新网页
在Kotlin中开发Android应用,使用Jetpack Compose框架时,可以通过添加一个按钮到TopAppBar来实现WebView页面的刷新功能。按钮位于右上角,点击后调用`webViewState?.reload()`来刷新网页内容。以下是代码摘要:
|
2天前
|
Android开发 Kotlin
Android面试题 之 Kotlin DataBinding 图片加载和绑定RecyclerView
本文介绍了如何在Android中使用DataBinding和BindingAdapter。示例展示了如何创建`MyBindingAdapter`,包含一个`setImage`方法来设置ImageView的图片。布局文件使用`&lt;data&gt;`标签定义变量,并通过`app:image`调用BindingAdapter。在Activity中设置变量值传递给Adapter处理。此外,还展示了如何在RecyclerView的Adapter中使用DataBinding,如`MyAdapter`,在子布局`item.xml`中绑定User对象到视图。关注公众号AntDream阅读更多内容。
9 1
|
2天前
|
JavaScript Java Android开发
kotlin安卓在Jetpack Compose 框架下跨组件通讯EventBus
**EventBus** 是一个Android事件总线库,简化组件间通信。要使用它,首先在Gradle中添加依赖`implementation &#39;org.greenrobot:eventbus:3.3.1&#39;`。然后,可选地定义事件类如`MessageEvent`。在活动或Fragment的`onCreate`中注册订阅者,在`onDestroy`中反注册。通过`@Subscribe`注解方法处理事件,如`onMessageEvent`。发送事件使用`EventBus.getDefault().post()`。
|
2天前
|
JavaScript 前端开发 Android开发
kotlin安卓在Jetpack Compose 框架下使用webview , 网页中的JavaScript代码如何与native交互
在Jetpack Compose中使用Kotlin创建Webview组件,设置JavaScript交互:`@Composable`函数`ComposableWebView`加载网页并启用JavaScript。通过`addJavascriptInterface`添加`WebAppInterface`类,允许JavaScript调用Android方法如播放音频。当页面加载完成时,执行`onWebViewReady`回调。
|
1月前
|
存储 设计模式 数据库
构建高效的安卓应用:探究Android Jetpack架构组件
【4月更文挑战第20天】 在移动开发的世界中,构建一个既高效又可维护的安卓应用是每个开发者追求的目标。随着Android Jetpack的推出,Google为开发者提供了一套高质量的库、工具和指南,以简化应用程序开发流程。本文将深入探讨Jetpack的核心组件之一——架构组件,并展示如何将其应用于实际项目中,以提升应用的响应性和稳定性。我们将通过分析这些组件的设计原则,以及它们如何协同工作,来揭示它们对于构建现代化安卓应用的重要性。
|
6天前
|
数据管理 API 数据库
探索Android Jetpack:现代安卓开发的利器
Android Jetpack是谷歌为简化和优化安卓应用开发而推出的一套高级组件库。本文深入探讨了Jetpack的主要构成及其在应用开发中的实际运用,展示了如何通过使用这些工具来提升开发效率和应用性能。
|
18天前
|
存储 安全 Android开发
构建高效的Android应用:Kotlin与Jetpack的结合
【5月更文挑战第31天】 在移动开发的世界中,Android 平台因其开放性和广泛的用户基础而备受开发者青睐。随着技术的进步和用户需求的不断升级,开发一个高效、流畅且易于维护的 Android 应用变得愈发重要。本文将探讨如何通过结合现代编程语言 Kotlin 和 Android Jetpack 组件来提升 Android 应用的性能和可维护性。我们将深入分析 Kotlin 语言的优势,探索 Jetpack 组件的核心功能,并通过实例演示如何在实际项目中应用这些技术。
|
19天前
|
物联网 区块链 Android开发
构建高效Android应用:Kotlin与Jetpack的实践之路未来技术的融合潮流:区块链、物联网与虚拟现实的交汇点
【5月更文挑战第30天】 在移动开发领域,效率和性能始终是开发者追求的核心。随着技术的不断进步,Kotlin语言以其简洁性和现代化特性成为Android开发的新宠。与此同时,Jetpack组件为应用开发提供了一套经过实践检验的库、工具和指南,旨在简化复杂任务并帮助提高应用质量。本文将深入探索如何通过Kotlin结合Jetpack组件来构建一个既高效又稳定的Android应用,并分享在此过程中的最佳实践和常见陷阱。
|
20天前
|
安全 Java Android开发
构建高效Android应用:Kotlin与Jetpack实践指南
【5月更文挑战第29天】 在移动开发的世界中,效率和性能始终是核心诉求。随着技术的演进,Kotlin语言以其简洁性和功能性成为Android开发的首选。结合Jetpack组件的推广,开发者得以构建更高效、可维护且易于测试的应用。本文将深入探讨利用Kotlin语言特性以及Jetpack架构组件来优化Android应用的策略和技巧,旨在帮助开发者提升应用质量并降低维护成本。
|
21天前
|
持续交付 Android开发 开发者
构建高性能微服务架构:后端开发的终极指南构建高效Android应用:Kotlin与Jetpack的完美结合
【5月更文挑战第28天】 在现代软件开发的浪潮中,微服务架构已经成为了设计灵活、可扩展且易于维护系统的重要模式。本文将深入探讨如何构建一个高性能的微服务架构,涵盖从基础概念理解到实践策略部署的全过程。我们将讨论关键的设计原则、技术选型、性能优化技巧以及安全性考虑,旨在为后端开发者提供一个全面的指南,帮助他们构建出能够适应快速变化的市场需求和技术挑战的系统。 【5月更文挑战第28天】 在移动开发的世界中,效率和性能是衡量一个应用成功与否的关键因素。本文将深入探讨如何通过结合Kotlin语言和Android Jetpack组件,来构建一个既高效又易维护的Android应用。我们将透过实际案例分析