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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 04:55:23