|
@@ -0,0 +1,875 @@
|
|
|
+#include "http_server.h"
|
|
|
+#include <iostream>
|
|
|
+#include <fstream>
|
|
|
+#include <sstream>
|
|
|
+#include <filesystem>
|
|
|
+#include <algorithm>
|
|
|
+#include <iomanip>
|
|
|
+#include <ctime>
|
|
|
+
|
|
|
+namespace jtjai_media {
|
|
|
+
|
|
|
+HttpServer::HttpServer(const std::string& output_directory, int port)
|
|
|
+ : output_directory_(output_directory)
|
|
|
+ , port_(port)
|
|
|
+ , is_running_(false) {
|
|
|
+}
|
|
|
+
|
|
|
+HttpServer::~HttpServer() {
|
|
|
+ stop();
|
|
|
+}
|
|
|
+
|
|
|
+bool HttpServer::start() {
|
|
|
+ if (is_running_.load()) {
|
|
|
+ std::cerr << "HTTP服务器已经在运行中" << std::endl;
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ is_running_.store(true);
|
|
|
+ server_thread_ = std::make_unique<std::thread>(&HttpServer::server_main_loop, this);
|
|
|
+
|
|
|
+ std::cout << "HTTP服务器已启动,监听端口: " << port_ << std::endl;
|
|
|
+ std::cout << "API端点:" << std::endl;
|
|
|
+ std::cout << " GET /api/videos - 列出所有视频" << std::endl;
|
|
|
+ std::cout << " GET /api/timestamps - 列出所有时间戳目录" << std::endl;
|
|
|
+ std::cout << " GET /api/videos/{timestamp} - 列出指定时间戳目录的视频" << std::endl;
|
|
|
+ std::cout << " DELETE /api/video?path={path} - 删除指定视频文件" << std::endl;
|
|
|
+ std::cout << " DELETE /api/timestamp/{timestamp} - 删除指定时间戳目录及其所有文件" << std::endl;
|
|
|
+ std::cout << "Web界面:" << std::endl;
|
|
|
+ std::cout << " GET / - 主页" << std::endl;
|
|
|
+ std::cout << " GET /manager - 视频管理界面" << std::endl;
|
|
|
+
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+void HttpServer::stop() {
|
|
|
+ if (!is_running_.load()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ std::cout << "正在停止HTTP服务器..." << std::endl;
|
|
|
+ is_running_.store(false);
|
|
|
+
|
|
|
+ if (server_thread_ && server_thread_->joinable()) {
|
|
|
+ server_thread_->join();
|
|
|
+ }
|
|
|
+
|
|
|
+ std::cout << "HTTP服务器已停止" << std::endl;
|
|
|
+}
|
|
|
+
|
|
|
+void HttpServer::server_main_loop() {
|
|
|
+ try {
|
|
|
+ boost::asio::io_context io_context;
|
|
|
+ boost::asio::ip::tcp::acceptor acceptor(
|
|
|
+ io_context,
|
|
|
+ boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port_)
|
|
|
+ );
|
|
|
+
|
|
|
+ acceptor.listen();
|
|
|
+
|
|
|
+ while (is_running_.load()) {
|
|
|
+ boost::asio::ip::tcp::socket socket(io_context);
|
|
|
+
|
|
|
+ // 设置接受超时
|
|
|
+ acceptor.non_blocking(true);
|
|
|
+
|
|
|
+ boost::system::error_code ec;
|
|
|
+ acceptor.accept(socket, ec);
|
|
|
+
|
|
|
+ if (!ec) {
|
|
|
+ // 在新线程中处理请求
|
|
|
+ std::thread([this](boost::asio::ip::tcp::socket sock) {
|
|
|
+ handle_client(std::move(sock));
|
|
|
+ }, std::move(socket)).detach();
|
|
|
+ } else if (ec != boost::asio::error::would_block) {
|
|
|
+ std::cerr << "接受连接错误: " << ec.message() << std::endl;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 短暂休眠避免CPU占用过高
|
|
|
+ std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (const std::exception& e) {
|
|
|
+ std::cerr << "HTTP服务器异常: " << e.what() << std::endl;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void HttpServer::handle_client(boost::asio::ip::tcp::socket socket) {
|
|
|
+ try {
|
|
|
+ // 读取请求
|
|
|
+ boost::asio::streambuf buffer;
|
|
|
+ boost::system::error_code ec;
|
|
|
+
|
|
|
+ // 读取直到双换行(HTTP请求结束标志)
|
|
|
+ boost::asio::read_until(socket, buffer, "\r\n\r\n", ec);
|
|
|
+
|
|
|
+ if (ec && ec != boost::asio::error::eof) {
|
|
|
+ std::cerr << "读取请求错误: " << ec.message() << std::endl;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 转换为字符串
|
|
|
+ std::string request_data(
|
|
|
+ boost::asio::buffers_begin(buffer.data()),
|
|
|
+ boost::asio::buffers_begin(buffer.data()) + buffer.size()
|
|
|
+ );
|
|
|
+
|
|
|
+ // 解析请求
|
|
|
+ HttpRequest request = parse_request(request_data);
|
|
|
+
|
|
|
+ // 路由处理
|
|
|
+ HttpResponse response = route_request(request);
|
|
|
+
|
|
|
+ // 构建响应
|
|
|
+ std::string response_data = build_response(response);
|
|
|
+
|
|
|
+ // 发送响应
|
|
|
+ boost::asio::write(socket, boost::asio::buffer(response_data), ec);
|
|
|
+
|
|
|
+ if (ec) {
|
|
|
+ std::cerr << "发送响应错误: " << ec.message() << std::endl;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 关闭连接
|
|
|
+ socket.close();
|
|
|
+
|
|
|
+ } catch (const std::exception& e) {
|
|
|
+ std::cerr << "处理客户端请求异常: " << e.what() << std::endl;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+HttpRequest HttpServer::parse_request(const std::string& request_data) {
|
|
|
+ HttpRequest request;
|
|
|
+ std::istringstream stream(request_data);
|
|
|
+ std::string line;
|
|
|
+
|
|
|
+ // 解析请求行
|
|
|
+ if (std::getline(stream, line)) {
|
|
|
+ std::istringstream request_line(line);
|
|
|
+ std::string uri;
|
|
|
+ request_line >> request.method >> uri;
|
|
|
+
|
|
|
+ // 分离路径和查询字符串
|
|
|
+ size_t query_pos = uri.find('?');
|
|
|
+ if (query_pos != std::string::npos) {
|
|
|
+ request.path = uri.substr(0, query_pos);
|
|
|
+ request.query_string = uri.substr(query_pos + 1);
|
|
|
+ } else {
|
|
|
+ request.path = uri;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析请求头
|
|
|
+ while (std::getline(stream, line) && line != "\r") {
|
|
|
+ size_t colon_pos = line.find(':');
|
|
|
+ if (colon_pos != std::string::npos) {
|
|
|
+ std::string key = line.substr(0, colon_pos);
|
|
|
+ std::string value = line.substr(colon_pos + 1);
|
|
|
+ // 去除首尾空格
|
|
|
+ value.erase(0, value.find_first_not_of(" \t\r\n"));
|
|
|
+ value.erase(value.find_last_not_of(" \t\r\n") + 1);
|
|
|
+ request.headers[key] = value;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return request;
|
|
|
+}
|
|
|
+
|
|
|
+std::string HttpServer::build_response(const HttpResponse& response) {
|
|
|
+ std::ostringstream oss;
|
|
|
+
|
|
|
+ // 状态行
|
|
|
+ oss << "HTTP/1.1 " << response.status_code << " " << response.status_message << "\r\n";
|
|
|
+
|
|
|
+ // 响应头
|
|
|
+ oss << "Content-Type: " << response.content_type << "\r\n";
|
|
|
+ oss << "Content-Length: " << response.body.length() << "\r\n";
|
|
|
+ oss << "Access-Control-Allow-Origin: *\r\n";
|
|
|
+ oss << "Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS\r\n";
|
|
|
+ oss << "Accept-Ranges: bytes\r\n"; // 支持Range请求
|
|
|
+ oss << "Connection: close\r\n";
|
|
|
+ oss << "\r\n";
|
|
|
+
|
|
|
+ // 响应体
|
|
|
+ oss << response.body;
|
|
|
+
|
|
|
+ return oss.str();
|
|
|
+}
|
|
|
+
|
|
|
+HttpResponse HttpServer::route_request(const HttpRequest& request) {
|
|
|
+ std::cout << request.method << " " << request.path;
|
|
|
+ if (!request.query_string.empty()) {
|
|
|
+ std::cout << "?" << request.query_string;
|
|
|
+ }
|
|
|
+ std::cout << std::endl;
|
|
|
+
|
|
|
+ // OPTIONS请求处理(CORS预检)
|
|
|
+ if (request.method == "OPTIONS") {
|
|
|
+ return HttpResponse(200, "OK", "");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 路由分发
|
|
|
+ if (request.method == "GET" || request.method == "HEAD") {
|
|
|
+ if (request.path == "/api/videos") {
|
|
|
+ return handle_list_videos(request);
|
|
|
+ } else if (request.path == "/api/timestamps") {
|
|
|
+ return handle_list_timestamps(request);
|
|
|
+ } else if (request.path.find("/api/videos/") == 0) {
|
|
|
+ return handle_get_video_info(request);
|
|
|
+ } else if (request.path.find("/videos/") == 0) {
|
|
|
+ // 处理视频文件访问
|
|
|
+ auto response = handle_video_file(request);
|
|
|
+ // 如果是HEAD请求,只返回头部,不返回体
|
|
|
+ if (request.method == "HEAD") {
|
|
|
+ response.body.clear();
|
|
|
+ }
|
|
|
+ return response;
|
|
|
+ } else {
|
|
|
+ // 处理静态文件访问(HTML、CSS、JS等)
|
|
|
+ return handle_static_file(request);
|
|
|
+ }
|
|
|
+ } else if (request.method == "DELETE") {
|
|
|
+ if (request.path == "/api/video") {
|
|
|
+ return handle_delete_video(request);
|
|
|
+ } else if (request.path.find("/api/timestamp/") == 0) {
|
|
|
+ return handle_delete_timestamp(request);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 404未找到
|
|
|
+ boost::json::object error_obj;
|
|
|
+ error_obj["error"] = "Not Found";
|
|
|
+ error_obj["message"] = "请求的资源不存在";
|
|
|
+ return HttpResponse(404, "Not Found", boost::json::serialize(error_obj));
|
|
|
+}
|
|
|
+
|
|
|
+HttpResponse HttpServer::handle_list_videos(const HttpRequest& request) {
|
|
|
+ try {
|
|
|
+ auto query_params = parse_query_string(request.query_string);
|
|
|
+ std::vector<VideoFileInfo> videos;
|
|
|
+
|
|
|
+ // 检查是否指定了时间戳
|
|
|
+ if (query_params.count("timestamp")) {
|
|
|
+ std::string timestamp = query_params["timestamp"];
|
|
|
+ std::string dir_path = output_directory_ + "/" + timestamp;
|
|
|
+ videos = list_videos_in_directory(dir_path);
|
|
|
+ } else {
|
|
|
+ videos = list_all_videos();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建JSON响应
|
|
|
+ boost::json::array videos_array;
|
|
|
+ for (const auto& video : videos) {
|
|
|
+ boost::json::object video_obj;
|
|
|
+ video_obj["filename"] = video.filename;
|
|
|
+ video_obj["full_path"] = video.full_path;
|
|
|
+ video_obj["timestamp_dir"] = video.timestamp_dir;
|
|
|
+ video_obj["file_size"] = video.file_size;
|
|
|
+ video_obj["created_time"] = video.created_time;
|
|
|
+ video_obj["stream_index"] = video.stream_index;
|
|
|
+ videos_array.push_back(video_obj);
|
|
|
+ }
|
|
|
+
|
|
|
+ boost::json::object result;
|
|
|
+ result["count"] = videos.size();
|
|
|
+ result["videos"] = videos_array;
|
|
|
+
|
|
|
+ return HttpResponse(200, "OK", boost::json::serialize(result));
|
|
|
+
|
|
|
+ } catch (const std::exception& e) {
|
|
|
+ boost::json::object error_obj;
|
|
|
+ error_obj["error"] = "Internal Server Error";
|
|
|
+ error_obj["message"] = e.what();
|
|
|
+ return HttpResponse(500, "Internal Server Error", boost::json::serialize(error_obj));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+HttpResponse HttpServer::handle_list_timestamps(const HttpRequest& request) {
|
|
|
+ try {
|
|
|
+ auto timestamps = list_timestamp_directories();
|
|
|
+
|
|
|
+ // 构建JSON响应
|
|
|
+ boost::json::array timestamps_array;
|
|
|
+ for (const auto& timestamp : timestamps) {
|
|
|
+ boost::json::object ts_obj;
|
|
|
+ ts_obj["timestamp"] = timestamp;
|
|
|
+
|
|
|
+ // 获取该目录下的视频数量
|
|
|
+ std::string dir_path = output_directory_ + "/" + timestamp;
|
|
|
+ auto videos = list_videos_in_directory(dir_path);
|
|
|
+ ts_obj["video_count"] = videos.size();
|
|
|
+
|
|
|
+ // 计算总大小
|
|
|
+ int64_t total_size = 0;
|
|
|
+ for (const auto& video : videos) {
|
|
|
+ total_size += video.file_size;
|
|
|
+ }
|
|
|
+ ts_obj["total_size"] = total_size;
|
|
|
+
|
|
|
+ timestamps_array.push_back(ts_obj);
|
|
|
+ }
|
|
|
+
|
|
|
+ boost::json::object result;
|
|
|
+ result["count"] = timestamps.size();
|
|
|
+ result["timestamps"] = timestamps_array;
|
|
|
+
|
|
|
+ return HttpResponse(200, "OK", boost::json::serialize(result));
|
|
|
+
|
|
|
+ } catch (const std::exception& e) {
|
|
|
+ boost::json::object error_obj;
|
|
|
+ error_obj["error"] = "Internal Server Error";
|
|
|
+ error_obj["message"] = e.what();
|
|
|
+ return HttpResponse(500, "Internal Server Error", boost::json::serialize(error_obj));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+HttpResponse HttpServer::handle_delete_video(const HttpRequest& request) {
|
|
|
+ try {
|
|
|
+ auto query_params = parse_query_string(request.query_string);
|
|
|
+
|
|
|
+ if (!query_params.count("path")) {
|
|
|
+ boost::json::object error_obj;
|
|
|
+ error_obj["error"] = "Bad Request";
|
|
|
+ error_obj["message"] = "缺少path参数";
|
|
|
+ return HttpResponse(400, "Bad Request", boost::json::serialize(error_obj));
|
|
|
+ }
|
|
|
+
|
|
|
+ std::string file_path = query_params["path"];
|
|
|
+
|
|
|
+ // 安全检查:确保文件路径在输出目录内
|
|
|
+ std::filesystem::path abs_path = std::filesystem::absolute(file_path);
|
|
|
+ std::filesystem::path abs_output = std::filesystem::absolute(output_directory_);
|
|
|
+
|
|
|
+ if (abs_path.string().find(abs_output.string()) != 0) {
|
|
|
+ boost::json::object error_obj;
|
|
|
+ error_obj["error"] = "Forbidden";
|
|
|
+ error_obj["message"] = "无权删除该文件";
|
|
|
+ return HttpResponse(403, "Forbidden", boost::json::serialize(error_obj));
|
|
|
+ }
|
|
|
+
|
|
|
+ bool success = delete_file(file_path);
|
|
|
+
|
|
|
+ boost::json::object result;
|
|
|
+ result["success"] = success;
|
|
|
+ result["path"] = file_path;
|
|
|
+
|
|
|
+ if (success) {
|
|
|
+ result["message"] = "文件删除成功";
|
|
|
+ return HttpResponse(200, "OK", boost::json::serialize(result));
|
|
|
+ } else {
|
|
|
+ result["message"] = "文件删除失败";
|
|
|
+ return HttpResponse(500, "Internal Server Error", boost::json::serialize(result));
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (const std::exception& e) {
|
|
|
+ boost::json::object error_obj;
|
|
|
+ error_obj["error"] = "Internal Server Error";
|
|
|
+ error_obj["message"] = e.what();
|
|
|
+ return HttpResponse(500, "Internal Server Error", boost::json::serialize(error_obj));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+HttpResponse HttpServer::handle_delete_timestamp(const HttpRequest& request) {
|
|
|
+ try {
|
|
|
+ // 从路径中提取时间戳 /api/timestamp/{timestamp}
|
|
|
+ std::string path = request.path;
|
|
|
+ size_t pos = path.find_last_of('/');
|
|
|
+ if (pos == std::string::npos) {
|
|
|
+ boost::json::object error_obj;
|
|
|
+ error_obj["error"] = "Bad Request";
|
|
|
+ error_obj["message"] = "无效的请求路径";
|
|
|
+ return HttpResponse(400, "Bad Request", boost::json::serialize(error_obj));
|
|
|
+ }
|
|
|
+
|
|
|
+ std::string timestamp = path.substr(pos + 1);
|
|
|
+ std::string dir_path = output_directory_ + "/" + timestamp;
|
|
|
+
|
|
|
+ // 安全检查
|
|
|
+ std::filesystem::path abs_path = std::filesystem::absolute(dir_path);
|
|
|
+ std::filesystem::path abs_output = std::filesystem::absolute(output_directory_);
|
|
|
+
|
|
|
+ if (abs_path.string().find(abs_output.string()) != 0) {
|
|
|
+ boost::json::object error_obj;
|
|
|
+ error_obj["error"] = "Forbidden";
|
|
|
+ error_obj["message"] = "无权删除该目录";
|
|
|
+ return HttpResponse(403, "Forbidden", boost::json::serialize(error_obj));
|
|
|
+ }
|
|
|
+
|
|
|
+ bool success = delete_directory(dir_path);
|
|
|
+
|
|
|
+ boost::json::object result;
|
|
|
+ result["success"] = success;
|
|
|
+ result["timestamp"] = timestamp;
|
|
|
+ result["path"] = dir_path;
|
|
|
+
|
|
|
+ if (success) {
|
|
|
+ result["message"] = "目录删除成功";
|
|
|
+ return HttpResponse(200, "OK", boost::json::serialize(result));
|
|
|
+ } else {
|
|
|
+ result["message"] = "目录删除失败";
|
|
|
+ return HttpResponse(500, "Internal Server Error", boost::json::serialize(result));
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (const std::exception& e) {
|
|
|
+ boost::json::object error_obj;
|
|
|
+ error_obj["error"] = "Internal Server Error";
|
|
|
+ error_obj["message"] = e.what();
|
|
|
+ return HttpResponse(500, "Internal Server Error", boost::json::serialize(error_obj));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+HttpResponse HttpServer::handle_get_video_info(const HttpRequest& request) {
|
|
|
+ // 从路径中提取时间戳 /api/videos/{timestamp}
|
|
|
+ std::string path = request.path;
|
|
|
+ size_t pos = path.find_last_of('/');
|
|
|
+ if (pos == std::string::npos) {
|
|
|
+ boost::json::object error_obj;
|
|
|
+ error_obj["error"] = "Bad Request";
|
|
|
+ error_obj["message"] = "无效的请求路径";
|
|
|
+ return HttpResponse(400, "Bad Request", boost::json::serialize(error_obj));
|
|
|
+ }
|
|
|
+
|
|
|
+ std::string timestamp = path.substr(pos + 1);
|
|
|
+ std::string dir_path = output_directory_ + "/" + timestamp;
|
|
|
+
|
|
|
+ auto videos = list_videos_in_directory(dir_path);
|
|
|
+
|
|
|
+ boost::json::array videos_array;
|
|
|
+ for (const auto& video : videos) {
|
|
|
+ boost::json::object video_obj;
|
|
|
+ video_obj["filename"] = video.filename;
|
|
|
+ video_obj["full_path"] = video.full_path;
|
|
|
+ video_obj["timestamp_dir"] = video.timestamp_dir;
|
|
|
+ video_obj["file_size"] = video.file_size;
|
|
|
+ video_obj["created_time"] = video.created_time;
|
|
|
+ video_obj["stream_index"] = video.stream_index;
|
|
|
+ videos_array.push_back(video_obj);
|
|
|
+ }
|
|
|
+
|
|
|
+ boost::json::object result;
|
|
|
+ result["timestamp"] = timestamp;
|
|
|
+ result["count"] = videos.size();
|
|
|
+ result["videos"] = videos_array;
|
|
|
+
|
|
|
+ return HttpResponse(200, "OK", boost::json::serialize(result));
|
|
|
+}
|
|
|
+
|
|
|
+HttpResponse HttpServer::handle_video_file(const HttpRequest& request) {
|
|
|
+ try {
|
|
|
+ // 从路径中提取文件路径 /videos/{timestamp}/{filename}
|
|
|
+ std::string path = request.path;
|
|
|
+ size_t pos = path.find("/videos/");
|
|
|
+ if (pos == std::string::npos) {
|
|
|
+ boost::json::object error_obj;
|
|
|
+ error_obj["error"] = "Bad Request";
|
|
|
+ error_obj["message"] = "无效的请求路径";
|
|
|
+ return HttpResponse(400, "Bad Request", boost::json::serialize(error_obj));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取相对路径(URL解码)
|
|
|
+ std::string encoded_path = path.substr(pos + 8); // 跳过 "/videos/"
|
|
|
+ std::string relative_path = url_decode(encoded_path);
|
|
|
+
|
|
|
+ // 构建完整文件路径
|
|
|
+ std::string file_path = output_directory_ + "/" + relative_path;
|
|
|
+
|
|
|
+ std::cout << "请求视频文件: " << file_path << std::endl;
|
|
|
+
|
|
|
+ // 安全检查:确保文件路径在输出目录内
|
|
|
+ std::filesystem::path abs_path = std::filesystem::absolute(file_path);
|
|
|
+ std::filesystem::path abs_output = std::filesystem::absolute(output_directory_);
|
|
|
+
|
|
|
+ if (abs_path.string().find(abs_output.string()) != 0) {
|
|
|
+ boost::json::object error_obj;
|
|
|
+ error_obj["error"] = "Forbidden";
|
|
|
+ error_obj["message"] = "无权访问该文件";
|
|
|
+ return HttpResponse(403, "Forbidden", boost::json::serialize(error_obj));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查文件是否存在
|
|
|
+ if (!std::filesystem::exists(file_path)) {
|
|
|
+ boost::json::object error_obj;
|
|
|
+ error_obj["error"] = "Not Found";
|
|
|
+ error_obj["message"] = "文件不存在: " + file_path;
|
|
|
+ return HttpResponse(404, "Not Found", boost::json::serialize(error_obj));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取文件大小
|
|
|
+ size_t file_size = std::filesystem::file_size(file_path);
|
|
|
+
|
|
|
+ // 读取文件内容
|
|
|
+ std::ifstream file(file_path, std::ios::binary);
|
|
|
+ if (!file.is_open()) {
|
|
|
+ boost::json::object error_obj;
|
|
|
+ error_obj["error"] = "Internal Server Error";
|
|
|
+ error_obj["message"] = "无法打开文件";
|
|
|
+ return HttpResponse(500, "Internal Server Error", boost::json::serialize(error_obj));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 读取全部文件内容
|
|
|
+ std::string content(file_size, '\0');
|
|
|
+ file.read(&content[0], file_size);
|
|
|
+ file.close();
|
|
|
+
|
|
|
+ std::cout << "发送视频文件: " << file_path << " (" << file_size << " bytes)" << std::endl;
|
|
|
+
|
|
|
+ // 构建响应
|
|
|
+ HttpResponse response;
|
|
|
+ response.status_code = 200;
|
|
|
+ response.status_message = "OK";
|
|
|
+
|
|
|
+ // 根据文件扩展名设置Content-Type
|
|
|
+ std::string ext = std::filesystem::path(file_path).extension().string();
|
|
|
+ if (ext == ".mp4") {
|
|
|
+ response.content_type = "video/mp4";
|
|
|
+ } else if (ext == ".avi") {
|
|
|
+ response.content_type = "video/x-msvideo";
|
|
|
+ } else if (ext == ".mkv") {
|
|
|
+ response.content_type = "video/x-matroska";
|
|
|
+ } else if (ext == ".flv") {
|
|
|
+ response.content_type = "video/x-flv";
|
|
|
+ } else {
|
|
|
+ response.content_type = "application/octet-stream";
|
|
|
+ }
|
|
|
+
|
|
|
+ response.body = content;
|
|
|
+
|
|
|
+ return response;
|
|
|
+
|
|
|
+ } catch (const std::exception& e) {
|
|
|
+ boost::json::object error_obj;
|
|
|
+ error_obj["error"] = "Internal Server Error";
|
|
|
+ error_obj["message"] = e.what();
|
|
|
+ return HttpResponse(500, "Internal Server Error", boost::json::serialize(error_obj));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+std::vector<std::string> HttpServer::list_timestamp_directories() {
|
|
|
+ std::vector<std::string> timestamps;
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (!std::filesystem::exists(output_directory_)) {
|
|
|
+ return timestamps;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const auto& entry : std::filesystem::directory_iterator(output_directory_)) {
|
|
|
+ if (entry.is_directory()) {
|
|
|
+ timestamps.push_back(entry.path().filename().string());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按时间戳排序(最新的在前)
|
|
|
+ std::sort(timestamps.begin(), timestamps.end(), std::greater<std::string>());
|
|
|
+
|
|
|
+ } catch (const std::exception& e) {
|
|
|
+ std::cerr << "列出时间戳目录错误: " << e.what() << std::endl;
|
|
|
+ }
|
|
|
+
|
|
|
+ return timestamps;
|
|
|
+}
|
|
|
+
|
|
|
+std::vector<VideoFileInfo> HttpServer::list_videos_in_directory(const std::string& dir_path) {
|
|
|
+ std::vector<VideoFileInfo> videos;
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (!std::filesystem::exists(dir_path)) {
|
|
|
+ return videos;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const auto& entry : std::filesystem::directory_iterator(dir_path)) {
|
|
|
+ if (entry.is_regular_file()) {
|
|
|
+ std::string filename = entry.path().filename().string();
|
|
|
+ std::string ext = entry.path().extension().string();
|
|
|
+
|
|
|
+ // 只列出视频文件
|
|
|
+ if (ext == ".mp4" || ext == ".avi" || ext == ".mkv" || ext == ".flv") {
|
|
|
+ VideoFileInfo info;
|
|
|
+ info.filename = filename;
|
|
|
+ info.full_path = entry.path().string();
|
|
|
+ info.timestamp_dir = std::filesystem::path(dir_path).filename().string();
|
|
|
+ info.file_size = get_file_size(info.full_path);
|
|
|
+ info.created_time = get_file_created_time(info.full_path);
|
|
|
+
|
|
|
+ // 从文件名提取流索引
|
|
|
+ size_t stream_pos = filename.find("stream");
|
|
|
+ if (stream_pos != std::string::npos) {
|
|
|
+ size_t num_start = stream_pos + 6;
|
|
|
+ size_t num_end = filename.find_first_not_of("0123456789", num_start);
|
|
|
+ if (num_end != std::string::npos) {
|
|
|
+ info.stream_index = filename.substr(num_start, num_end - num_start);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ videos.push_back(info);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按文件名排序
|
|
|
+ std::sort(videos.begin(), videos.end(),
|
|
|
+ [](const VideoFileInfo& a, const VideoFileInfo& b) {
|
|
|
+ return a.filename < b.filename;
|
|
|
+ });
|
|
|
+
|
|
|
+ } catch (const std::exception& e) {
|
|
|
+ std::cerr << "列出视频文件错误: " << e.what() << std::endl;
|
|
|
+ }
|
|
|
+
|
|
|
+ return videos;
|
|
|
+}
|
|
|
+
|
|
|
+std::vector<VideoFileInfo> HttpServer::list_all_videos() {
|
|
|
+ std::vector<VideoFileInfo> all_videos;
|
|
|
+
|
|
|
+ auto timestamps = list_timestamp_directories();
|
|
|
+ for (const auto& timestamp : timestamps) {
|
|
|
+ std::string dir_path = output_directory_ + "/" + timestamp;
|
|
|
+ auto videos = list_videos_in_directory(dir_path);
|
|
|
+ all_videos.insert(all_videos.end(), videos.begin(), videos.end());
|
|
|
+ }
|
|
|
+
|
|
|
+ return all_videos;
|
|
|
+}
|
|
|
+
|
|
|
+bool HttpServer::delete_file(const std::string& file_path) {
|
|
|
+ try {
|
|
|
+ if (std::filesystem::exists(file_path)) {
|
|
|
+ std::filesystem::remove(file_path);
|
|
|
+ std::cout << "已删除文件: " << file_path << std::endl;
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ } catch (const std::exception& e) {
|
|
|
+ std::cerr << "删除文件错误: " << e.what() << std::endl;
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+bool HttpServer::delete_directory(const std::string& dir_path) {
|
|
|
+ try {
|
|
|
+ if (std::filesystem::exists(dir_path)) {
|
|
|
+ std::filesystem::remove_all(dir_path);
|
|
|
+ std::cout << "已删除目录: " << dir_path << std::endl;
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ } catch (const std::exception& e) {
|
|
|
+ std::cerr << "删除目录错误: " << e.what() << std::endl;
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+std::string HttpServer::get_file_created_time(const std::string& file_path) {
|
|
|
+ try {
|
|
|
+ auto ftime = std::filesystem::last_write_time(file_path);
|
|
|
+ auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
|
|
|
+ ftime - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now()
|
|
|
+ );
|
|
|
+ auto time_t = std::chrono::system_clock::to_time_t(sctp);
|
|
|
+ auto tm = *std::localtime(&time_t);
|
|
|
+
|
|
|
+ std::ostringstream oss;
|
|
|
+ oss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
|
|
|
+ return oss.str();
|
|
|
+ } catch (const std::exception& e) {
|
|
|
+ return "Unknown";
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+int64_t HttpServer::get_file_size(const std::string& file_path) {
|
|
|
+ try {
|
|
|
+ return std::filesystem::file_size(file_path);
|
|
|
+ } catch (const std::exception& e) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+std::map<std::string, std::string> HttpServer::parse_query_string(const std::string& query) {
|
|
|
+ std::map<std::string, std::string> params;
|
|
|
+
|
|
|
+ if (query.empty()) {
|
|
|
+ return params;
|
|
|
+ }
|
|
|
+
|
|
|
+ std::istringstream stream(query);
|
|
|
+ std::string pair;
|
|
|
+
|
|
|
+ while (std::getline(stream, pair, '&')) {
|
|
|
+ size_t eq_pos = pair.find('=');
|
|
|
+ if (eq_pos != std::string::npos) {
|
|
|
+ std::string key = pair.substr(0, eq_pos);
|
|
|
+ std::string value = pair.substr(eq_pos + 1);
|
|
|
+ params[key] = value;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return params;
|
|
|
+}
|
|
|
+
|
|
|
+std::string HttpServer::url_decode(const std::string& str) {
|
|
|
+ std::string result;
|
|
|
+ result.reserve(str.size());
|
|
|
+
|
|
|
+ for (size_t i = 0; i < str.size(); ++i) {
|
|
|
+ if (str[i] == '%') {
|
|
|
+ if (i + 2 < str.size()) {
|
|
|
+ int value;
|
|
|
+ std::istringstream is(str.substr(i + 1, 2));
|
|
|
+ if (is >> std::hex >> value) {
|
|
|
+ result += static_cast<char>(value);
|
|
|
+ i += 2;
|
|
|
+ } else {
|
|
|
+ result += str[i];
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ result += str[i];
|
|
|
+ }
|
|
|
+ } else if (str[i] == '+') {
|
|
|
+ result += ' ';
|
|
|
+ } else {
|
|
|
+ result += str[i];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+}
|
|
|
+
|
|
|
+HttpResponse HttpServer::handle_static_file(const HttpRequest& request) {
|
|
|
+ try {
|
|
|
+ std::string file_path;
|
|
|
+ std::string content_type = "text/html";
|
|
|
+
|
|
|
+ // 路由映射
|
|
|
+ if (request.path == "/" || request.path == "/index.html") {
|
|
|
+ // 主页 - 显示系统信息
|
|
|
+ return generate_index_page();
|
|
|
+ } else if (request.path == "/manager" || request.path == "/manager.html") {
|
|
|
+ // 视频管理界面
|
|
|
+ file_path = "video_manager.html";
|
|
|
+ content_type = "text/html";
|
|
|
+ } else if (request.path == "/api" || request.path == "/api.html") {
|
|
|
+ // API文档
|
|
|
+ return generate_api_doc();
|
|
|
+ } else {
|
|
|
+ // 404
|
|
|
+ boost::json::object error_obj;
|
|
|
+ error_obj["error"] = "Not Found";
|
|
|
+ error_obj["message"] = "请求的资源不存在";
|
|
|
+ return HttpResponse(404, "Not Found", boost::json::serialize(error_obj));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 读取静态文件
|
|
|
+ if (!std::filesystem::exists(file_path)) {
|
|
|
+ boost::json::object error_obj;
|
|
|
+ error_obj["error"] = "Not Found";
|
|
|
+ error_obj["message"] = "文件不存在: " + file_path;
|
|
|
+ return HttpResponse(404, "Not Found", boost::json::serialize(error_obj));
|
|
|
+ }
|
|
|
+
|
|
|
+ std::ifstream file(file_path);
|
|
|
+ if (!file.is_open()) {
|
|
|
+ boost::json::object error_obj;
|
|
|
+ error_obj["error"] = "Internal Server Error";
|
|
|
+ error_obj["message"] = "无法打开文件";
|
|
|
+ return HttpResponse(500, "Internal Server Error", boost::json::serialize(error_obj));
|
|
|
+ }
|
|
|
+
|
|
|
+ std::string content((std::istreambuf_iterator<char>(file)),
|
|
|
+ std::istreambuf_iterator<char>());
|
|
|
+ file.close();
|
|
|
+
|
|
|
+ HttpResponse response;
|
|
|
+ response.status_code = 200;
|
|
|
+ response.status_message = "OK";
|
|
|
+ response.content_type = content_type;
|
|
|
+ response.body = content;
|
|
|
+
|
|
|
+ return response;
|
|
|
+
|
|
|
+ } catch (const std::exception& e) {
|
|
|
+ boost::json::object error_obj;
|
|
|
+ error_obj["error"] = "Internal Server Error";
|
|
|
+ error_obj["message"] = e.what();
|
|
|
+ return HttpResponse(500, "Internal Server Error", boost::json::serialize(error_obj));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+HttpResponse HttpServer::generate_index_page() {
|
|
|
+ std::string html = R"(
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="zh-CN">
|
|
|
+<head>
|
|
|
+ <meta charset="UTF-8">
|
|
|
+ <title>RTSP视频流管理系统</title>
|
|
|
+ <style>
|
|
|
+ body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; margin: 0; padding: 40px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; }
|
|
|
+ .container { max-width: 800px; margin: 0 auto; background: white; padding: 40px; border-radius: 10px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); }
|
|
|
+ h1 { color: #333; text-align: center; }
|
|
|
+ .card { background: #f8f9fa; padding: 20px; margin: 20px 0; border-radius: 8px; }
|
|
|
+ .btn { display: inline-block; padding: 12px 24px; background: #667eea; color: white; text-decoration: none; border-radius: 5px; margin: 10px 10px 10px 0; }
|
|
|
+ .btn:hover { background: #5568d3; }
|
|
|
+ </style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+ <div class="container">
|
|
|
+ <h1>🌅 RTSP视频流管理系统</h1>
|
|
|
+ <div class="card">
|
|
|
+ <h2>🚀 快速开始</h2>
|
|
|
+ <p>欢迎使用RTSP视频流管理系统!</p>
|
|
|
+ <a href="/manager" class="btn">📹 视频管理</a>
|
|
|
+ <a href="/api" class="btn">📊 API文档</a>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</body>
|
|
|
+</html>
|
|
|
+ )";
|
|
|
+
|
|
|
+ HttpResponse response;
|
|
|
+ response.status_code = 200;
|
|
|
+ response.status_message = "OK";
|
|
|
+ response.content_type = "text/html";
|
|
|
+ response.body = html;
|
|
|
+ return response;
|
|
|
+}
|
|
|
+
|
|
|
+HttpResponse HttpServer::generate_api_doc() {
|
|
|
+ std::string html = R"(
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="zh-CN">
|
|
|
+<head>
|
|
|
+ <meta charset="UTF-8">
|
|
|
+ <title>API文档</title>
|
|
|
+ <style>
|
|
|
+ body { font-family: sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
|
|
|
+ .container { max-width: 1000px; margin: 0 auto; background: white; padding: 40px; border-radius: 10px; }
|
|
|
+ .endpoint { background: #f8f9fa; padding: 20px; margin: 20px 0; border-radius: 8px; }
|
|
|
+ .method { display: inline-block; padding: 4px 8px; border-radius: 4px; color: white; font-weight: bold; margin-right: 10px; }
|
|
|
+ .get { background: #28a745; }
|
|
|
+ .delete { background: #dc3545; }
|
|
|
+ </style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+ <div class="container">
|
|
|
+ <h1>📊 API文档</h1>
|
|
|
+ <div class="endpoint">
|
|
|
+ <h3><span class="method get">GET</span>/api/videos</h3>
|
|
|
+ <p>列出所有视频文件</p>
|
|
|
+ </div>
|
|
|
+ <div class="endpoint">
|
|
|
+ <h3><span class="method delete">DELETE</span>/api/video</h3>
|
|
|
+ <p>删除指定视频文件</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</body>
|
|
|
+</html>
|
|
|
+ )";
|
|
|
+
|
|
|
+ HttpResponse response;
|
|
|
+ response.status_code = 200;
|
|
|
+ response.status_message = "OK";
|
|
|
+ response.content_type = "text/html";
|
|
|
+ response.body = html;
|
|
|
+ return response;
|
|
|
+}
|
|
|
+
|
|
|
+} // namespace jtjai_media
|