From c3c0fb4448f7a7f754113fabf8c28ced04171606 Mon Sep 17 00:00:00 2001 From: XiaoYan Lin Date: Wed, 1 Apr 2026 20:42:32 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81rtsp=E5=9B=9E=E6=94=BE?= =?UTF-8?q?=E6=8E=A7=E5=88=B6=20(#4691)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 目前对接过很多第三方系统(海康ISC、大华ICC、华为IVS、中维等)都支持rtsp回放,觉得有必要支持该功能 --- server/WebApi.cpp | 83 ++++++++++++++++++++++++++++++++++++++++ src/Rtsp/Rtsp.cpp | 24 ++++++++---- src/Rtsp/Rtsp.h | 3 ++ src/Rtsp/RtspPlayer.cpp | 74 +++++++++++++++++++++++++++++++++-- src/Rtsp/RtspPlayer.h | 6 +++ src/Rtsp/RtspPlayerImp.h | 8 +++- 6 files changed, 186 insertions(+), 12 deletions(-) diff --git a/server/WebApi.cpp b/server/WebApi.cpp index d99186a2..40c03181 100755 --- a/server/WebApi.cpp +++ b/server/WebApi.cpp @@ -2564,6 +2564,89 @@ void installWebApi() { invoker(200, headerOut, val.toStyledString()); }); #endif + + // 设置流播放速度 + // Set stream playback speed + api_regist("/index/api/setStreamSpeed", [](API_ARGS_JSON_ASYNC) { + CHECK_SECRET(); + CHECK_ARGS("vhost", "app", "stream", "speed"); + + std::string vhost = allArgs["vhost"]; + std::string app = allArgs["app"]; + std::string stream = allArgs["stream"]; + float speed = allArgs["speed"].as(); + + auto tuple = MediaTuple { vhost, app, stream, "" }; + std::string key = tuple.shortUrl(); + + auto player_proxy = s_player_proxy.find(key); + if (!player_proxy) { + throw ApiRetException("can not find the stream proxy", API::NotFound); + } + + player_proxy->getPoller()->async([=]() mutable { + player_proxy->MediaPlayer::speed(speed); + val["result"] = 0; + val["msg"] = "success"; + val["code"] = API::Success; + invoker(200, headerOut, val.toStyledString()); + }); + }); + + // 暂停/恢复流播放 + // Pause/Resume stream playback + api_regist("/index/api/pauseStream", [](API_ARGS_JSON_ASYNC) { + CHECK_SECRET(); + CHECK_ARGS("vhost", "app", "stream"); + + std::string vhost = allArgs["vhost"]; + std::string app = allArgs["app"]; + std::string stream = allArgs["stream"]; + + auto tuple = MediaTuple { vhost, app, stream, "" }; + std::string key = tuple.shortUrl(); + + auto player_proxy = s_player_proxy.find(key); + if (!player_proxy) { + throw ApiRetException("can not find the stream proxy", API::NotFound); + } + + player_proxy->getPoller()->async([=]() mutable { + player_proxy->MediaPlayer::pause(true); + val["result"] = 0; + val["msg"] = "success"; + val["code"] = API::Success; + invoker(200, headerOut, val.toStyledString()); + }); + }); + + // 跳转到指定位置 + // Seek to specified position + api_regist("/index/api/seekStream", [](API_ARGS_JSON_ASYNC) { + CHECK_SECRET(); + CHECK_ARGS("vhost", "app", "stream"); + + std::string vhost = allArgs["vhost"]; + std::string app = allArgs["app"]; + std::string stream = allArgs["stream"]; + uint32_t pos = allArgs["position"].as(); + + auto tuple = MediaTuple { vhost, app, stream, "" }; + std::string key = tuple.shortUrl(); + + auto player_proxy = s_player_proxy.find(key); + if (!player_proxy) { + throw ApiRetException("can not find the stream proxy", API::NotFound); + } + + player_proxy->getPoller()->async([=]() mutable { + player_proxy->MediaPlayer::seekTo(pos); + val["result"] = 0; + val["msg"] = "success"; + val["code"] = API::Success; + invoker(200, headerOut, val.toStyledString()); + }); + }); } void unInstallWebApi(){ diff --git a/src/Rtsp/Rtsp.cpp b/src/Rtsp/Rtsp.cpp index c320fa8a..7787d635 100644 --- a/src/Rtsp/Rtsp.cpp +++ b/src/Rtsp/Rtsp.cpp @@ -235,15 +235,25 @@ void SdpParser::load(const string &sdp) { auto &track = *track_ptr; auto it = track._attr.find("range"); if (it != track._attr.end()) { - char name[16] = { 0 }, start[16] = { 0 }, end[16] = { 0 }; - int ret = sscanf(it->second.data(), "%15[^=]=%15[^-]-%15s", name, start, end); + char name[16] = { 0 }, start[17] = { 0 }, end[17] = { 0 }; + int ret = sscanf(it->second.data(), "%15[^=]=%16[^-]-%16s", name, start, end); if (3 == ret || 2 == ret) { - if (strcmp(start, "now") == 0) { - strcpy(start, "0"); + // 保存 range 类型 + track._range_type = name; + if (strcmp(name, "clock") == 0) { + // clock 格式:clock=20251123T000000Z-20251124T000000Z + track._range_start_str = start; + track._range_end_str = end; + // 对于 clock 格式,不解析为数值 + } else { + // npt 格式或其他格式 + if (strcmp(start, "now") == 0) { + strcpy(start, "0"); + } + track._start = (float)atof(start); + track._end = (float)atof(end); + track._duration = track._end - track._start; } - track._start = (float)atof(start); - track._end = (float)atof(end); - track._duration = track._end - track._start; } } diff --git a/src/Rtsp/Rtsp.h b/src/Rtsp/Rtsp.h index dbb494c1..4f9cd137 100644 --- a/src/Rtsp/Rtsp.h +++ b/src/Rtsp/Rtsp.h @@ -237,6 +237,9 @@ public: float _duration = 0; float _start = 0; float _end = 0; + std::string _range_type; // 新增:保存 range 类型,如 "npt" 或 "clock" + std::string _range_start_str; // 新增:保存原始 range start 字符串(用于 clock 格式) + std::string _range_end_str; // 新增:保存原始 range end 字符串(用于 clock 格式) std::map _other; std::multimap _attr; diff --git a/src/Rtsp/RtspPlayer.cpp b/src/Rtsp/RtspPlayer.cpp index c1b668f7..dbf36af4 100644 --- a/src/Rtsp/RtspPlayer.cpp +++ b/src/Rtsp/RtspPlayer.cpp @@ -21,6 +21,12 @@ #include #include #include +#include +#include + +#if defined(_WIN32) +#include "Util/strptime_win.h" +#endif using namespace toolkit; using namespace std; @@ -212,6 +218,20 @@ void RtspPlayer::handleResDESCRIBE(const Parser &parser) { // Parse SDP SdpParser sdpParser(parser.content()); + // 保存 range 信息(从第一个 track 获取) + auto tracks = sdpParser.getAvailableTrack(); + if (!tracks.empty()) { + auto title_track = sdpParser.getTrack(TrackTitle); + if (title_track && !title_track->_range_type.empty()) { + _range_type = title_track->_range_type; + _range_start_str = title_track->_range_start_str; + _range_end_str = title_track->_range_end_str; + } else if (!tracks.empty() && !tracks[0]->_range_type.empty()) { + _range_type = tracks[0]->_range_type; + _range_start_str = tracks[0]->_range_start_str; + _range_end_str = tracks[0]->_range_end_str; + } + } _control_url = sdpParser.getControlUrl(_content_base); string sdp; @@ -468,10 +488,52 @@ void RtspPlayer::sendPause(int type, uint32_t seekMS) { // Start or pause RTSP switch (type) { case type_pause: sendRtspRequest("PAUSE", _control_url, {}); break; - case type_play: - case type_seek: - sendRtspRequest("PLAY", _control_url, { "Range", StrPrinter << "npt=" << setiosflags(ios::fixed) << setprecision(2) << seekMS / 1000.0 << "-" }); - break; + case type_play: sendRtspRequest("PLAY", _content_base); break; + case type_seek: { + std::string range_header; + if (_range_type == "clock" && !_range_start_str.empty()) { + // clock 格式:需要计算新的时间 + // 解析起始时间:20251123T000000Z + struct tm tm_start; + const char *start_str = _range_start_str.c_str(); + if (strptime(start_str, "%Y%m%dT%H%M%SZ", &tm_start) != nullptr) { + // 转换为 time_t,加上 seekMS 毫秒 +#if defined(_WIN32) + time_t start_time = _mkgmtime(&tm_start); +#else + time_t start_time = timegm(&tm_start); +#endif + start_time += seekMS / 1000; // 加上秒数 + + // 格式化新的时间 + struct tm tm_new; +#if defined(_WIN32) + auto gmtime_ret = gmtime_s(&tm_new, &start_time); + if (gmtime_ret == 0) +#else + auto gmtime_ret = gmtime_r(&start_time, &tm_new); + if (gmtime_ret != nullptr) +#endif + { + char new_time[32]; + strftime(new_time, sizeof(new_time), "%Y%m%dT%H%M%SZ", &tm_new); + + // 构建 Range 头 + range_header = StrPrinter << "clock=" << new_time << "-" << _range_end_str; + } else { + // 解析失败,回退到 npt 格式 + range_header = StrPrinter << "npt=" << setiosflags(ios::fixed) << setprecision(2) << seekMS / 1000.0 << "-"; + } + } else { + // 解析失败,回退到 npt 格式 + range_header = StrPrinter << "npt=" << setiosflags(ios::fixed) << setprecision(2) << seekMS / 1000.0 << "-"; + } + } else { + // npt 格式或其他格式 + range_header = StrPrinter << "npt=" << setiosflags(ios::fixed) << setprecision(2) << seekMS / 1000.0 << "-"; + } + sendRtspRequest("PLAY", _control_url, { "Range", range_header }); + } break; case type_speed: speed(_speed); break; default: WarnL << "unknown type : " << type; @@ -488,6 +550,10 @@ void RtspPlayer::speed(float speed) { sendRtspRequest("PLAY", _control_url, { "Scale", StrPrinter << speed }); } +void RtspPlayer::seekTo(uint32_t pos) { + seekToMilliSecond(pos * 1000); +} + void RtspPlayer::handleResPAUSE(const Parser &parser, int type) { if (parser.status() != "200") { switch (type) { diff --git a/src/Rtsp/RtspPlayer.h b/src/Rtsp/RtspPlayer.h index 9c98714e..6103d8f9 100644 --- a/src/Rtsp/RtspPlayer.h +++ b/src/Rtsp/RtspPlayer.h @@ -36,6 +36,7 @@ public: void play(const std::string &strUrl) override; void pause(bool pause) override; void speed(float speed) override; + void seekTo(uint32_t pos) override; // 新增 void teardown() override; float getPacketLossRate(TrackType type) const override; @@ -181,6 +182,11 @@ private: uint32_t _cseq_send = 1; std::string _content_base; std::string _control_url; + + std::string _range_type; // 新增:保存 range 类型 + std::string _range_start_str; // 新增:保存 clock 格式的起始时间 + std::string _range_end_str; // 新增:保存 clock 格式的结束时间 + protected: Rtsp::eRtpType _rtp_type = Rtsp::RTP_TCP; diff --git a/src/Rtsp/RtspPlayerImp.h b/src/Rtsp/RtspPlayerImp.h index 0bb7b474..4f65e582 100644 --- a/src/Rtsp/RtspPlayerImp.h +++ b/src/Rtsp/RtspPlayerImp.h @@ -51,7 +51,13 @@ public: } void seekTo(uint32_t seekPos) override { - uint32_t pos = MAX(float(0), MIN(seekPos, getDuration())) * 1000; + uint32_t pos = seekPos * 1000; + // 如果是点播流(有时长),限制在有效范围内 + // If it's a VOD stream (has duration), limit to valid range + float duration = getDuration(); + if (duration > 0) { + pos = MAX(float(0), MIN(seekPos, getDuration())) * 1000; + } seekToMilliSecond(pos); }