You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 08:30:50