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

如何在Android实现视频录制并附带缩略图上传至服务器?

实现Android视频录制+缩略图生成+上传服务器完整方案

我来帮你梳理下实现步骤,结合你已有的代码,咱们一步步来完善整个流程:

一、先完善视频录制的核心逻辑

你现有的代码已经初始化了录制按钮,接下来需要补充权限处理、视频文件生成、相机启动和结果回调这几个关键部分:

1. 动态权限申请(Android 6.0+)

首先要确保获取必要的权限:CAMERA、WRITE_EXTERNAL_STORAGE(Android 13+可替换为READ_MEDIA_VIDEO)、INTERNET,代码可以放在按钮点击事件里触发:

private static final int REQUEST_PERMISSIONS = 100;
private String[] requiredPermissions = {
    Manifest.permission.CAMERA,
    Manifest.permission.WRITE_EXTERNAL_STORAGE,
    Manifest.permission.INTERNET
};

private boolean checkPermissions() {
    for (String perm : requiredPermissions) {
        if (ContextCompat.checkSelfPermission(this, perm) != PackageManager.PERMISSION_GRANTED) {
            return false;
        }
    }
    return true;
}

// 录制按钮点击事件
btnRecordVideo.setOnClickListener(v -> {
    if (checkPermissions()) {
        startRecordingVideo();
    } else {
        ActivityCompat.requestPermissions(this, requiredPermissions, REQUEST_PERMISSIONS);
    }
});

// 处理权限申请结果
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == REQUEST_PERMISSIONS) {
        boolean allGranted = true;
        for (int result : grantResults) {
            if (result != PackageManager.PERMISSION_GRANTED) {
                allGranted = false;
                break;
            }
        }
        if (allGranted) {
            startRecordingVideo();
        } else {
            Toast.makeText(this, "需要授予权限才能录制视频", Toast.LENGTH_SHORT).show();
        }
    }
}

2. 生成视频存储的Uri

补充方法来创建唯一的视频文件路径,避免文件名重复:

private Uri getOutputMediaFileUri() {
    File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(
            Environment.DIRECTORY_MOVIES), "MyVideoRecorder");
    
    // 创建存储目录
    if (!mediaStorageDir.exists()) {
        if (!mediaStorageDir.mkdirs()) {
            Log.d(TAG, "无法创建视频存储目录");
            return null;
        }
    }
    
    // 生成带时间戳的唯一文件名
    String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
    File mediaFile = new File(mediaStorageDir.getPath() + File.separator +
            "VID_" + timeStamp + ".mp4");
    
    return Uri.fromFile(mediaFile);
}

3. 启动相机录制视频

完善启动相机的方法,可设置视频质量和时长限制:

private void startRecordingVideo() {
    Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
    
    fileUri = getOutputMediaFileUri();
    if (fileUri == null) {
        Toast.makeText(this, "无法创建视频文件", Toast.LENGTH_SHORT).show();
        return;
    }
    
    // 可选配置:高清质量+最长30秒录制
    intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1);
    intent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, 30);
    intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
    
    startActivityForResult(intent, CAMERA_CAPTURE_VIDEO_REQUEST_CODE);
}

4. 处理录制完成的回调

在onActivityResult中接收录制结果,成功后触发缩略图生成和上传:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == CAMERA_CAPTURE_VIDEO_REQUEST_CODE) {
        if (resultCode == RESULT_OK) {
            Toast.makeText(this, "视频录制完成", Toast.LENGTH_SHORT).show();
            // 生成缩略图并上传
            generateThumbnailAndUpload();
        } else if (resultCode == RESULT_CANCELED) {
            Toast.makeText(this, "录制已取消", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "录制失败", Toast.LENGTH_SHORT).show();
        }
    }
}

二、生成视频缩略图

使用MediaMetadataRetriever获取视频第一帧作为缩略图,还可以压缩尺寸减小上传体积:

