mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-01-13 16:03:22 -05:00
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:
parent
055883d248
commit
40cd983355
@ -49,6 +49,7 @@ set(MOONFIRE_NVR_SRCS
|
|||||||
filesystem.cc
|
filesystem.cc
|
||||||
h264.cc
|
h264.cc
|
||||||
http.cc
|
http.cc
|
||||||
|
moonfire-db.cc
|
||||||
moonfire-nvr.cc
|
moonfire-nvr.cc
|
||||||
mp4.cc
|
mp4.cc
|
||||||
profiler.cc
|
profiler.cc
|
||||||
@ -56,7 +57,8 @@ set(MOONFIRE_NVR_SRCS
|
|||||||
sqlite.cc
|
sqlite.cc
|
||||||
string.cc
|
string.cc
|
||||||
time.cc
|
time.cc
|
||||||
uuid.cc)
|
uuid.cc
|
||||||
|
web.cc)
|
||||||
|
|
||||||
add_library(moonfire-nvr-lib ${MOONFIRE_NVR_SRCS} ${PROTO_SRCS} ${PROTO_HDRS})
|
add_library(moonfire-nvr-lib ${MOONFIRE_NVR_SRCS} ${PROTO_SRCS} ${PROTO_HDRS})
|
||||||
target_link_libraries(moonfire-nvr-lib ${MOONFIRE_DEPS})
|
target_link_libraries(moonfire-nvr-lib ${MOONFIRE_DEPS})
|
||||||
|
@ -62,7 +62,7 @@ class QueryParameters {
|
|||||||
// Caller should check ok() afterward.
|
// Caller should check ok() afterward.
|
||||||
QueryParameters(const char *uri) {
|
QueryParameters(const char *uri) {
|
||||||
TAILQ_INIT(&me_);
|
TAILQ_INIT(&me_);
|
||||||
ok_ = evhttp_parse_query_str(uri, &me_) == 0;
|
ok_ = evhttp_parse_query(uri, &me_) == 0;
|
||||||
}
|
}
|
||||||
QueryParameters(const QueryParameters &) = delete;
|
QueryParameters(const QueryParameters &) = delete;
|
||||||
void operator=(const QueryParameters &) = delete;
|
void operator=(const QueryParameters &) = delete;
|
||||||
|
342
src/moonfire-db.cc
Normal file
342
src/moonfire-db.cc
Normal 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
141
src/moonfire-db.h
Normal 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
|
@ -211,4 +211,15 @@ bool SampleFileWriter::Close(std::string *sha1, std::string *error_message) {
|
|||||||
return ok;
|
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
|
} // namespace moonfire_nvr
|
||||||
|
@ -194,6 +194,8 @@ struct VideoSampleEntry {
|
|||||||
uint16_t height = 0;
|
uint16_t height = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
std::string PrettyTimestamp(int64_t ts_90k);
|
||||||
|
|
||||||
} // namespace moonfire_nvr
|
} // namespace moonfire_nvr
|
||||||
|
|
||||||
#endif // MOONFIRE_NVR_RECORDING_H
|
#endif // MOONFIRE_NVR_RECORDING_H
|
||||||
|
@ -145,6 +145,41 @@ std::string HumanizeWithBinaryPrefix(float n, re2::StringPiece suffix) {
|
|||||||
return Humanize(kPrefixes, 1024., n, 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) {
|
bool strto64(const char *str, int base, const char **endptr, int64_t *value) {
|
||||||
static_assert(sizeof(int64_t) == sizeof(long long int),
|
static_assert(sizeof(int64_t) == sizeof(long long int),
|
||||||
"unknown memory model");
|
"unknown memory model");
|
||||||
|
@ -127,6 +127,8 @@ std::string ToHex(re2::StringPiece in, bool pad = false);
|
|||||||
std::string HumanizeWithDecimalPrefix(float n, re2::StringPiece suffix);
|
std::string HumanizeWithDecimalPrefix(float n, re2::StringPiece suffix);
|
||||||
std::string HumanizeWithBinaryPrefix(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
|
// Wrapper around ::strtoll that returns true iff valid and corrects
|
||||||
// constness. Returns false if |str| is null.
|
// constness. Returns false if |str| is null.
|
||||||
bool strto64(const char *str, int base, const char **endptr, int64_t *value);
|
bool strto64(const char *str, int base, const char **endptr, int64_t *value);
|
||||||
|
224
src/web.cc
Normal file
224
src/web.cc
Normal 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
78
src/web.h
Normal 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
|
Loading…
Reference in New Issue
Block a user