如何在CameraX录制启动后添加0-30秒预览计时器?
实现CameraX预览界面的30秒录制计时器
需求说明
开发基于Android Java的视频录制应用,要求:
- 用户仅能录制一次30秒的视频
- 录制完成后将视频发送至指定服务器
- 点击开始录制按钮后,在CameraX预览界面显示从0到30秒的实时计时器
实现步骤及代码修改
1. 布局文件修改(activity_main.xml)
在PreviewView上方添加一个TextView作为计时器,确保它覆盖在预览界面上:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.camera.view.PreviewView android:id="@+id/previewView" android:layout_width="match_parent" android:layout_height="match_parent" /> <TextView android:id="@+id/tvTimer" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_marginTop="32dp" android:text="00:00" android:textColor="#FFFFFF" android:textSize="36sp" android:textStyle="bold" android:shadowColor="#000000" android:shadowRadius="4"/> <Button android:id="@+id/bRecord" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:layout_marginBottom="32dp" android:text="start recording"/> </RelativeLayout>
2. 代码逻辑修改(VideoKycActivity.java)
添加计时器变量、录制状态标记,整合倒计时、自动停止录制、上传逻辑:
import android.Manifest; import android.content.ContentValues; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.CountDownTimer; import android.provider.MediaStore; import android.view.View; import android.widget.Button; import android.widget.TextView; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import androidx.camera.core.CameraSelector; import androidx.camera.core.ImageAnalysis; import androidx.camera.core.ImageProxy; import androidx.camera.core.Preview; import androidx.camera.core.VideoCapture; import androidx.camera.lifecycle.ProcessCameraProvider; import androidx.core.content.ContextCompat; import com.google.common.util.concurrent.ListenableFuture; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; public class VideoKycActivity extends AppCompatActivity implements ImageAnalysis.Analyzer, View.OnClickListener { private ListenableFuture<ProcessCameraProvider> cameraProviderFuture; private PreviewView previewView; private VideoCapture videoCapture; private Button bRecord; private TextView tvTimer; private CountDownTimer countDownTimer; private boolean isRecording = false; private boolean hasRecorded = false; // 标记是否已经录制过一次 private String videoFilePath; // 存储录制后的视频路径 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); previewView = findViewById(R.id.previewView); bRecord = findViewById(R.id.bRecord); tvTimer = findViewById(R.id.tvTimer); bRecord.setText("start recording"); bRecord.setOnClickListener(this); cameraProviderFuture = ProcessCameraProvider.getInstance(this); cameraProviderFuture.addListener(() -> { try { ProcessCameraProvider cameraProvider = cameraProviderFuture.get(); startCameraX(cameraProvider); } catch (ExecutionException | InterruptedException e) { e.printStackTrace(); } }, getExecutor()); } Executor getExecutor() { return ContextCompat.getMainExecutor(this); } private void startCameraX(ProcessCameraProvider cameraProvider) { cameraProvider.unbindAll(); CameraSelector cameraSelector = new CameraSelector.Builder() .requireLensFacing(CameraSelector.LENS_FACING_BACK) .build(); Preview preview = new Preview.Builder().build(); preview.setSurfaceProvider(previewView.getSurfaceProvider()); videoCapture = new VideoCapture.Builder() .setVideoFrameRate(30) .build(); ImageAnalysis imageAnalysis = new ImageAnalysis.Builder() .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build(); imageAnalysis.setAnalyzer(getExecutor(), this); cameraProvider.bindToLifecycle(this, cameraSelector, preview, videoCapture); } @Override public void analyze(@NonNull ImageProxy image) { image.close(); } @Override public void onClick(View view) { if (view.getId() == R.id.bRecord) { if (hasRecorded) { Toast.makeText(this, "仅允许录制一次", Toast.LENGTH_SHORT).show(); return; } if (!isRecording) { bRecord.setText("stop recording"); isRecording = true; startRecordingWithTimer(); } else { stopRecordingManually(); } } } private void startRecordingWithTimer() { // 初始化倒计时器,总时长30秒,每秒更新一次 countDownTimer = new CountDownTimer(30000, 1000) { @Override public void onTick(long millisUntilFinished) { long elapsed = 30000 - millisUntilFinished; int seconds = (int) (elapsed / 1000); tvTimer.setText(String.format("%02d:%02d", 0, seconds)); } @Override public void onFinish() { tvTimer.setText("00:30"); stopRecordingAuto(); } }.start(); recordVideo(); } private void stopRecordingManually() { if (countDownTimer != null) { countDownTimer.cancel(); } videoCapture.stopRecording(); resetRecordingState(); } private void stopRecordingAuto() { videoCapture.stopRecording(); hasRecorded = true; // 标记已录制过 resetRecordingState(); bRecord.setText("已录制完成"); bRecord.setEnabled(false); // 录制完成后上传视频 uploadVideoToServer(); } private void resetRecordingState() { isRecording = false; bRecord.setText("start recording"); tvTimer.setText("00:00"); } private void recordVideo() { if (videoCapture == null) return; if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { Toast.makeText(this, "请授予录音权限", Toast.LENGTH_SHORT).show(); return; } long timestamp = System.currentTimeMillis(); ContentValues contentValues = new ContentValues(); contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, timestamp); contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4"); try { videoCapture.startRecording( new VideoCapture.OutputFileOptions.Builder( getContentResolver(), MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues ).build(), getExecutor(), new VideoCapture.OnVideoSavedCallback() { @Override public void onVideoSaved(@NonNull VideoCapture.OutputFileResults outputFileResults) { videoFilePath = outputFileResults.getSavedUri().getPath(); Toast.makeText(VideoKycActivity.this, "视频保存成功", Toast.LENGTH_SHORT).show(); } @Override public void onError(int videoCaptureError, @NonNull String message, @Nullable Throwable cause) { Toast.makeText(VideoKycActivity.this, "保存失败: " + message, Toast.LENGTH_SHORT).show(); resetRecordingState(); if (countDownTimer != null) countDownTimer.cancel(); } } ); } catch (Exception e) { e.printStackTrace(); } } private void uploadVideoToServer() { if (videoFilePath == null) return; new Thread(() -> { HttpURLConnection connection = null; InputStream inputStream = null; OutputStream outputStream = null; try { URL url = new URL("你的服务器上传接口地址"); connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("POST"); connection.setDoOutput(true); connection.setRequestProperty("Content-Type", "video/mp4"); File videoFile = new File(videoFilePath); inputStream = getContentResolver().openInputStream(MediaStore.Video.Media.EXTERNAL_CONTENT_URI); outputStream = connection.getOutputStream(); byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); } int responseCode = connection.getResponseCode(); runOnUiThread(() -> { if (responseCode == HttpURLConnection.HTTP_OK) { Toast.makeText(VideoKycActivity.this, "视频上传成功", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(VideoKycActivity.this, "上传失败,响应码: " + responseCode, Toast.LENGTH_SHORT).show(); } }); } catch (IOException e) { e.printStackTrace(); runOnUiThread(() -> Toast.makeText(VideoKycActivity.this, "上传出错: " + e.getMessage(), Toast.LENGTH_SHORT).show()); } finally { try { if (inputStream != null) inputStream.close(); if (outputStream != null) outputStream.close(); if (connection != null) connection.disconnect(); } catch (IOException e) { e.printStackTrace(); } } }).start(); } }
关键实现说明
- 计时器显示:通过
CountDownTimer实现30秒倒计时,每秒更新TextView的显示内容,格式化为00:SS。 - 录制控制:开始录制时启动计时器,计时结束自动停止录制,并设置
hasRecorded标记防止重复录制;用户也可以手动点击停止按钮中断录制。 - 视频上传:录制完成后开启子线程,通过
HttpURLConnection将视频文件上传到服务器(需替换为实际的服务器接口地址)。 - 权限处理:确保提前申请
RECORD_AUDIO和WRITE_EXTERNAL_STORAGE(Android 10及以上无需后者)权限。
内容的提问来源于stack exchange,提问作者Anshul
相关产品推荐
相关产品推荐

