Web interface to the new SQLite schema.

This is almost certain to have performance problems with large databases,
but it's a useful starting point.

No tests yet. It shouldn't be too hard to add some for moonfire-db.h, but
I'm impatient to fake up enough data to check on the performance and see
what needs to change there first.
This commit is contained in:
Scott Lamb 2016-01-16 22:54:16 -08:00
parent 055883d248
commit 40cd983355
10 changed files with 839 additions and 2 deletions

View File

@ -49,6 +49,7 @@ set(MOONFIRE_NVR_SRCS
filesystem.cc
h264.cc
http.cc
moonfire-db.cc
moonfire-nvr.cc
mp4.cc
profiler.cc
@ -56,7 +57,8 @@ set(MOONFIRE_NVR_SRCS
sqlite.cc
string.cc
time.cc
uuid.cc)
uuid.cc
web.cc)
add_library(moonfire-nvr-lib ${MOONFIRE_NVR_SRCS} ${PROTO_SRCS} ${PROTO_HDRS})
target_link_libraries(moonfire-nvr-lib ${MOONFIRE_DEPS})

View File

@ -62,7 +62,7 @@ class QueryParameters {
// Caller should check ok() afterward.
QueryParameters(const char *uri) {
TAILQ_INIT(&me_);
ok_ = evhttp_parse_query_str(uri, &me_) == 0;
ok_ = evhttp_parse_query(uri, &me_) == 0;
}
QueryParameters(const QueryParameters &) = delete;
void operator=(const QueryParameters &) = delete;

342
src/moonfire-db.cc Normal file
View File

@ -0,0 +1,342 @@
// 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.
#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(std::string *error_message) {
list_cameras_query_ = db_->Prepare(
R"(
select
camera.id,
camera.uuid,
camera.short_name,
camera.description,
camera.retain_bytes,
min(recording.start_time_90k),
max(recording.end_time_90k),
sum(recording.end_time_90k - recording.start_time_90k),
sum(recording.sample_file_bytes)
from
camera
left join recording on
(camera.id = recording.camera_id and
recording.status = 1)
group by
camera.id,
camera.uuid,
camera.short_name,
camera.description,
camera.retain_bytes;
)",
nullptr, error_message);
if (!list_cameras_query_.valid()) {
return false;
}
get_camera_query_ = db_->Prepare(
R"(
select
uuid,
short_name,
description,
retain_bytes
from
camera
where
id = :camera_id;)",
nullptr, error_message);
if (!get_camera_query_.valid()) {
return false;
}
list_camera_recordings_query_ = db_->Prepare(
R"(
select
recording.start_time_90k,
recording.end_time_90k,
recording.video_samples,
recording.sample_file_bytes,
recording.video_sample_entry_sha1,
video_sample_entry.width,
video_sample_entry.height
from
recording
join video_sample_entry on
(recording.video_sample_entry_sha1 = video_sample_entry.sha1)
where
recording.status = 1 and
camera_id = :camera_id
order by
recording.start_time_90k;)",
nullptr, error_message);
if (!list_camera_recordings_query_.valid()) {
return false;
}
build_mp4_query_ = db_->Prepare(
R"(
select
recording.rowid,
recording.start_time_90k,
recording.end_time_90k,
recording.sample_file_bytes,
recording.sample_file_uuid,
recording.sample_file_sha1,
recording.video_sample_entry_sha1,
recording.video_index,
recording.video_samples,
recording.video_sync_samples,
video_sample_entry.bytes,
video_sample_entry.width,
video_sample_entry.height
from
recording join video_sample_entry on
(recording.video_sample_entry_sha1 = video_sample_entry.sha1)
where
recording.status = 1 and
camera_id = :camera_id and
recording.start_time_90k < :end_time_90k and
recording.end_time_90k > :start_time_90k
order by
recording.start_time_90k;)",
nullptr, error_message);
if (!build_mp4_query_.valid()) {
return false;
}
return true;
}
bool MoonfireDatabase::ListCameras(
std::function<IterationControl(const ListCamerasRow &)> cb,
std::string *error_message) {
DatabaseContext ctx(db_);
auto run = ctx.Borrow(&list_cameras_query_);
ListCamerasRow row;
while (run.Step() == SQLITE_ROW) {
row.id = run.ColumnInt64(0);
if (!row.uuid.ParseBinary(run.ColumnBlob(1))) {
*error_message = StrCat("invalid uuid in row id ", row.id);
return false;
}
row.short_name = run.ColumnText(2).as_string();
row.description = run.ColumnText(3).as_string();
row.retain_bytes = run.ColumnInt64(4);
row.min_recording_start_time_90k = run.ColumnInt64(5);
row.max_recording_end_time_90k = run.ColumnInt64(6);
row.total_recording_duration_90k = run.ColumnInt64(7);
row.total_sample_file_bytes = run.ColumnInt64(8);
if (cb(row) == IterationControl::kBreak) {
break;
}
}
if (run.status() != SQLITE_DONE) {
*error_message = StrCat("sqlite query failed: ", run.error_message());
return false;
}
return true;
}
bool MoonfireDatabase::GetCamera(int64_t camera_id, GetCameraRow *row,
std::string *error_message) {
DatabaseContext ctx(db_);
auto run = ctx.Borrow(&get_camera_query_);
run.BindInt64(":camera_id", camera_id);
if (run.Step() == SQLITE_ROW) {
if (!row->uuid.ParseBinary(run.ColumnBlob(0))) {
*error_message =
StrCat("unable to parse uuid ", ToHex(run.ColumnBlob(0)));
return false;
}
row->short_name = run.ColumnText(1).as_string();
row->description = run.ColumnText(2).as_string();
row->retain_bytes = run.ColumnInt64(3);
} else if (run.status() == SQLITE_DONE) {
*error_message = "no such camera";
return false;
}
if (run.Step() == SQLITE_ROW) {
*error_message = "multiple rows returned unexpectedly";
return false;
}
return true;
}
bool MoonfireDatabase::ListCameraRecordings(
int64_t camera_id,
std::function<IterationControl(const ListCameraRecordingsRow &)> cb,
std::string *error_message) {
DatabaseContext ctx(db_);
auto run = ctx.Borrow(&list_camera_recordings_query_);
run.BindInt64(":camera_id", camera_id);
ListCameraRecordingsRow row;
while (run.Step() == SQLITE_ROW) {
row.start_time_90k = run.ColumnInt64(0);
row.end_time_90k = run.ColumnInt64(1);
row.video_samples = run.ColumnInt64(2);
row.sample_file_bytes = run.ColumnInt64(3);
auto video_sample_entry_sha1 = run.ColumnBlob(4);
row.video_sample_entry_sha1.assign(video_sample_entry_sha1.data(),
video_sample_entry_sha1.size());
row.width = run.ColumnInt64(5);
row.height = run.ColumnInt64(6);
if (cb(row) == IterationControl::kBreak) {
break;
}
}
if (run.status() != SQLITE_DONE) {
*error_message = StrCat("sqlite query failed: ", run.error_message());
return false;
}
return true;
}
std::shared_ptr<VirtualFile> MoonfireDatabase::BuildMp4(
int64_t camera_id, int64_t start_time_90k, int64_t end_time_90k,
std::string *error_message) {
LOG(INFO) << "Building mp4 for camera: " << camera_id
<< ", start_time_90k: " << start_time_90k
<< ", end_time_90k: " << end_time_90k;
Mp4FileBuilder builder;
int64_t next_row_start_time_90k = start_time_90k;
VideoSampleEntry sample_entry;
int64_t rows = 0;
{
VLOG(1) << "...(1/4): Waiting for database lock";
DatabaseContext ctx(db_);
VLOG(1) << "...(2/4): Querying database";
auto run = ctx.Borrow(&build_mp4_query_);
run.BindInt64(":camera_id", camera_id);
run.BindInt64(":end_time_90k", end_time_90k);
run.BindInt64(":start_time_90k", start_time_90k);
Recording recording;
while (run.Step() == SQLITE_ROW) {
recording.rowid = run.ColumnInt64(0);
VLOG(2) << "row: " << recording.rowid;
recording.start_time_90k = run.ColumnInt64(1);
recording.end_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.rowid, " has unparseable uuid ",
ToHex(run.ColumnBlob(4)));
return false;
}
recording.sample_file_path =
StrCat("/home/slamb/new-moonfire/sample/",
recording.sample_file_uuid.UnparseText());
recording.sample_file_sha1 = run.ColumnBlob(5).as_string();
recording.video_sample_entry_sha1 = run.ColumnBlob(6).as_string();
recording.video_index = run.ColumnBlob(7).as_string();
recording.video_samples = run.ColumnInt64(8);
recording.video_sync_samples = run.ColumnInt64(9);
if (rows == 0 && recording.start_time_90k != next_row_start_time_90k) {
*error_message =
StrCat("recording starts late: ",
PrettyTimestamp(recording.start_time_90k), " (",
recording.start_time_90k, ") rather than requested: ",
PrettyTimestamp(start_time_90k), " (", start_time_90k, ")");
return false;
} else if (recording.start_time_90k != next_row_start_time_90k) {
*error_message =
StrCat("gap/overlap in recording: ",
PrettyTimestamp(next_row_start_time_90k), " (",
next_row_start_time_90k, ") to: ",
PrettyTimestamp(recording.start_time_90k), " (",
recording.start_time_90k, ") before row ", rows);
return false;
}
next_row_start_time_90k = recording.end_time_90k;
if (rows > 0 && recording.video_sample_entry_sha1 != sample_entry.sha1) {
*error_message =
StrCat("inconsistent video sample entries: this recording has ",
ToHex(recording.video_sample_entry_sha1), ", previous had ",
ToHex(sample_entry.sha1));
return false;
} else if (rows == 0) {
sample_entry.sha1 = run.ColumnBlob(6).as_string();
sample_entry.data = run.ColumnBlob(10).as_string();
sample_entry.width = run.ColumnInt64(11);
sample_entry.height = run.ColumnInt64(12);
builder.SetSampleEntry(sample_entry);
}
// TODO: correct bounds within recording.
// Currently this can return too much data.
builder.Append(std::move(recording), 0,
std::numeric_limits<int32_t>::max());
++rows;
}
if (run.status() != SQLITE_DONE) {
*error_message = StrCat("sqlite query failed: ", run.error_message());
return false;
}
}
if (rows == 0) {
*error_message = StrCat("no recordings in range");
return false;
}
if (next_row_start_time_90k != end_time_90k) {
*error_message = StrCat("recording ends early: ",
PrettyTimestamp(next_row_start_time_90k), " (",
next_row_start_time_90k, "), not requested: ",
PrettyTimestamp(end_time_90k), " (", end_time_90k,
") after ", rows, " rows");
return false;
}
VLOG(1) << "...(3/4) building VirtualFile from " << rows << " recordings.";
auto file = builder.Build(error_message);
if (file == nullptr) {
return false;
}
VLOG(1) << "...(4/4) success, " << file->size() << " bytes, etag "
<< file->etag();
return file;
}
} // namespace moonfire_nvr

