// This file is part of Moonfire NVR, a security camera network video recorder. // Copyright (C) 2016 Scott Lamb // // 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 . // // web.cc: implementation of web.h interface. #include "web.h" #include #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(arg); EvBuffer buf; buf.Add( "\n" "\n" "\n" "Camera list\n" "\n" "\n" "\n" "\n"); auto row_cb = [&](const ListCamerasRow &row) { auto seconds = (row.max_end_time_90k - row.min_start_time_90k) / kTimeUnitsPerSecond; std::string min_start_time_90k = row.min_start_time_90k == -1 ? std::string("n/a") : PrettyTimestamp(row.min_start_time_90k); std::string max_end_time_90k = row.max_end_time_90k == -1 ? std::string("n/a") : PrettyTimestamp(row.max_end_time_90k); buf.AddPrintf( "\n" "\n" "\n" "\n" "\n" "\n" "\n", row.uuid.UnparseText().c_str(), 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(min_start_time_90k).c_str(), EscapeHtml(max_end_time_90k).c_str(), EscapeHtml(HumanizeDuration(seconds)).c_str()); return IterationControl::kContinue; }; this_->mdb_->ListCameras(row_cb); buf.Add( "
%s" "
description%s
space%s / %s (%.1f%%)
uuid%s
oldest recording%s
newest recording%s
total duration%s
\n" "\n" "\n"); evhttp_send_reply(req, HTTP_OK, "OK", buf.get()); } void WebInterface::HandleCameraDetail(evhttp_request *req, void *arg) { auto *this_ = reinterpret_cast(arg); Uuid camera_uuid; QueryParameters params(evhttp_request_get_uri(req)); if (!params.ok() || !camera_uuid.ParseText(params.Get("uuid"))) { return evhttp_send_error(req, HTTP_BADREQUEST, "bad query parameters"); } GetCameraRow camera_row; if (!this_->mdb_->GetCamera(camera_uuid, &camera_row)) { return evhttp_send_error(req, HTTP_NOTFOUND, "no such camera"); } EvBuffer buf; buf.AddPrintf( "\n" "\n" "\n" "%s recordings\n" "\n" "\n" "\n" "

%s

\n" "

%s

\n" "\n" "" "" "\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(aggregated.end_time_90k - aggregated.start_time_90k) / kTimeUnitsPerSecond; buf.AddPrintf( "" "\n", camera_uuid.UnparseText().c_str(), aggregated.start_time_90k, aggregated.end_time_90k, PrettyTimestamp(aggregated.start_time_90k).c_str(), PrettyTimestamp(aggregated.end_time_90k).c_str(), static_cast(aggregated.width), static_cast(aggregated.height), static_cast(aggregated.video_samples) / seconds, HumanizeWithBinaryPrefix(aggregated.sample_file_bytes, "B").c_str(), HumanizeWithDecimalPrefix( static_cast(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.end_time_90k == aggregated.start_time_90k && new_duration_90k < kForceSplitDuration90k) { // Append to current .mp4. aggregated.start_time_90k = row.start_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; }; int64_t start_time_90k = 0; int64_t end_time_90k = std::numeric_limits::max(); std::string error_message; if (!this_->mdb_->ListCameraRecordings(camera_uuid, start_time_90k, end_time_90k, 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( "
startendresolutionfpssizebitrate
%s%s%dx%d%.0f%s%s
\n" "\n"); evhttp_send_reply(req, HTTP_OK, "OK", buf.get()); } void WebInterface::HandleMp4View(evhttp_request *req, void *arg) { auto *this_ = reinterpret_cast(arg); Uuid camera_uuid; int64_t start_time_90k; int64_t end_time_90k; QueryParameters params(evhttp_request_get_uri(req)); if (!params.ok() || !camera_uuid.ParseText(params.Get("camera_uuid")) || !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_uuid, 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