mirror of
https://gitee.com/xia-chu/ZLMediaKit.git
synced 2026-05-21 17:17:49 +08:00
支持rtsp回放控制 (#4691)
Some checks failed
Android / build (push) Has been cancelled
CodeQL / Analyze (cpp) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Docker / build (push) Has been cancelled
Linux / build (push) Has been cancelled
Linux_Python / build (push) Has been cancelled
macOS / build (push) Has been cancelled
macOS_Python / build (push) Has been cancelled
Windows / build (push) Has been cancelled
Windows_Python / build (push) Has been cancelled
Some checks failed
Android / build (push) Has been cancelled
CodeQL / Analyze (cpp) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Docker / build (push) Has been cancelled
Linux / build (push) Has been cancelled
Linux_Python / build (push) Has been cancelled
macOS / build (push) Has been cancelled
macOS_Python / build (push) Has been cancelled
Windows / build (push) Has been cancelled
Windows_Python / build (push) Has been cancelled
目前对接过很多第三方系统(海康ISC、大华ICC、华为IVS、中维等)都支持rtsp回放,觉得有必要支持该功能
This commit is contained in:
parent
66b94b266c
commit
c3c0fb4448
@ -2564,6 +2564,89 @@ void installWebApi() {
|
|||||||
invoker(200, headerOut, val.toStyledString());
|
invoker(200, headerOut, val.toStyledString());
|
||||||
});
|
});
|
||||||
#endif
|
#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<float>();
|
||||||
|
|
||||||
|
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<uint32_t>();
|
||||||
|
|
||||||
|
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(){
|
void unInstallWebApi(){
|
||||||
|
|||||||
@ -235,15 +235,25 @@ void SdpParser::load(const string &sdp) {
|
|||||||
auto &track = *track_ptr;
|
auto &track = *track_ptr;
|
||||||
auto it = track._attr.find("range");
|
auto it = track._attr.find("range");
|
||||||
if (it != track._attr.end()) {
|
if (it != track._attr.end()) {
|
||||||
char name[16] = { 0 }, start[16] = { 0 }, end[16] = { 0 };
|
char name[16] = { 0 }, start[17] = { 0 }, end[17] = { 0 };
|
||||||
int ret = sscanf(it->second.data(), "%15[^=]=%15[^-]-%15s", name, start, end);
|
int ret = sscanf(it->second.data(), "%15[^=]=%16[^-]-%16s", name, start, end);
|
||||||
if (3 == ret || 2 == ret) {
|
if (3 == ret || 2 == ret) {
|
||||||
if (strcmp(start, "now") == 0) {
|
// 保存 range 类型
|
||||||
strcpy(start, "0");
|
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -237,6 +237,9 @@ public:
|
|||||||
float _duration = 0;
|
float _duration = 0;
|
||||||
float _start = 0;
|
float _start = 0;
|
||||||
float _end = 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<char, std::string> _other;
|
std::map<char, std::string> _other;
|
||||||
std::multimap<std::string, std::string> _attr;
|
std::multimap<std::string, std::string> _attr;
|
||||||
|
|||||||
@ -21,6 +21,12 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
#include <cstring>
|
||||||
|
#include <ctime>
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#include "Util/strptime_win.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
using namespace toolkit;
|
using namespace toolkit;
|
||||||
using namespace std;
|
using namespace std;
|
||||||
@ -212,6 +218,20 @@ void RtspPlayer::handleResDESCRIBE(const Parser &parser) {
|
|||||||
// Parse SDP
|
// Parse SDP
|
||||||
SdpParser sdpParser(parser.content());
|
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);
|
_control_url = sdpParser.getControlUrl(_content_base);
|
||||||
|
|
||||||
string sdp;
|
string sdp;
|
||||||
@ -468,10 +488,52 @@ void RtspPlayer::sendPause(int type, uint32_t seekMS) {
|
|||||||
// Start or pause RTSP
|
// Start or pause RTSP
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case type_pause: sendRtspRequest("PAUSE", _control_url, {}); break;
|
case type_pause: sendRtspRequest("PAUSE", _control_url, {}); break;
|
||||||
case type_play:
|
case type_play: sendRtspRequest("PLAY", _content_base); break;
|
||||||
case type_seek:
|
case type_seek: {
|
||||||
sendRtspRequest("PLAY", _control_url, { "Range", StrPrinter << "npt=" << setiosflags(ios::fixed) << setprecision(2) << seekMS / 1000.0 << "-" });
|
std::string range_header;
|
||||||
break;
|
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;
|
case type_speed: speed(_speed); break;
|
||||||
default:
|
default:
|
||||||
WarnL << "unknown type : " << type;
|
WarnL << "unknown type : " << type;
|
||||||
@ -488,6 +550,10 @@ void RtspPlayer::speed(float speed) {
|
|||||||
sendRtspRequest("PLAY", _control_url, { "Scale", StrPrinter << 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) {
|
void RtspPlayer::handleResPAUSE(const Parser &parser, int type) {
|
||||||
if (parser.status() != "200") {
|
if (parser.status() != "200") {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
|||||||
@ -36,6 +36,7 @@ public:
|
|||||||
void play(const std::string &strUrl) override;
|
void play(const std::string &strUrl) override;
|
||||||
void pause(bool pause) override;
|
void pause(bool pause) override;
|
||||||
void speed(float speed) override;
|
void speed(float speed) override;
|
||||||
|
void seekTo(uint32_t pos) override; // 新增
|
||||||
void teardown() override;
|
void teardown() override;
|
||||||
float getPacketLossRate(TrackType type) const override;
|
float getPacketLossRate(TrackType type) const override;
|
||||||
|
|
||||||
@ -181,6 +182,11 @@ private:
|
|||||||
uint32_t _cseq_send = 1;
|
uint32_t _cseq_send = 1;
|
||||||
std::string _content_base;
|
std::string _content_base;
|
||||||
std::string _control_url;
|
std::string _control_url;
|
||||||
|
|
||||||
|
std::string _range_type; // 新增:保存 range 类型
|
||||||
|
std::string _range_start_str; // 新增:保存 clock 格式的起始时间
|
||||||
|
std::string _range_end_str; // 新增:保存 clock 格式的结束时间
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
Rtsp::eRtpType _rtp_type = Rtsp::RTP_TCP;
|
Rtsp::eRtpType _rtp_type = Rtsp::RTP_TCP;
|
||||||
|
|
||||||
|
|||||||
@ -51,7 +51,13 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
void seekTo(uint32_t seekPos) override {
|
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);
|
seekToMilliSecond(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user