141
src/moonfire-db.h Normal file
View File

@ -0,0 +1,141 @@
// 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.h: database access logic for the Moonfire NVR SQLite schema.
// Currently focused on stuff needed by WebInterface to build a HTML or JSON
// interface.
//
// Performance note: camera-level operations do a sequential scan through
// essentially the entire database. This is unacceptable for full-sized
// databases; it will have to be measured and improved. Ideas:
//
// * separate the video index blob from the rest of the recording row,
// as it's expected to be 10X-100X larger than everything else and not
// necessary for these operations.
// * paged results + SQL indexes (but this may only help so much, as it'd be
// useful to at least see what days have recordings in one go).
// * keep aggregates, either in-memory or as denormalized data in the camera
// table. Likely integrating with the recording system, although triggers
// may also be possible.
#ifndef MOONFIRE_NVR_MOONFIRE_DB_H
#define MOONFIRE_NVR_MOONFIRE_DB_H
#include <functional>
#include <memory>
#include <string>
#include "common.h"
#include "http.h"
#include "mp4.h"
#include "sqlite.h"
#include "uuid.h"
namespace moonfire_nvr {
// For use with MoonfireDatabase::ListCameras.
struct ListCamerasRow {
int64_t id = -1;
Uuid uuid;
std::string short_name;
std::string description;
int64_t retain_bytes = -1;
// Aggregates summarizing completed (status=1) recordings.
int64_t min_recording_start_time_90k = -1;
int64_t max_recording_end_time_90k = -1;
int64_t total_recording_duration_90k = -1;
int64_t total_sample_file_bytes = -1;
};
// For use with MoonfireDatabase::GetCamera.
// This is the same information as in ListCamerasRow minus the stuff
// that's calculable from ListCameraRecordingsRow, which the camera details
// webpage also grabs.
struct GetCameraRow {
int64_t retain_bytes = -1;
Uuid uuid;
std::string short_name;
std::string description;
};
// For use with MoonfireDatabase::ListCameraRecordings.
struct ListCameraRecordingsRow {
// From the recording table.
int64_t start_time_90k = -1;
int64_t end_time_90k = -1;
int64_t video_samples = -1;
int64_t sample_file_bytes = -1;
std::string video_sample_entry_sha1;
// Joined from the video_sample_entry table.
int64_t width = -1;
int64_t height = -1;
};
class MoonfireDatabase {
public:
explicit MoonfireDatabase(Database *db) : db_(db) {}
MoonfireDatabase(const MoonfireDatabase &) = delete;
void operator=(const MoonfireDatabase &) = delete;
bool Init(std::string *error_message);
// List all cameras in the system, ordered by short name.
// Holds database lock; callback should be quick.
bool ListCameras(std::function<IterationControl(const ListCamerasRow &)> cb,
std::string *error_message);
bool GetCamera(int64_t camera_id, GetCameraRow *row,
std::string *error_message);
// List all recordings associated with a camera, ordered by start time..
// Holds database lock; callback should be quick.
bool ListCameraRecordings(
int64_t camera_id,
std::function<IterationControl(const ListCameraRecordingsRow &)>,
std::string *error_message);
std::shared_ptr<VirtualFile> BuildMp4(int64_t camera_id,
int64_t start_time_90k,
int64_t end_time_90k,
std::string *error_message);
private:
Database *const db_;
Statement list_cameras_query_;
Statement get_camera_query_;
Statement list_camera_recordings_query_;
Statement build_mp4_query_;
};
} // namespace moonfire_nvr
#endif // MOONFIRE_NVR_MOONFIRE_DB_H

