diff --git a/postman/ZLMediaKit.postman_collection.json b/postman/ZLMediaKit.postman_collection.json index 49ea10b9..91cb79ae 100644 --- a/postman/ZLMediaKit.postman_collection.json +++ b/postman/ZLMediaKit.postman_collection.json @@ -1380,6 +1380,62 @@ }, "response": [] }, + { + "name": "开始事件视频录制(startRecordTask)", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{ZLMediaKit_URL}}/index/api/startRecordTask?secret={{ZLMediaKit_secret}}&vhost={{defaultVhost}}&app=live&stream=test&path=1.mp4&back_ms=10000&forward_ms=10000", + "host": [ + "{{ZLMediaKit_URL}}" + ], + "path": [ + "index", + "api", + "startRecordTask" + ], + "query": [ + { + "key": "secret", + "value": "{{ZLMediaKit_secret}}", + "description": "api操作密钥(配置文件配置)" + }, + { + "key": "vhost", + "value": "{{defaultVhost}}", + "description": "虚拟主机,例如__defaultVhost__" + }, + { + "key": "app", + "value": "live", + "description": "应用名,例如 live" + }, + { + "key": "stream", + "value": "test", + "description": "流id,例如 obs" + }, + { + "key": "path", + "value": "1.mp4", + "description": "录像文件保存相对路径,包括名称" + }, + { + "key": "back_ms", + "value": "10000", + "description": "回溯录制时长" + }, + { + "key": "forward_ms", + "value": "10000", + "description": "后续录制时长" + } + ] + } + }, + "response": [] + }, { "name": "设置录像速度(setRecordSpeed)", "request": { diff --git a/server/WebApi.cpp b/server/WebApi.cpp index 54042afb..05884c51 100755 --- a/server/WebApi.cpp +++ b/server/WebApi.cpp @@ -1776,6 +1776,30 @@ void installWebApi() { }); }); + api_regist("/index/api/startRecordTask",[](API_ARGS_MAP_ASYNC){ + CHECK_SECRET(); + CHECK_ARGS("vhost", "app", "stream", "path", "back_ms", "forward_ms"); + + auto src = MediaSource::find(allArgs["vhost"], allArgs["app"], allArgs["stream"]); + if (!src) { + throw ApiRetException("can not find the stream", API::NotFound); + } + + src->getOwnerPoller()->async([=]() mutable { + std::string err; + std::string path; + try { + path = src->getMuxer()->startRecord(allArgs["path"], allArgs["back_ms"], allArgs["forward_ms"]); + } catch (std::exception &ex) { + err = ex.what(); + } + val["code"] = err.empty() ? API::Success : API::OtherFailed; + val["data"]["path"] = path; + val["msg"] = err; + invoker(200, headerOut, val.toStyledString()); + }); + }); + // 设置录像流播放速度 [AUTO-TRANSLATED:a8d82298] // Set the playback speed of the recording stream api_regist("/index/api/setRecordSpeed", [](API_ARGS_MAP_ASYNC) { @@ -1888,11 +1912,13 @@ void installWebApi() { // http://127.0.0.1/index/api/deleteRecordDirectroy?vhost=__defaultVhost__&app=live&stream=ss&period=2020-01-01 api_regist("/index/api/deleteRecordDirectory", [](API_ARGS_MAP) { CHECK_SECRET(); - CHECK_ARGS("vhost", "app", "stream", "period"); + CHECK_ARGS("vhost", "app", "stream"); auto tuple = MediaTuple{allArgs["vhost"], allArgs["app"], allArgs["stream"], ""}; auto record_path = Recorder::getRecordPath(Recorder::type_mp4, tuple, allArgs["customized_path"]); auto period = allArgs["period"]; - record_path = record_path + period + "/"; + if (!period.empty()) { + record_path = record_path + period + "/"; + } bool recording = false; auto name = allArgs["name"]; diff --git a/src/Common/MultiMediaSourceMuxer.cpp b/src/Common/MultiMediaSourceMuxer.cpp index 2c6b30ac..356f9996 100644 --- a/src/Common/MultiMediaSourceMuxer.cpp +++ b/src/Common/MultiMediaSourceMuxer.cpp @@ -11,6 +11,7 @@ #include #include "Common/config.h" #include "MultiMediaSourceMuxer.h" +#include "Thread/WorkThreadPool.h" using namespace std; using namespace toolkit; @@ -400,6 +401,88 @@ bool MultiMediaSourceMuxer::setupRecord(MediaSource &sender, Recorder::type type } } +std::string MultiMediaSourceMuxer::startRecord(const std::string &file_path, uint32_t back_time_ms, uint32_t forward_time_ms) { +#if !defined(ENABLE_MP4) + throw std::invalid_argument("mp4相关功能未打开,请开启ENABLE_MP4宏后编译再测试"); +#else + if (!_ring) { + throw std::runtime_error("frame gop cache disabled, start record event video failed"); + } + auto path = Recorder::getRecordPath(Recorder::type_mp4, _tuple, _option.mp4_save_path); + path += file_path; + TraceL << "mp4 save path: " << path; + + auto muxer = std::make_shared(); + muxer->openMP4(path); + for (auto &track : MediaSink::getTracks()) { + muxer->addTrack(track); + } + muxer->addTrackCompleted(); + + std::list history; + _ring->flushGop([&](const Frame::Ptr &frame) { history.emplace_back(frame); }); + if (!history.empty()) { + auto now_dts = history.back()->dts(); + + decltype(history)::iterator pos = history.end(); + for (auto it = history.rbegin(); it != history.rend(); ++it) { + auto &frame = *it; + if (frame->getTrackType() != TrackVideo || (!frame->configFrame() && !frame->keyFrame())) { + continue; + } + // 如果视频关键帧到末尾的时长超过一定的时间,那前面的数据应该全部删除 + if (frame->dts() + back_time_ms < now_dts) { + pos = it.base(); + --pos; + break; + } + } + if (pos != history.end()) { + // 移除前面过多的数据 + TraceL << "clear history video: " << history.front()->dts() << " -> " << (*pos)->dts(); + history.erase(history.begin(), pos); + } + if (!history.empty()) { + auto &front = history.front(); + InfoL << "start record: " << path + << ", start_dts: " << front->dts() << ", key_frame: " << front->keyFrame() << ", config_frame: " << front->configFrame() + << ", now_dts: " << now_dts; + } + + for (auto &frame : history) { + muxer->inputFrame(frame); + } + } + + auto reader = _ring->attach(MultiMediaSourceMuxer::getOwnerPoller(MediaSource::NullMediaSource()), false); + uint64_t now_dts = 0; + int selected_index = -1; + reader->setReadCB([muxer, now_dts, selected_index, forward_time_ms, reader, path](const Frame::Ptr &frame) mutable { + // 循环引用自身 + if (!now_dts) { + now_dts = frame->dts(); + selected_index = frame->getIndex(); + } + if (frame->getIndex() == selected_index && now_dts + forward_time_ms < frame->dts()) { + InfoL << "stop record: " << path << ", end dts: " << frame->dts(); + WorkThreadPool::Instance().getPoller()->async([muxer]() { muxer->closeMP4(); }); + reader = nullptr; + return; + } + muxer->inputFrame(frame); + }); + std::weak_ptr weak_reader = reader; + reader->setDetachCB([weak_reader]() { + if (auto strong_reader = weak_reader.lock()) { + // 防止循环引用 + strong_reader->setReadCB(nullptr); + } + }); + + return path; +#endif +} + // 此函数可能跨线程调用 [AUTO-TRANSLATED:e8c5f74d] // This function may be called across threads bool MultiMediaSourceMuxer::isRecording(MediaSource &sender, Recorder::type type) { diff --git a/src/Common/MultiMediaSourceMuxer.h b/src/Common/MultiMediaSourceMuxer.h index 58de6a06..73a78b06 100644 --- a/src/Common/MultiMediaSourceMuxer.h +++ b/src/Common/MultiMediaSourceMuxer.h @@ -122,6 +122,15 @@ public: */ bool setupRecord(MediaSource &sender, Recorder::type type, bool start, const std::string &custom_path, size_t max_second) override; + /** + * 开始录制mp4 + * @param file_path mp4相对路径 + * @param back_time_ms 回溯录制时长 + * @param forward_time_ms 后续录制时长 + * @return 录制文件绝对路径 + */ + std::string startRecord(const std::string &file_path, uint32_t back_time_ms, uint32_t forward_time_ms); + /** * 获取录制状态 * @param type 录制类型