如何在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
相关产品推荐
相关产品推荐

