mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-04-14 08:16:01 -04:00
In particular, this returns all the extra configuration data that will be necessary to actually instantiate streams from the database rather than the soon-to-be-removed configuration file.
805 lines
28 KiB
C++
805 lines
28 KiB
C++
// This file is part of Moonfire NVR, a security camera network video recorder.
|
|
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// In addition, as a special exception, the copyright holders give
|
|
// permission to link the code of portions of this program with the
|
|
// OpenSSL library under certain conditions as described in each
|
|
// individual source file, and distribute linked combinations including
|
|
// the two.
|
|
//
|
|
// You must obey the GNU General Public License in all respects for all
|
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
|
// exception, you may extend this exception to your version of the
|
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
|
// so, delete this exception statement from your version. If you delete
|
|
// this exception statement from all source files in the program, then
|
|
// also delete it here.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
//
|
|
// moonfire-db.cc: implementation of moonfire-db.h interface.
|
|
// see top-level comments there on performance & efficiency.
|
|
|
|
#include "moonfire-db.h"
|
|
|
|
#include <string>
|
|
|
|
#include <glog/logging.h>
|
|
|
|
#include "http.h"
|
|
#include "mp4.h"
|
|
#include "recording.h"
|
|
|
|
namespace moonfire_nvr {
|
|
|
|
bool MoonfireDatabase::Init(Database *db, std::string *error_message) {
|
|
CHECK(db_ == nullptr);
|
|
db_ = db;
|
|
|
|
{
|
|
DatabaseContext ctx(db_);
|
|
|
|
// This query scans the entirety of the recording table's index.
|
|
// It is quite slow, so the results are cached.
|
|
auto list_cameras_run = ctx.UseOnce(
|
|
R"(
|
|
select
|
|
camera.id,
|
|
camera.uuid,
|
|
camera.short_name,
|
|
camera.description,
|
|
camera.host,
|
|
camera.username,
|
|
camera.password,
|
|
camera.main_rtsp_path,
|
|
camera.sub_rtsp_path,
|
|
camera.retain_bytes,
|
|
min(recording.start_time_90k),
|
|
max(recording.start_time_90k + recording.duration_90k),
|
|
sum(recording.duration_90k),
|
|
sum(recording.sample_file_bytes)
|
|
from
|
|
camera
|
|
left join recording on (camera.id = recording.camera_id)
|
|
group by
|
|
camera.id,
|
|
camera.uuid,
|
|
camera.short_name,
|
|
camera.description,
|
|
camera.retain_bytes;
|
|
)");
|
|
while (list_cameras_run.Step() == SQLITE_ROW) {
|
|
CameraData data;
|
|
data.id = list_cameras_run.ColumnInt64(0);
|
|
Uuid uuid;
|
|
if (!uuid.ParseBinary(list_cameras_run.ColumnBlob(1))) {
|
|
*error_message =
|
|
StrCat("bad uuid ", ToHex(list_cameras_run.ColumnBlob(1)),
|
|
" for camera id ", data.id);
|
|
return false;
|
|
}
|
|
data.short_name = list_cameras_run.ColumnText(2).as_string();
|
|
data.description = list_cameras_run.ColumnText(3).as_string();
|
|
data.host = list_cameras_run.ColumnText(4).as_string();
|
|
data.username = list_cameras_run.ColumnText(5).as_string();
|
|
data.password = list_cameras_run.ColumnText(6).as_string();
|
|
data.main_rtsp_path = list_cameras_run.ColumnText(7).as_string();
|
|
data.sub_rtsp_path = list_cameras_run.ColumnText(8).as_string();
|
|
data.retain_bytes = list_cameras_run.ColumnInt64(9);
|
|
data.min_start_time_90k = list_cameras_run.ColumnType(10) == SQLITE_NULL
|
|
? -1
|
|
: list_cameras_run.ColumnInt64(10);
|
|
data.max_end_time_90k = list_cameras_run.ColumnType(11) == SQLITE_NULL
|
|
? -1
|
|
: list_cameras_run.ColumnInt64(11);
|
|
data.total_duration_90k = list_cameras_run.ColumnInt64(12);
|
|
data.total_sample_file_bytes = list_cameras_run.ColumnInt64(13);
|
|
|
|
auto ret = cameras_by_uuid_.insert(std::make_pair(uuid, data));
|
|
if (!ret.second) {
|
|
*error_message = StrCat("Duplicate camera uuid ", uuid.UnparseText());
|
|
return false;
|
|
}
|
|
CameraData *data_p = &ret.first->second;
|
|
if (!cameras_by_id_.insert(std::make_pair(data.id, data_p)).second) {
|
|
*error_message = StrCat("Duplicate camera id ", data.id);
|
|
return false;
|
|
}
|
|
}
|
|
if (list_cameras_run.status() != SQLITE_DONE) {
|
|
*error_message = StrCat("Camera list query failed: ",
|
|
list_cameras_run.error_message());
|
|
}
|
|
|
|
// It's simplest to just keep the video sample entries in RAM.
|
|
auto video_sample_entries_run = ctx.UseOnce(
|
|
R"(
|
|
select
|
|
id,
|
|
sha1,
|
|
width,
|
|
height,
|
|
data
|
|
from
|
|
video_sample_entry
|
|
)");
|
|
while (video_sample_entries_run.Step() == SQLITE_ROW) {
|
|
VideoSampleEntry entry;
|
|
entry.id = video_sample_entries_run.ColumnInt64(0);
|
|
entry.sha1 = video_sample_entries_run.ColumnBlob(1).as_string();
|
|
int64_t width_tmp = video_sample_entries_run.ColumnInt64(2);
|
|
int64_t height_tmp = video_sample_entries_run.ColumnInt64(3);
|
|
auto max = std::numeric_limits<uint16_t>::max();
|
|
if (width_tmp <= 0 || width_tmp > max || height_tmp <= 0 ||
|
|
height_tmp > max) {
|
|
*error_message =
|
|
StrCat("video_sample_entry id ", entry.id, " width ", width_tmp,
|
|
" / height ", height_tmp, " out of range.");
|
|
return false;
|
|
}
|
|
entry.width = width_tmp;
|
|
entry.height = height_tmp;
|
|
entry.data = video_sample_entries_run.ColumnBlob(4).as_string();
|
|
CHECK(
|
|
video_sample_entries_.insert(std::make_pair(entry.id, entry)).second)
|
|
<< "duplicate: " << entry.id;
|
|
}
|
|
}
|
|
|
|
std::string list_camera_recordings_sql = StrCat(
|
|
R"(
|
|
select
|
|
recording.start_time_90k,
|
|
recording.duration_90k,
|
|
recording.video_samples,
|
|
recording.sample_file_bytes,
|
|
recording.video_sample_entry_id
|
|
from
|
|
recording
|
|
where
|
|
camera_id = :camera_id and
|
|
recording.start_time_90k > :start_time_90k - )",
|
|
kMaxRecordingDuration, " and\n",
|
|
R"(
|
|
recording.start_time_90k < :end_time_90k and
|
|
recording.start_time_90k + recording.duration_90k > :start_time_90k
|
|
order by
|
|
recording.start_time_90k desc;)");
|
|
list_camera_recordings_stmt_ =
|
|
db_->Prepare(list_camera_recordings_sql, nullptr, error_message);
|
|
if (!list_camera_recordings_stmt_.valid()) {
|
|
return false;
|
|
}
|
|
|
|
std::string build_mp4_sql = StrCat(
|
|
R"(
|
|
select
|
|
recording.id,
|
|
recording.start_time_90k,
|
|
recording.duration_90k,
|
|
recording.sample_file_bytes,
|
|
recording.sample_file_uuid,
|
|
recording.sample_file_sha1,
|
|
recording.video_index,
|
|
recording.video_samples,
|
|
recording.video_sync_samples,
|
|
recording.video_sample_entry_id
|
|
from
|
|
recording
|
|
where
|
|
camera_id = :camera_id and
|
|
recording.start_time_90k > :start_time_90k - )",
|
|
kMaxRecordingDuration, " and\n",
|
|
R"(
|
|
recording.start_time_90k < :end_time_90k and
|
|
recording.start_time_90k + recording.duration_90k > :start_time_90k
|
|
order by
|
|
recording.start_time_90k;)");
|
|
build_mp4_stmt_ = db_->Prepare(build_mp4_sql, nullptr, error_message);
|
|
if (!build_mp4_stmt_.valid()) {
|
|
return false;
|
|
}
|
|
|
|
insert_reservation_stmt_ = db_->Prepare(
|
|
"insert into reserved_sample_files (uuid, state)\n"
|
|
" values (:uuid, :state);",
|
|
nullptr, error_message);
|
|
if (!insert_reservation_stmt_.valid()) {
|
|
return false;
|
|
}
|
|
|
|
delete_reservation_stmt_ =
|
|
db_->Prepare("delete from reserved_sample_files where uuid = :uuid;",
|
|
nullptr, error_message);
|
|
if (!delete_reservation_stmt_.valid()) {
|
|
return false;
|
|
}
|
|
|
|
insert_video_sample_entry_stmt_ = db_->Prepare(
|
|
R"(
|
|
insert into video_sample_entry (sha1, width, height, data)
|
|
values (:sha1, :width, :height, :data);
|
|
)",
|
|
nullptr, error_message);
|
|
if (!insert_video_sample_entry_stmt_.valid()) {
|
|
return false;
|
|
}
|
|
|
|
insert_recording_stmt_ = db_->Prepare(
|
|
R"(
|
|
insert into recording (camera_id, sample_file_bytes, start_time_90k,
|
|
duration_90k, video_samples, video_sync_samples,
|
|
video_sample_entry_id, sample_file_uuid,
|
|
sample_file_sha1, video_index)
|
|
values (:camera_id, :sample_file_bytes, :start_time_90k,
|
|
:duration_90k, :video_samples, :video_sync_samples,
|
|
:video_sample_entry_id, :sample_file_uuid,
|
|
:sample_file_sha1, :video_index);
|
|
)",
|
|
nullptr, error_message);
|
|
if (!insert_recording_stmt_.valid()) {
|
|
return false;
|
|
}
|
|
|
|
list_oldest_sample_files_stmt_ = db_->Prepare(
|
|
R"(
|
|
select
|
|
id,
|
|
sample_file_uuid,
|
|
duration_90k,
|
|
sample_file_bytes
|
|
from
|
|
recording
|
|
where
|
|
camera_id = :camera_id
|
|
order by
|
|
start_time_90k
|
|
)",
|
|
nullptr, error_message);
|
|
if (!list_oldest_sample_files_stmt_.valid()) {
|
|
return false;
|
|
}
|
|
|
|
delete_recording_stmt_ =
|
|
db_->Prepare("delete from recording where id = :recording_id;", nullptr,
|
|
error_message);
|
|
if (!delete_recording_stmt_.valid()) {
|
|
return false;
|
|
}
|
|
|
|
camera_min_start_stmt_ = db_->Prepare(
|
|
R"(
|
|
select
|
|
start_time_90k
|
|
from
|
|
recording
|
|
where
|
|
camera_id = :camera_id
|
|
order by start_time_90k limit 1;
|
|
)",
|
|
nullptr, error_message);
|
|
if (!camera_min_start_stmt_.valid()) {
|
|
return false;
|
|
}
|
|
|
|
camera_max_start_stmt_ = db_->Prepare(
|
|
R"(
|
|
select
|
|
start_time_90k,
|
|
duration_90k
|
|
from
|
|
recording
|
|
where
|
|
camera_id = :camera_id
|
|
order by start_time_90k desc;
|
|
)",
|
|
nullptr, error_message);
|
|
if (!camera_max_start_stmt_.valid()) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void MoonfireDatabase::ListCameras(
|
|
std::function<IterationControl(const ListCamerasRow &)> cb) {
|
|
DatabaseContext ctx(db_);
|
|
ListCamerasRow row;
|
|
for (const auto &entry : cameras_by_uuid_) {
|
|
row.id = entry.second.id;
|
|
row.uuid = entry.first;
|
|
row.short_name = entry.second.short_name;
|
|
row.description = entry.second.description;
|
|
row.host = entry.second.host;
|
|
row.username = entry.second.username;
|
|
row.password = entry.second.password;
|
|
row.main_rtsp_path = entry.second.main_rtsp_path;
|
|
row.sub_rtsp_path = entry.second.sub_rtsp_path;
|
|
row.retain_bytes = entry.second.retain_bytes;
|
|
row.min_start_time_90k = entry.second.min_start_time_90k;
|
|
row.max_end_time_90k = entry.second.max_end_time_90k;
|
|
row.total_duration_90k = entry.second.total_duration_90k;
|
|
row.total_sample_file_bytes = entry.second.total_sample_file_bytes;
|
|
if (cb(row) == IterationControl::kBreak) {
|
|
return;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
bool MoonfireDatabase::GetCamera(Uuid camera_uuid, GetCameraRow *row) {
|
|
DatabaseContext ctx(db_);
|
|
const auto it = cameras_by_uuid_.find(camera_uuid);
|
|
if (it == cameras_by_uuid_.end()) {
|
|
return false;
|
|
}
|
|
const CameraData &data = it->second;
|
|
row->short_name = data.short_name;
|
|
row->description = data.description;
|
|
row->retain_bytes = data.retain_bytes;
|
|
row->min_start_time_90k = data.min_start_time_90k;
|
|
row->max_end_time_90k = data.max_end_time_90k;
|
|
row->total_duration_90k = data.total_duration_90k;
|
|
row->total_sample_file_bytes = data.total_sample_file_bytes;
|
|
return true;
|
|
}
|
|
|
|
bool MoonfireDatabase::ListCameraRecordings(
|
|
Uuid camera_uuid, int64_t start_time_90k, int64_t end_time_90k,
|
|
std::function<IterationControl(const ListCameraRecordingsRow &)> cb,
|
|
std::string *error_message) {
|
|
DatabaseContext ctx(db_);
|
|
const auto camera_it = cameras_by_uuid_.find(camera_uuid);
|
|
if (camera_it == cameras_by_uuid_.end()) {
|
|
*error_message = StrCat("no such camera ", camera_uuid.UnparseText());
|
|
return false;
|
|
}
|
|
auto run = ctx.Borrow(&list_camera_recordings_stmt_);
|
|
run.BindInt64(":camera_id", camera_it->second.id);
|
|
run.BindInt64(":start_time_90k", start_time_90k);
|
|
run.BindInt64(":end_time_90k", end_time_90k);
|
|
ListCameraRecordingsRow row;
|
|
while (run.Step() == SQLITE_ROW) {
|
|
row.start_time_90k = run.ColumnInt64(0);
|
|
row.end_time_90k = row.start_time_90k + run.ColumnInt64(1);
|
|
row.video_samples = run.ColumnInt64(2);
|
|
row.sample_file_bytes = run.ColumnInt64(3);
|
|
int64_t video_sample_entry_id = run.ColumnInt64(4);
|
|
const auto it = video_sample_entries_.find(video_sample_entry_id);
|
|
if (it == video_sample_entries_.end()) {
|
|
*error_message =
|
|
StrCat("recording references invalid video sample entry ",
|
|
video_sample_entry_id);
|
|
return false;
|
|
}
|
|
const VideoSampleEntry &entry = it->second;
|
|
row.video_sample_entry_sha1 = entry.sha1;
|
|
row.width = entry.width;
|
|
row.height = entry.height;
|
|
if (cb(row) == IterationControl::kBreak) {
|
|
return true;
|
|
}
|
|
}
|
|
if (run.status() != SQLITE_DONE) {
|
|
*error_message = StrCat("sqlite query failed: ", run.error_message());
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool MoonfireDatabase::ListMp4Recordings(
|
|
Uuid camera_uuid, int64_t start_time_90k, int64_t end_time_90k,
|
|
std::function<IterationControl(Recording &, const VideoSampleEntry &)>
|
|
row_cb,
|
|
std::string *error_message) {
|
|
VLOG(1) << "...(1/4): Waiting for database lock";
|
|
DatabaseContext ctx(db_);
|
|
const auto it = cameras_by_uuid_.find(camera_uuid);
|
|
if (it == cameras_by_uuid_.end()) {
|
|
*error_message = StrCat("no such camera ", camera_uuid.UnparseText());
|
|
return false;
|
|
}
|
|
const CameraData &data = it->second;
|
|
VLOG(1) << "...(2/4): Querying database";
|
|
auto run = ctx.Borrow(&build_mp4_stmt_);
|
|
run.BindInt64(":camera_id", data.id);
|
|
run.BindInt64(":end_time_90k", end_time_90k);
|
|
run.BindInt64(":start_time_90k", start_time_90k);
|
|
Recording recording;
|
|
VideoSampleEntry sample_entry;
|
|
while (run.Step() == SQLITE_ROW) {
|
|
recording.id = run.ColumnInt64(0);
|
|
recording.camera_id = data.id;
|
|
recording.start_time_90k = run.ColumnInt64(1);
|
|
recording.end_time_90k = recording.start_time_90k + run.ColumnInt64(2);
|
|
recording.sample_file_bytes = run.ColumnInt64(3);
|
|
if (!recording.sample_file_uuid.ParseBinary(run.ColumnBlob(4))) {
|
|
*error_message =
|
|
StrCat("recording ", recording.id, " has unparseable uuid ",
|
|
ToHex(run.ColumnBlob(4)));
|
|
return false;
|
|
}
|
|
recording.sample_file_sha1 = run.ColumnBlob(5).as_string();
|
|
recording.video_index = run.ColumnBlob(6).as_string();
|
|
recording.video_samples = run.ColumnInt64(7);
|
|
recording.video_sync_samples = run.ColumnInt64(8);
|
|
recording.video_sample_entry_id = run.ColumnInt64(9);
|
|
|
|
auto it = video_sample_entries_.find(recording.video_sample_entry_id);
|
|
if (it == video_sample_entries_.end()) {
|
|
*error_message = StrCat("recording ", recording.id,
|
|
" references unknown video sample entry ",
|
|
recording.video_sample_entry_id);
|
|
return false;
|
|
}
|
|
const VideoSampleEntry &entry = it->second;
|
|
|
|
if (row_cb(recording, entry) == IterationControl::kBreak) {
|
|
return true;
|
|
}
|
|
}
|
|
if (run.status() != SQLITE_DONE && run.status() != SQLITE_ROW) {
|
|
*error_message = StrCat("sqlite query failed: ", run.error_message());
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool MoonfireDatabase::ListReservedSampleFiles(std::vector<Uuid> *reserved,
|
|
std::string *error_message) {
|
|
reserved->clear();
|
|
DatabaseContext ctx(db_);
|
|
auto run = ctx.UseOnce("select uuid from reserved_sample_files;");
|
|
while (run.Step() == SQLITE_ROW) {
|
|
Uuid uuid;
|
|
if (!uuid.ParseBinary(run.ColumnBlob(0))) {
|
|
*error_message = StrCat("unparseable uuid ", ToHex(run.ColumnBlob(0)));
|
|
return false;
|
|
}
|
|
reserved->push_back(uuid);
|
|
}
|
|
if (run.status() != SQLITE_DONE) {
|
|
*error_message = run.error_message();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
std::vector<Uuid> MoonfireDatabase::ReserveSampleFiles(
|
|
int n, std::string *error_message) {
|
|
if (n == 0) {
|
|
return std::vector<Uuid>();
|
|
}
|
|
auto *gen = GetRealUuidGenerator();
|
|
std::vector<Uuid> uuids;
|
|
uuids.reserve(n);
|
|
for (int i = 0; i < n; ++i) {
|
|
uuids.push_back(gen->Generate());
|
|
}
|
|
DatabaseContext ctx(db_);
|
|
if (!ctx.BeginTransaction(error_message)) {
|
|
return std::vector<Uuid>();
|
|
}
|
|
for (const auto &uuid : uuids) {
|
|
auto run = ctx.Borrow(&insert_reservation_stmt_);
|
|
run.BindBlob(":uuid", uuid.binary_view());
|
|
run.BindInt64(":state", static_cast<int64_t>(ReservationState::kWriting));
|
|
if (run.Step() != SQLITE_DONE) {
|
|
ctx.RollbackTransaction();
|
|
*error_message = run.error_message();
|
|
return std::vector<Uuid>();
|
|
}
|
|
}
|
|
if (!ctx.CommitTransaction(error_message)) {
|
|
return std::vector<Uuid>();
|
|
}
|
|
return uuids;
|
|
}
|
|
|
|
bool MoonfireDatabase::InsertVideoSampleEntry(VideoSampleEntry *entry,
|
|
std::string *error_message) {
|
|
if (entry->id != -1) {
|
|
*error_message = StrCat("video_sample_entry already has id ", entry->id);
|
|
return false;
|
|
}
|
|
DatabaseContext ctx(db_);
|
|
for (const auto &some_entry : video_sample_entries_) {
|
|
if (some_entry.second.sha1 == entry->sha1) {
|
|
if (entry->width != some_entry.second.width ||
|
|
entry->height != some_entry.second.height) {
|
|
*error_message =
|
|
StrCat("inconsistent entry for sha1 ", ToHex(entry->sha1),
|
|
": existing entry has ", some_entry.second.width, "x",
|
|
some_entry.second.height, ", new entry has ", entry->width,
|
|
"x", entry->height);
|
|
return false;
|
|
}
|
|
entry->id = some_entry.first;
|
|
return true;
|
|
}
|
|
}
|
|
auto insert_run = ctx.Borrow(&insert_video_sample_entry_stmt_);
|
|
insert_run.BindBlob(":sha1", entry->sha1);
|
|
insert_run.BindInt64(":width", entry->width);
|
|
insert_run.BindInt64(":height", entry->height);
|
|
insert_run.BindBlob(":data", entry->data);
|
|
if (insert_run.Step() != SQLITE_DONE) {
|
|
*error_message = insert_run.error_message();
|
|
return false;
|
|
}
|
|
entry->id = ctx.last_insert_rowid();
|
|
CHECK(video_sample_entries_.insert(std::make_pair(entry->id, *entry)).second)
|
|
<< "duplicate: " << entry->id;
|
|
return true;
|
|
}
|
|
|
|
bool MoonfireDatabase::InsertRecording(Recording *recording,
|
|
std::string *error_message) {
|
|
if (recording->id != -1) {
|
|
*error_message = StrCat("recording already has id ", recording->id);
|
|
return false;
|
|
}
|
|
if (recording->end_time_90k <= recording->start_time_90k) {
|
|
*error_message =
|
|
StrCat("end time ", recording->end_time_90k,
|
|
" must be greater than start time ", recording->start_time_90k);
|
|
return false;
|
|
}
|
|
DatabaseContext ctx(db_);
|
|
auto it = cameras_by_id_.find(recording->camera_id);
|
|
if (it == cameras_by_id_.end()) {
|
|
*error_message = StrCat("no camera with id ", recording->camera_id);
|
|
return false;
|
|
}
|
|
CameraData *camera_data = it->second;
|
|
if (!ctx.BeginTransaction(error_message)) {
|
|
return false;
|
|
}
|
|
auto delete_run = ctx.Borrow(&delete_reservation_stmt_);
|
|
delete_run.BindBlob(":uuid", recording->sample_file_uuid.binary_view());
|
|
if (delete_run.Step() != SQLITE_DONE) {
|
|
*error_message = delete_run.error_message();
|
|
ctx.RollbackTransaction();
|
|
return false;
|
|
}
|
|
if (ctx.changes() != 1) {
|
|
*error_message = StrCat("uuid ", recording->sample_file_uuid.UnparseText(),
|
|
" is not reserved");
|
|
ctx.RollbackTransaction();
|
|
return false;
|
|
}
|
|
auto insert_run = ctx.Borrow(&insert_recording_stmt_);
|
|
insert_run.BindInt64(":camera_id", recording->camera_id);
|
|
insert_run.BindInt64(":sample_file_bytes", recording->sample_file_bytes);
|
|
insert_run.BindInt64(":start_time_90k", recording->start_time_90k);
|
|
insert_run.BindInt64(":duration_90k",
|
|
recording->end_time_90k - recording->start_time_90k);
|
|
insert_run.BindInt64(":video_samples", recording->video_samples);
|
|
insert_run.BindInt64(":video_sync_samples", recording->video_sync_samples);
|
|
insert_run.BindInt64(":video_sample_entry_id",
|
|
recording->video_sample_entry_id);
|
|
insert_run.BindBlob(":sample_file_uuid",
|
|
recording->sample_file_uuid.binary_view());
|
|
insert_run.BindBlob(":sample_file_sha1", recording->sample_file_sha1);
|
|
insert_run.BindBlob(":video_index", recording->video_index);
|
|
if (insert_run.Step() != SQLITE_DONE) {
|
|
LOG(ERROR) << "insert_run failed: " << insert_run.error_message();
|
|
*error_message = insert_run.error_message();
|
|
ctx.RollbackTransaction();
|
|
return false;
|
|
}
|
|
if (!ctx.CommitTransaction(error_message)) {
|
|
LOG(ERROR) << "commit failed";
|
|
return false;
|
|
}
|
|
recording->id = ctx.last_insert_rowid();
|
|
if (camera_data->min_start_time_90k == -1 ||
|
|
camera_data->min_start_time_90k > recording->start_time_90k) {
|
|
camera_data->min_start_time_90k = recording->start_time_90k;
|
|
}
|
|
if (camera_data->max_end_time_90k == -1 ||
|
|
camera_data->max_end_time_90k < recording->end_time_90k) {
|
|
camera_data->max_end_time_90k = recording->end_time_90k;
|
|
}
|
|
camera_data->total_duration_90k +=
|
|
recording->end_time_90k - recording->start_time_90k;
|
|
camera_data->total_sample_file_bytes += recording->sample_file_bytes;
|
|
return true;
|
|
}
|
|
|
|
bool MoonfireDatabase::ListOldestSampleFiles(
|
|
Uuid camera_uuid,
|
|
std::function<IterationControl(const ListOldestSampleFilesRow &)> row_cb,
|
|
std::string *error_message) {
|
|
DatabaseContext ctx(db_);
|
|
auto it = cameras_by_uuid_.find(camera_uuid);
|
|
if (it == cameras_by_uuid_.end()) {
|
|
*error_message = StrCat("no such camera ", camera_uuid.UnparseText());
|
|
return false;
|
|
}
|
|
const CameraData &camera_data = it->second;
|
|
auto run = ctx.Borrow(&list_oldest_sample_files_stmt_);
|
|
run.BindInt64(":camera_id", camera_data.id);
|
|
ListOldestSampleFilesRow row;
|
|
while (run.Step() == SQLITE_ROW) {
|
|
row.camera_id = camera_data.id;
|
|
row.recording_id = run.ColumnInt64(0);
|
|
if (!row.sample_file_uuid.ParseBinary(run.ColumnBlob(1))) {
|
|
*error_message =
|
|
StrCat("recording ", row.recording_id, " has unparseable uuid ",
|
|
ToHex(run.ColumnBlob(1)));
|
|
return false;
|
|
}
|
|
row.duration_90k = run.ColumnInt64(2);
|
|
row.sample_file_bytes = run.ColumnInt64(3);
|
|
if (row_cb(row) == IterationControl::kBreak) {
|
|
return true;
|
|
}
|
|
}
|
|
if (run.status() != SQLITE_DONE) {
|
|
*error_message = run.error_message();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool MoonfireDatabase::DeleteRecordings(
|
|
const std::vector<ListOldestSampleFilesRow> &recordings,
|
|
std::string *error_message) {
|
|
if (recordings.empty()) {
|
|
return true;
|
|
}
|
|
|
|
DatabaseContext ctx(db_);
|
|
if (!ctx.BeginTransaction(error_message)) {
|
|
return false;
|
|
}
|
|
struct State {
|
|
int64_t deleted_duration_90k = 0;
|
|
int64_t deleted_sample_file_bytes = 0;
|
|
int64_t min_start_time_90k = -1;
|
|
int64_t max_end_time_90k = -1;
|
|
CameraData *camera_data = nullptr;
|
|
};
|
|
std::map<int64_t, State> state_by_camera_id;
|
|
for (const auto &recording : recordings) {
|
|
State &state = state_by_camera_id[recording.camera_id];
|
|
state.deleted_duration_90k += recording.duration_90k;
|
|
state.deleted_sample_file_bytes += recording.sample_file_bytes;
|
|
|
|
auto delete_run = ctx.Borrow(&delete_recording_stmt_);
|
|
delete_run.BindInt64(":recording_id", recording.recording_id);
|
|
if (delete_run.Step() != SQLITE_DONE) {
|
|
ctx.RollbackTransaction();
|
|
*error_message = StrCat("delete: ", delete_run.error_message());
|
|
return false;
|
|
}
|
|
if (ctx.changes() != 1) {
|
|
ctx.RollbackTransaction();
|
|
*error_message = StrCat("no such recording ", recording.recording_id);
|
|
return false;
|
|
}
|
|
|
|
auto insert_run = ctx.Borrow(&insert_reservation_stmt_);
|
|
insert_run.BindBlob(":uuid", recording.sample_file_uuid.binary_view());
|
|
insert_run.BindInt64(":state",
|
|
static_cast<int64_t>(ReservationState::kDeleting));
|
|
if (insert_run.Step() != SQLITE_DONE) {
|
|
ctx.RollbackTransaction();
|
|
*error_message = StrCat("insert: ", insert_run.error_message());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Recompute start and end times for each camera.
|
|
for (auto &state_entry : state_by_camera_id) {
|
|
int64_t camera_id = state_entry.first;
|
|
State &state = state_entry.second;
|
|
auto it = cameras_by_id_.find(camera_id);
|
|
if (it == cameras_by_id_.end()) {
|
|
*error_message =
|
|
StrCat("internal error; can't find camera id ", camera_id);
|
|
return false;
|
|
}
|
|
state.camera_data = it->second;
|
|
|
|
// The minimum is straightforward, taking advantage of the start_time_90k
|
|
// index for speed.
|
|
auto min_run = ctx.Borrow(&camera_min_start_stmt_);
|
|
min_run.BindInt64(":camera_id", camera_id);
|
|
if (min_run.Step() == SQLITE_ROW) {
|
|
state.min_start_time_90k = min_run.ColumnInt64(0);
|
|
} else if (min_run.Step() == SQLITE_DONE) {
|
|
// There are no recordings left.
|
|
state.min_start_time_90k = -1;
|
|
state.max_end_time_90k = -1;
|
|
continue; // skip additional query below to calculate max.
|
|
} else {
|
|
ctx.RollbackTransaction();
|
|
*error_message = StrCat("min: ", min_run.error_message());
|
|
return false;
|
|
}
|
|
|
|
// The maximum is less straightforward in the case of overlap - all
|
|
// recordings starting in the last kMaxRecordingDuration must be examined
|
|
// to take advantage of the start_time_90k index.
|
|
auto max_run = ctx.Borrow(&camera_max_start_stmt_);
|
|
max_run.BindInt64(":camera_id", camera_id);
|
|
if (max_run.Step() != SQLITE_ROW) {
|
|
// If there was a min row, there should be a max row too, so this is an
|
|
// error even in the SQLITE_DONE case.
|
|
ctx.RollbackTransaction();
|
|
*error_message = StrCat("max[0]: ", max_run.error_message());
|
|
return false;
|
|
}
|
|
int64_t max_start_90k = max_run.ColumnInt64(0);
|
|
do {
|
|
auto end_time_90k = max_run.ColumnInt64(0) + max_run.ColumnInt64(1);
|
|
state.max_end_time_90k = std::max(state.max_end_time_90k, end_time_90k);
|
|
} while (max_run.Step() == SQLITE_ROW &&
|
|
max_run.ColumnInt64(0) > max_start_90k - kMaxRecordingDuration);
|
|
if (max_run.status() != SQLITE_DONE && max_run.status() != SQLITE_ROW) {
|
|
*error_message = StrCat("max[1]: ", max_run.error_message());
|
|
ctx.RollbackTransaction();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!ctx.CommitTransaction(error_message)) {
|
|
*error_message = StrCat("commit: ", *error_message);
|
|
return false;
|
|
}
|
|
|
|
for (auto &state_entry : state_by_camera_id) {
|
|
State &state = state_entry.second;
|
|
state.camera_data->total_duration_90k -= state.deleted_duration_90k;
|
|
state.camera_data->total_sample_file_bytes -=
|
|
state.deleted_sample_file_bytes;
|
|
state.camera_data->min_start_time_90k = state.min_start_time_90k;
|
|
state.camera_data->max_end_time_90k = state.max_end_time_90k;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool MoonfireDatabase::MarkSampleFilesDeleted(const std::vector<Uuid> &uuids,
|
|
std::string *error_message) {
|
|
if (uuids.empty()) {
|
|
return true;
|
|
}
|
|
DatabaseContext ctx(db_);
|
|
if (!ctx.BeginTransaction(error_message)) {
|
|
return false;
|
|
}
|
|
for (const auto &uuid : uuids) {
|
|
auto run = ctx.Borrow(&delete_reservation_stmt_);
|
|
run.BindBlob(":uuid", uuid.binary_view());
|
|
if (run.Step() != SQLITE_DONE) {
|
|
*error_message = run.error_message();
|
|
ctx.RollbackTransaction();
|
|
return false;
|
|
}
|
|
if (ctx.changes() != 1) {
|
|
*error_message = StrCat("no reservation for uuid ", uuid.UnparseText());
|
|
ctx.RollbackTransaction();
|
|
return false;
|
|
}
|
|
}
|
|
if (!ctx.CommitTransaction(error_message)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} // namespace moonfire_nvr
|