如何将CameraX录制的30秒保密视频存储到SQLite数据库?
解决方案:将保密视频存储到SQLite而非系统图库
核心思路
- 放弃
MediaStore存储到系统图库的逻辑,改为录制到应用私有缓存目录的临时文件,避免视频暴露 - 将临时文件读取为字节数组,存入SQLite的
BLOB类型字段 - 发送至服务器后,立即删除SQLite中的记录和临时文件,确保数据仅使用一次
步骤1:创建SQLite数据库工具类
实现数据库的创建、表结构定义,以及视频数据的增删查操作:
public class VideoDbHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME = "SecureVideo.db"; private static final int DATABASE_VERSION = 1; // 表与字段定义 public static final String TABLE_VIDEOS = "videos"; public static final String COLUMN_ID = "_id"; public static final String COLUMN_VIDEO_DATA = "video_data"; public static final String COLUMN_TIMESTAMP = "timestamp"; // 创建表SQL语句 private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_VIDEOS + " (" + COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + COLUMN_VIDEO_DATA + " BLOB NOT NULL, " + COLUMN_TIMESTAMP + " INTEGER NOT NULL)"; public VideoDbHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE_TABLE); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 简单升级逻辑:删除旧表重建 db.execSQL("DROP TABLE IF EXISTS " + TABLE_VIDEOS); onCreate(db); } // 插入视频字节数组到数据库 public long insertVideo(byte[] videoData, long timestamp) { SQLiteDatabase db = getWritableDatabase(); ContentValues values = new ContentValues(); values.put(COLUMN_VIDEO_DATA, videoData); values.put(COLUMN_TIMESTAMP, timestamp); long videoId = db.insert(TABLE_VIDEOS, null, values); db.close(); return videoId; } // 根据ID查询视频字节数组 public byte[] getVideoById(long id) { SQLiteDatabase db = getReadableDatabase(); Cursor cursor = db.query(TABLE_VIDEOS, new String[]{COLUMN_VIDEO_DATA}, COLUMN_ID + "=?", new String[]{String.valueOf(id)}, null, null, null); byte[] videoData = null; if (cursor != null && cursor.moveToFirst()) { videoData = cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_VIDEO_DATA)); cursor.close(); } db.close(); return videoData; } // 根据ID删除视频记录 public void deleteVideoById(long id) { SQLiteDatabase db = getWritableDatabase(); db.delete(TABLE_VIDEOS, COLUMN_ID + "=?", new String[]{String.valueOf(id)}); db.close(); } }
步骤2:修改CameraX录制逻辑,输出到私有临时文件
替换原有的MediaStore存储逻辑,改为录制到应用缓存目录的临时文件:
@SuppressLint("RestrictedApi") private void recordVideo() { if (videoCapture != null) { long timestamp = System.currentTimeMillis(); // 创建应用私有缓存目录下的临时视频文件 File tempVideoFile = new File(getCacheDir(), "temp_video_" + timestamp + ".mp4"); try { if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, 100); return; } // 配置输出到临时文件 VideoCapture.OutputFileOptions outputOptions = new VideoCapture.OutputFileOptions.Builder(tempVideoFile).build(); videoCapture.startRecording( outputOptions, getExecutor(), new VideoCapture.OnVideoSavedCallback() { @Override public void onVideoSaved(@NonNull VideoCapture.OutputFileResults outputFileResults) { Toast.makeText(VideoKycActivity.this, "录制完成,保存到数据库中...", Toast.LENGTH_SHORT).show(); // 异步处理文件转存数据库 new SaveVideoToDbTask(tempVideoFile, timestamp).execute(); } @Override public void onError(int videoCaptureError, @NonNull String message, @Nullable Throwable cause) { Toast.makeText(VideoKycActivity.this, "录制失败: " + message, Toast.LENGTH_SHORT).show(); // 清理失败的临时文件 if (tempVideoFile.exists()) { tempVideoFile.delete(); } } } ); } catch (Exception e) { e.printStackTrace(); if (tempVideoFile.exists()) { tempVideoFile.delete(); } } } }
步骤3:异步将视频存入数据库
文件读取和数据库操作不能在主线程执行,用AsyncTask完成异步处理:
private class SaveVideoToDbTask extends AsyncTask<Void, Void, Long> { private final File videoFile; private final long timestamp; public SaveVideoToDbTask(File videoFile, long timestamp) { this.videoFile = videoFile; this.timestamp = timestamp; } @Override protected Long doInBackground(Void... voids) { // 将临时文件读取为字节数组 byte[] videoData = readFileToByteArray(videoFile); if (videoData == null) { return -1L; } // 存入数据库 VideoDbHelper dbHelper = new VideoDbHelper(VideoKycActivity.this); long videoId = dbHelper.insertVideo(videoData, timestamp); dbHelper.close(); // 删除临时文件,避免残留 if (videoFile.exists()) { videoFile.delete(); } return videoId; } @Override protected void onPostExecute(Long videoId) { super.onPostExecute(videoId); if (videoId != -1L) { Toast.makeText(VideoKycActivity.this, "视频已存入数据库,ID: " + videoId, Toast.LENGTH_SHORT).show(); // 这里调用发送到服务器的方法 // sendVideoToServer(videoId); } else { Toast.makeText(VideoKycActivity.this, "保存到数据库失败", Toast.LENGTH_SHORT).show(); } } // 文件转字节数组工具方法 private byte[] readFileToByteArray(File file) { try (FileInputStream fis = new FileInputStream(file); ByteArrayOutputStream bos = new ByteArrayOutputStream()) { byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = fis.read(buffer)) != -1) { bos.write(buffer, 0, bytesRead); } return bos.toByteArray(); } catch (IOException e) { e.printStackTrace(); return null; } } }
步骤4:发送视频到服务器并清理数据
实现服务器上传逻辑,成功后删除数据库记录:
private void sendVideoToServer(long videoId) { // 从数据库获取视频字节数组 VideoDbHelper dbHelper = new VideoDbHelper(this); byte[] videoData = dbHelper.getVideoById(videoId); dbHelper.close(); if (videoData == null) { Toast.makeText(this, "未找到目标视频", Toast.LENGTH_SHORT).show(); return; } // 这里实现上传服务器的逻辑,示例用OkHttp/Retrofit发送字节数组 // 伪代码示例: // OkHttpClient client = new OkHttpClient(); // RequestBody requestBody = RequestBody.create(videoData, MediaType.parse("video/mp4")); // Request request = new Request.Builder() // .url("你的服务器上传地址") // .post(requestBody) // .build(); // client.newCall(request).enqueue(new Callback() { // @Override // public void onFailure(Call call, IOException e) { // Toast.makeText(VideoKycActivity.this, "上传失败: " + e.getMessage(), Toast.LENGTH_SHORT).show(); // } // // @Override // public void onResponse(Call call, Response response) throws IOException { // if (response.isSuccessful()) { // Toast.makeText(VideoKycActivity.this, "上传成功", Toast.LENGTH_SHORT).show(); // // 上传成功后删除数据库记录 // VideoDbHelper deleteHelper = new VideoDbHelper(VideoKycActivity.this); // deleteHelper.deleteVideoById(videoId); // deleteHelper.close(); // } else { // Toast.makeText(VideoKycActivity.this, "上传失败", Toast.LENGTH_SHORT).show(); // } // } // }); }
注意事项
- 权限:仅需声明
RECORD_AUDIO权限,无需WRITE_EXTERNAL_STORAGE权限 - 内存优化:30秒MP4视频字节数组可能占用较大内存,务必在异步线程处理,避免OOM
- 数据安全:发送成功后必须删除数据库记录,确保数据仅使用一次;临时文件在存入数据库后立即删除
- 性能备选:若视频过大,SQLite存储Blob可能影响性能,可考虑加密后存储在应用私有目录,数据库仅存加密文件路径
内容的提问来源于stack exchange,提问作者Anshul
相关产品推荐
相关产品推荐