private Bitmap generateVideoThumbnail(String videoPath) {
    MediaMetadataRetriever retriever = new MediaMetadataRetriever();
    Bitmap thumbnail = null;
    try {
        retriever.setDataSource(videoPath);
        // 获取最接近起始点的帧
        thumbnail = retriever.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
        // 压缩缩略图到指定尺寸(320x240)
        thumbnail = Bitmap.createScaledBitmap(thumbnail, 320, 240, true);
    } catch (Exception e) {
        Log.e(TAG, "生成缩略图失败: " + e.getMessage());
    } finally {
        try {
            retriever.release();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return thumbnail;
}

// 将Bitmap保存为本地文件,方便后续上传
private File saveThumbnailToFile(Bitmap thumbnail) {
    File thumbnailDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "VideoThumbnails");
    if (!thumbnailDir.exists()) thumbnailDir.mkdirs();
    
    String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
    File thumbnailFile = new File(thumbnailDir, "THUMB_" + timeStamp + ".jpg");
    
    try (FileOutputStream fos = new FileOutputStream(thumbnailFile)) {
        // 80%质量压缩JPG
        thumbnail.compress(Bitmap.CompressFormat.JPEG, 80, fos);
        fos.flush();
    } catch (IOException e) {
        Log.e(TAG, "保存缩略图失败: " + e.getMessage());
        return null;
    }
    return thumbnailFile;
}

三、上传视频和缩略图到服务器

推荐用OkHttp实现多部分表单上传,先在build.gradle添加依赖:

implementation 'com.squareup.okhttp3:okhttp:4.10.0'

然后编写上传方法,支持同时传视频和缩略图:

private void uploadFiles(File videoFile, File thumbnailFile) {
    if (videoFile == null || !videoFile.exists() || thumbnailFile == null || !thumbnailFile.exists()) {
        Toast.makeText(this, "文件不存在,无法上传", Toast.LENGTH_SHORT).show();
        return;
    }
    
    OkHttpClient client = new OkHttpClient();
    
    // 构建多部分请求体
    RequestBody requestBody = new MultipartBody.Builder()
            .setType(MultipartBody.FORM)
            .addFormDataPart("video", videoFile.getName(),
                    RequestBody.create(MediaType.parse("video/mp4"), videoFile))
            .addFormDataPart("thumbnail", thumbnailFile.getName(),
                    RequestBody.create(MediaType.parse("image/jpeg"), thumbnailFile))
            // 可选:添加其他参数,比如用户ID
            // .addFormDataPart("user_id", "12345")
            .build();
    
    Request request = new Request.Builder()
            .url("你的服务器上传接口地址")
            .post(requestBody)
            .build();
    
    // 异步上传,避免阻塞主线程
    client.newCall(request).enqueue(new Callback() {
        @Override
        public void onFailure(@NonNull Call call, @NonNull IOException e) {
            runOnUiThread(() -> Toast.makeText(MainActivity.this, "上传失败: " + e.getMessage(), Toast.LENGTH_SHORT).show());
            Log.e(TAG, "上传失败: " + e.getMessage());
        }

        @Override
        public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
            if (response.isSuccessful()) {
                String responseStr = response.body().string();
                runOnUiThread(() -> Toast.makeText(MainActivity.this, "上传成功", Toast.LENGTH_SHORT).show());
                Log.d(TAG, "上传成功,服务器响应: " + responseStr);
            } else {
                runOnUiThread(() -> Toast.makeText(MainActivity.this, "上传失败,服务器返回错误", Toast.LENGTH_SHORT).show());
                Log.e(TAG, "上传失败,响应码: " + response.code());
            }
        }
    });
}

最后整合生成缩略图和上传的逻辑:

private void generateThumbnailAndUpload() {
    String videoPath = fileUri.getPath();
    if (videoPath == null) {
        Toast.makeText(this, "视频路径无效", Toast.LENGTH_SHORT).show();
        return;
    }
    
    Bitmap thumbnail = generateVideoThumbnail(videoPath);
    if (thumbnail == null) {
        Toast.makeText(this, "生成缩略图失败", Toast.LENGTH_SHORT).show();
        return;
    }
    
    File thumbnailFile = saveThumbnailToFile(thumbnail);
    if (thumbnailFile == null) {
        Toast.makeText(this, "保存缩略图失败", Toast.LENGTH_SHORT).show();
        return;
    }
    
    File videoFile = new File(videoPath);
    uploadFiles(videoFile, thumbnailFile);
}

四、额外注意事项

  • Android 10+存储适配:如果targetSdkVersion >=29,建议使用MediaStore API保存文件,避免直接操作外部存储的权限问题。
  • 大文件后台上传:如果视频体积较大,推荐用WorkManager实现后台上传,防止页面销毁导致上传中断。
  • 服务器端配合:确保你的服务器接口支持接收multipart/form-data格式的请求,能正确解析两个文件参数。

内容的提问来源于stack exchange,提问作者JackNc

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 03:35:16