View File

@ -211,4 +211,15 @@ bool SampleFileWriter::Close(std::string *sha1, std::string *error_message) {
return ok;
}
std::string PrettyTimestamp(int64_t ts_90k) {
struct tm mytm;
memset(&mytm, 0, sizeof(mytm));
time_t ts = ts_90k / kTimeUnitsPerSecond;
localtime_r(&ts, &mytm);
const size_t kTimeBufLen = 50;
char tmbuf[kTimeBufLen];
strftime(tmbuf, kTimeBufLen, "%a, %d %b %Y %H:%M:%S %Z", &mytm);
return tmbuf;
}
} // namespace moonfire_nvr

View File

@ -194,6 +194,8 @@ struct VideoSampleEntry {
uint16_t height = 0;
};
std::string PrettyTimestamp(int64_t ts_90k);
} // namespace moonfire_nvr
#endif // MOONFIRE_NVR_RECORDING_H

View File

@ -145,6 +145,41 @@ std::string HumanizeWithBinaryPrefix(float n, re2::StringPiece suffix) {
return Humanize(kPrefixes, 1024., n, suffix);
}
std::string HumanizeDuration(int64_t seconds) {
static const int64_t kMinuteInSeconds = 60;
static const int64_t kHourInSeconds = 60 * kMinuteInSeconds;
static const int64_t kDayInSeconds = 24 * kHourInSeconds;
int64_t days = seconds / kDayInSeconds;
seconds %= kDayInSeconds;
int64_t hours = seconds / kHourInSeconds;
seconds %= kHourInSeconds;
int64_t minutes = seconds / kMinuteInSeconds;
seconds %= kMinuteInSeconds;
std::string out;
if (days > 0) {
out.append(StrCat(days, days == 1 ? " day" : " days"));
}
if (hours > 0) {
if (!out.empty()) {
out.append(" ");
}
out.append(StrCat(hours, hours == 1 ? " hour" : " hours"));
}
if (minutes > 0) {
if (!out.empty()) {
out.append(" ");
}
out.append(StrCat(minutes, minutes == 1 ? " minute" : " minutes"));
}
if (seconds > 0 || out.empty()) {
if (!out.empty()) {
out.append(" ");
}
out.append(StrCat(seconds, seconds == 1 ? " second" : " seconds"));
}
return out;
}
bool strto64(const char *str, int base, const char **endptr, int64_t *value) {
static_assert(sizeof(int64_t) == sizeof(long long int),
"unknown memory model");

View File

@ -127,6 +127,8 @@ std::string ToHex(re2::StringPiece in, bool pad = false);
std::string HumanizeWithDecimalPrefix(float n, re2::StringPiece suffix);
std::string HumanizeWithBinaryPrefix(float n, re2::StringPiece suffix);
std::string HumanizeDuration(int64_t sec);
// Wrapper around ::strtoll that returns true iff valid and corrects
// constness. Returns false if |str| is null.
bool strto64(const char *str, int base, const char **endptr, int64_t *value);

224
src/web.cc Normal file
View File

@ -0,0 +1,224 @@
// 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/>.
//
// web.cc: implementation of web.h interface.
#include "web.h"
#include <glog/logging.h>
#include "recording.h"
#include "string.h"
namespace moonfire_nvr {
void WebInterface::Register(evhttp *http) {
evhttp_set_cb(http, "/", &WebInterface::HandleCameraList, this);
evhttp_set_cb(http, "/camera", &WebInterface::HandleCameraDetail, this);
evhttp_set_cb(http, "/view.mp4", &WebInterface::HandleMp4View, this);
}
void WebInterface::HandleCameraList(evhttp_request *req, void *arg) {
auto *this_ = reinterpret_cast<WebInterface *>(arg);
EvBuffer buf;
buf.Add(
"<!DOCTYPE html>\n"
"<html>\n"
"<head>\n"
"<title>Camera list</title>\n"
"<style type=\"text/css\">\n"
".header { background-color: #ddd; }\n"
"td { padding-right: 3em; }\n"
"</style>\n"
"</head>\n"
"<body>\n"
"<table>\n");
auto row_cb = [&](const ListCamerasRow &row) {
auto seconds =
(row.max_recording_end_time_90k - row.min_recording_start_time_90k) /
kTimeUnitsPerSecond;
buf.AddPrintf(
"<tr class=header><td colspan=2><a href=\"/camera?id=%" PRId64
"\">%s</a></td></tr>\n"
"<tr><td>description</td><td>%s</td></tr>\n"
"<tr><td>space</td><td>%s / %s (%.1f%%)</td></tr>\n"
"<tr><td>uuid</td><td>%s</td></tr>\n"
"<tr><td>oldest recording</td><td>%s</td></tr>\n"
"<tr><td>newest recording</td><td>%s</td></tr>\n"
"<tr><td>total duration</td><td>%s</td></tr>\n",
row.id, EscapeHtml(row.short_name).c_str(),
EscapeHtml(row.description).c_str(),
EscapeHtml(HumanizeWithBinaryPrefix(row.total_sample_file_bytes, "B"))
.c_str(),
EscapeHtml(HumanizeWithBinaryPrefix(row.retain_bytes, "B")).c_str(),
100.f * row.total_sample_file_bytes / row.retain_bytes,
EscapeHtml(row.uuid.UnparseText()).c_str(),
EscapeHtml(PrettyTimestamp(row.min_recording_start_time_90k)).c_str(),
EscapeHtml(PrettyTimestamp(row.max_recording_end_time_90k)).c_str(),
EscapeHtml(HumanizeDuration(seconds)).c_str());
return IterationControl::kContinue;
};
std::string error_message;
if (!this_->mdb_->ListCameras(row_cb, &error_message)) {
return evhttp_send_error(req, HTTP_INTERNAL,
EscapeHtml(error_message).c_str());
}
buf.Add(
"</table>\n"
"</body>\n"
"<html>\n");
evhttp_send_reply(req, HTTP_OK, "OK", buf.get());
}
void WebInterface::HandleCameraDetail(evhttp_request *req, void *arg) {
auto *this_ = reinterpret_cast<WebInterface *>(arg);
int64_t camera_id;
QueryParameters params(evhttp_request_get_uri(req));
if (!params.ok() || !Atoi64(params.Get("id"), 10, &camera_id)) {
return evhttp_send_error(req, HTTP_BADREQUEST, "bad query parameters");
}
GetCameraRow camera_row;
std::string error_message;
if (!this_->mdb_->GetCamera(camera_id, &camera_row, &error_message)) {
// TODO: more nuanced error here, such as HTTP_NOTFOUND where appropriate.
return evhttp_send_error(
req, HTTP_INTERNAL,
StrCat("sqlite query failed: ", EscapeHtml(error_message)).c_str());
}
EvBuffer buf;
buf.AddPrintf(
"<!DOCTYPE html>\n"
"<html>\n"
"<head>\n"
"<title>%s recordings</title>\n"
"<style type=\"text/css\">\n"
"tr:not(:first-child):hover { background-color: #ddd; }\n"
"th, td { padding: 0.5ex 1.5em; text-align: right; }\n"
"</style>\n"
"</head>\n"
"<body>\n"
"<h1>%s</h1>\n"
"<p>%s</p>\n"
"<table>\n"
"<tr><th>start</th><th>end</th><th>resolution</th>"
"<th>fps</th><th>size</th><th>bitrate</th>"
"</tr>\n",
EscapeHtml(camera_row.short_name).c_str(),
EscapeHtml(camera_row.short_name).c_str(),
EscapeHtml(camera_row.description).c_str());
// Rather than listing each 60-second recording, generate a HTML row for
// aggregated .mp4 files of up to kForceSplitDuration90k each, provided
// there is no gap or change in video parameters between recordings.
static const int64_t kForceSplitDuration90k =
4 * 60 * 60 * kTimeUnitsPerSecond;
ListCameraRecordingsRow aggregated;
auto maybe_finish_html_row = [&]() {
if (aggregated.start_time_90k == -1) {
return; // there is no row to finish.
}
auto seconds = static_cast<float>(aggregated.end_time_90k -
aggregated.start_time_90k) /
kTimeUnitsPerSecond;
buf.AddPrintf(
"<tr><td><a href=\"/view.mp4?camera_id=%" PRId64
"&start_time_90k=%" PRId64 "&end_time_90k=%" PRId64
"\">%s</a></td><td>%s</td><td>%dx%d</td>"
"<td>%.0f</td><td>%s</td><td>%s</td></tr>\n",
camera_id, aggregated.start_time_90k, aggregated.end_time_90k,
PrettyTimestamp(aggregated.start_time_90k).c_str(),
PrettyTimestamp(aggregated.end_time_90k).c_str(),
static_cast<int>(aggregated.width), static_cast<int>(aggregated.height),
static_cast<float>(aggregated.video_samples) / seconds,
HumanizeWithBinaryPrefix(aggregated.sample_file_bytes, "B").c_str(),
HumanizeWithDecimalPrefix(
static_cast<float>(aggregated.sample_file_bytes) * 8 / seconds,
"bps")
.c_str());
};
auto handle_sql_row = [&](const ListCameraRecordingsRow &row) {
auto new_duration_90k = row.end_time_90k - aggregated.start_time_90k;
if (row.video_sample_entry_sha1 == aggregated.video_sample_entry_sha1 &&
row.start_time_90k == aggregated.end_time_90k &&
new_duration_90k < kForceSplitDuration90k) {
// Append to current .mp4.
aggregated.end_time_90k = row.end_time_90k;
aggregated.video_samples += row.video_samples;
aggregated.sample_file_bytes += row.sample_file_bytes;
} else {
// Start a new .mp4.
maybe_finish_html_row();
aggregated = row;
}
return IterationControl::kContinue;
};
if (!this_->mdb_->ListCameraRecordings(camera_id, handle_sql_row,
&error_message)) {
return evhttp_send_error(
req, HTTP_INTERNAL,
StrCat("sqlite query failed: ", EscapeHtml(error_message)).c_str());
}
maybe_finish_html_row();
buf.Add(
"</table>\n"
"</html>\n");
evhttp_send_reply(req, HTTP_OK, "OK", buf.get());
}
void WebInterface::HandleMp4View(evhttp_request *req, void *arg) {
auto *this_ = reinterpret_cast<WebInterface *>(arg);
int64_t camera_id;
int64_t start_time_90k;
int64_t end_time_90k;
QueryParameters params(evhttp_request_get_uri(req));
if (!params.ok() || !Atoi64(params.Get("camera_id"), 10, &camera_id) ||
!Atoi64(params.Get("start_time_90k"), 10, &start_time_90k) ||
!Atoi64(params.Get("end_time_90k"), 10, &end_time_90k) ||
start_time_90k < 0 || start_time_90k >= end_time_90k) {
return evhttp_send_error(req, HTTP_BADREQUEST, "bad query parameters");
}
std::string error_message;
auto file = this_->mdb_->BuildMp4(camera_id, start_time_90k, end_time_90k,
&error_message);
if (file == nullptr) {
// TODO: more nuanced HTTP status codes.
return evhttp_send_error(req, HTTP_INTERNAL,
EscapeHtml(error_message).c_str());
}
return HttpServe(file, req);
}
} // namespace moonfire_nvr

78
src/web.h Normal file
View File

@ -0,0 +1,78 @@
// 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/>.
//
// web.h: web (HTTP/HTML) interface to the SQLite-based recording schema.
// Currently, during the transition from the old bunch-of-.mp4-files schema to
// the SQLite-based schema, it's convenient for this to be a separate class
// that interacts with the recording system only through the SQLite database
// and filesystem. In fact, the only advantage of being in-process is that it
// shares the same database mutex and avoids hitting SQLITE_BUSY.
//
// In the future, the interface will be reworked for tighter integration to
// support more features:
//
// * including the recording currently being written in the web interface
// * subscribing to changes
// * reconfiguring the recording system, such as
// adding/removing/starting/stopping/editing cameras
// * showing thumbnails of the latest key frame from each camera
// * ...
#ifndef MOONFIRE_NVR_WEB_H
#define MOONFIRE_NVR_WEB_H
#include <string>
#include <event2/http.h>
#include "moonfire-db.h"
#include "http.h"
namespace moonfire_nvr {
class WebInterface {
public:
explicit WebInterface(MoonfireDatabase *mdb) : mdb_(mdb) {}
WebInterface(const WebInterface &) = delete;
void operator=(const WebInterface &) = delete;
void Register(evhttp *http);
private:
static void HandleCameraList(evhttp_request *req, void *arg);
static void HandleCameraDetail(evhttp_request *req, void *arg);
static void HandleMp4View(evhttp_request *req, void *arg);
MoonfireDatabase *const mdb_;
};
} // namespace moonfire_nvr
#endif // MOONFIRE_NVR_WEB_H