Compare commits

...

3 Commits

Author SHA1 Message Date
xia-chu
22a8a9a2ec 更新postman接口文档
Some checks are pending
Android / build (push) Waiting to run
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
Docker / build (push) Waiting to run
Linux / build (push) Waiting to run
Linux_Python / build (push) Waiting to run
macOS / build (push) Waiting to run
macOS_Python / build (push) Waiting to run
Windows / build (push) Waiting to run
Windows_Python / build (push) Waiting to run
2026-03-17 20:12:49 +08:00
xia-chu
3b54168b44 listStreamProxy接口支持返回track、status_str信息 2026-03-17 19:43:50 +08:00
xia-chu
4e170e9281 addStreamProxy新增force,支持强制重试拉流 2026-03-17 19:33:43 +08:00
7 changed files with 391 additions and 68 deletions

View File

@ -46,7 +46,8 @@
"path": [
"index",
"api",
"getApiList"
"stack",
"stop"
],
"query": [
{
@ -56,7 +57,44 @@
},
{
"key": "id",
"value": "stack_test"
"value": "stack_test",
"description": "多屏拼接id"
}
]
}
},
"response": []
},
{
"name": "重置多屏拼接(stack/reset)",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\r\n \"gapv\": 0.002,\r\n \"gaph\": 0.001,\r\n \"width\": 1920,\r\n \"url\": [\r\n [\r\n \"rtsp://kkem.me/live/test3\",\r\n \"rtsp://kkem.me/live/cy1\",\r\n \"rtsp://kkem.me/live/cy1\",\r\n \"rtsp://kkem.me/live/cy2\"\r\n ],\r\n [\r\n \"rtsp://kkem.me/live/cy1\",\r\n \"rtsp://kkem.me/live/cy5\",\r\n \"rtsp://kkem.me/live/cy3\",\r\n \"rtsp://kkem.me/live/cy4\"\r\n ],\r\n [\r\n \"rtsp://kkem.me/live/cy5\",\r\n \"rtsp://kkem.me/live/cy6\",\r\n \"rtsp://kkem.me/live/cy7\",\r\n \"rtsp://kkem.me/live/cy8\"\r\n ],\r\n [\r\n \"rtsp://kkem.me/live/cy9\",\r\n \"rtsp://kkem.me/live/cy10\",\r\n \"rtsp://kkem.me/live/cy11\",\r\n \"rtsp://kkem.me/live/cy12\"\r\n ]\r\n ],\r\n \"id\": \"89\",\r\n \"row\": 4,\r\n \"col\": 4,\r\n \"height\": 1080,\r\n \"span\": [\r\n [\r\n [\r\n 0,\r\n 0\r\n ],\r\n [\r\n 1,\r\n 1\r\n ]\r\n ],\r\n [\r\n [\r\n 3,\r\n 0\r\n ],\r\n [\r\n 3,\r\n 1\r\n ]\r\n ],\r\n [\r\n [\r\n 2,\r\n 3\r\n ],\r\n [\r\n 3,\r\n 3\r\n ]\r\n ]\r\n ]\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{ZLMediaKit_URL}}/index/api/stack/reset?secret={{ZLMediaKit_secret}}",
"host": [
"{{ZLMediaKit_URL}}"
],
"path": [
"index",
"api",
"stack",
"reset"
],
"query": [
{
"key": "secret",
"value": "{{ZLMediaKit_secret}}",
"description": "api操作密钥(配置文件配置)"
}
]
}
@ -875,6 +913,12 @@
"description": "推流重试次数,不传此参数或传值<=0时则无限重试",
"disabled": true
},
{
"key": "force",
"value": null,
"description": "是否强制添加代理默认0设置为1时如果拉流失败也会不断重试",
"disabled": true
},
{
"key": "latency",
"value": null,
@ -1226,19 +1270,19 @@
"response": []
},
{
"name": "获取流信息(getMp4RecordFile)",
"name": "获取录像文件列表(getMP4RecordFile)",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{ZLMediaKit_URL}}/index/api/getMp4RecordFile?secret={{ZLMediaKit_secret}}&vhost={{defaultVhost}}&app=proxy&stream=2&customized_path=/www&period=2020-05-26",
"raw": "{{ZLMediaKit_URL}}/index/api/getMP4RecordFile?secret={{ZLMediaKit_secret}}&vhost={{defaultVhost}}&app=proxy&stream=2&customized_path=/www&period=2020-05-26",
"host": [
"{{ZLMediaKit_URL}}"
],
"path": [
"index",
"api",
"getMp4RecordFile"
"getMP4RecordFile"
],
"query": [
{
@ -2951,6 +2995,246 @@
},
"response": []
},
{
"name": "下载程序二进制文件(downloadBin)",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{ZLMediaKit_URL}}/index/api/downloadBin?secret={{ZLMediaKit_secret}}",
"host": [
"{{ZLMediaKit_URL}}"
],
"path": [
"index",
"api",
"downloadBin"
],
"query": [
{
"key": "secret",
"value": "{{ZLMediaKit_secret}}",
"description": "api操作密钥(配置文件配置)"
}
]
}
},
"response": []
},
{
"name": "WebRTC交互(webrtc)",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": ""
},
"url": {
"raw": "{{ZLMediaKit_URL}}/index/api/webrtc?secret={{ZLMediaKit_secret}}&type=play&app=live&stream=test",
"host": [
"{{ZLMediaKit_URL}}"
],
"path": [
"index",
"api",
"webrtc"
],
"query": [
{
"key": "type",
"value": "play",
"description": "webrtc类型play为播放push为推流echo为回显测试"
},
{
"key": "app",
"value": "live",
"description": "应用名"
},
{
"key": "stream",
"value": "test",
"description": "流id"
},
{
"key": "preferred_tcp",
"value": null,
"description": "是否webrtc over tcp优先模式",
"disabled": true
},
{
"key": "cand_udp",
"value": "test",
"description": "指定zlm服务器udp candidate",
"disabled": true
},
{
"key": "cand_tcp",
"value": null,
"description": "指定zlm服务器tcp candidate",
"disabled": true
}
]
},
"description": "WebRTC交互接口body为SDP offer"
},
"response": []
},
{
"name": "WebRTC-WHIP推流(whip)",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/sdp"
}
],
"body": {
"mode": "raw",
"raw": ""
},
"url": {
"raw": "{{ZLMediaKit_URL}}/index/api/whip?app=live&stream=test",
"host": [
"{{ZLMediaKit_URL}}"
],
"path": [
"index",
"api",
"whip"
],
"query": [
{
"key": "app",
"value": "live",
"description": "应用名"
},
{
"key": "stream",
"value": "test",
"description": "流id"
},
{
"key": "preferred_tcp",
"value": null,
"description": "是否webrtc over tcp优先模式",
"disabled": true
},
{
"key": "cand_udp",
"value": "test",
"description": "指定zlm服务器udp candidate",
"disabled": true
},
{
"key": "cand_tcp",
"value": null,
"description": "指定zlm服务器tcp candidate",
"disabled": true
}
]
},
"description": "WebRTC WHIP标准推流接口body为SDP offer"
},
"response": []
},
{
"name": "WebRTC-WHEP播放(whep)",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/sdp"
}
],
"body": {
"mode": "raw",
"raw": ""
},
"url": {
"raw": "{{ZLMediaKit_URL}}/index/api/whep?app=live&stream=test",
"host": [
"{{ZLMediaKit_URL}}"
],
"path": [
"index",
"api",
"whep"
],
"query": [
{
"key": "app",
"value": "live",
"description": "应用名"
},
{
"key": "stream",
"value": "test",
"description": "流id"
},
{
"key": "preferred_tcp",
"value": null,
"description": "是否webrtc over tcp优先模式",
"disabled": true
},
{
"key": "cand_udp",
"value": "test",
"description": "指定zlm服务器udp candidate",
"disabled": true
},
{
"key": "cand_tcp",
"value": null,
"description": "指定zlm服务器tcp candidate",
"disabled": true
}
]
},
"description": "WebRTC WHEP标准播放接口body为SDP offer"
},
"response": []
},
{
"name": "WebRTC-删除连接(delete_webrtc)",
"request": {
"method": "DELETE",
"header": [],
"url": {
"raw": "{{ZLMediaKit_URL}}/index/api/delete_webrtc?id=&token=",
"host": [
"{{ZLMediaKit_URL}}"
],
"path": [
"index",
"api",
"delete_webrtc"
],
"query": [
{
"key": "id",
"value": "",
"description": "WebRTC连接的唯一标识"
},
{
"key": "token",
"value": "",
"description": "删除操作的验证token"
}
]
},
"description": "删除WebRTC连接需要使用DELETE方法。id和token由whip/whep接口返回的Location头中获取。"
},
"response": []
},
{
"name": "登录(login)",
"request": {

View File

@ -379,10 +379,53 @@ Value ToJson(const PusherProxy::Ptr& p) {
return item;
}
Json::Value dumpTracks(const std::vector<Track::Ptr> &tracks) {
Json::Value ret(arrayValue);
for (auto &track : tracks) {
Value obj;
auto codec_type = track->getTrackType();
obj["codec_id"] = track->getCodecId();
obj["codec_id_name"] = track->getCodecName();
obj["ready"] = track->ready();
obj["codec_type"] = codec_type;
obj["frames"] = track->getFrames();
obj["duration"] = track->getDuration();
switch (codec_type) {
case TrackAudio: {
auto audio_track = dynamic_pointer_cast<AudioTrack>(track);
obj["sample_rate"] = audio_track->getAudioSampleRate();
obj["channels"] = audio_track->getAudioChannel();
obj["sample_bit"] = audio_track->getAudioSampleBit();
break;
}
case TrackVideo: {
auto video_track = dynamic_pointer_cast<VideoTrack>(track);
obj["width"] = video_track->getVideoWidth();
obj["height"] = video_track->getVideoHeight();
obj["key_frames"] = video_track->getVideoKeyFrames();
int gop_size = video_track->getVideoGopSize();
int gop_interval_ms = video_track->getVideoGopInterval();
float fps = video_track->getVideoFps();
if (fps <= 1 && gop_interval_ms) {
fps = gop_size * 1000.0 / gop_interval_ms;
}
obj["fps"] = round(fps);
obj["gop_size"] = gop_size;
obj["gop_interval_ms"] = gop_interval_ms;
break;
}
default: break;
}
ret.append(obj);
}
return ret;
}
Value ToJson(const PlayerProxy::Ptr& p) {
Value item;
item["url"] = p->getUrl();
item["status"] = p->getStatus();
item["status_str"] = p->getStatusStr();
item["liveSecs"] = p->getLiveSecs();
item["rePullCount"] = p->getRePullCount();
item["totalReaderCount"] = p->totalReaderCount();
@ -390,10 +433,11 @@ Value ToJson(const PlayerProxy::Ptr& p) {
item["totalBytes"] = (Json::UInt64) p->getRecvTotalBytes();
dumpMediaTuple(p->getMediaTuple(), item["src"]);
item["tracks"] = dumpTracks(p->getTracks(false));
return item;
}
Value makeMediaSourceJson(MediaSource &media){
Value makeMediaSourceJson(MediaSource &media) {
Value item;
item["schema"] = media.getSchema();
dumpMediaTuple(media.getMediaTuple(), item);
@ -421,17 +465,13 @@ Value makeMediaSourceJson(MediaSource &media){
auto current_thread = false;
try { current_thread = media.getOwnerPoller()->isCurrentThread();} catch (...) {}
float last_loss = -1;
for(auto &track : media.getTracks(false)){
Value obj;
auto codec_type = track->getTrackType();
obj["codec_id"] = track->getCodecId();
obj["codec_id_name"] = track->getCodecName();
obj["ready"] = track->ready();
obj["codec_type"] = codec_type;
if (current_thread) {
auto tracks = dumpTracks(media.getTracks(false));
if (current_thread) {
for (auto &obj : tracks) {
// rtp推流只有一个统计器但是可能有多个track如果短时间多次获取间隔丢包率第二次会获取为-1 [AUTO-TRANSLATED:5bfbc951]
// RTP push stream has only one statistics, but may have multiple tracks. If you get the interval packet loss rate multiple times in a short time, the second time will get -1
auto loss = media.getLossRate(codec_type);
// RTP push stream has only one statistics, but may have multiple tracks. If you get the interval packet loss rate multiple times in a short time,
// the second time will get -1
auto loss = media.getLossRate(getTrackType(static_cast<CodecId>(obj["codec_type"].asInt())));
if (loss == -1) {
loss = last_loss;
} else {
@ -439,37 +479,8 @@ Value makeMediaSourceJson(MediaSource &media){
}
obj["loss"] = loss;
}
obj["frames"] = track->getFrames();
obj["duration"] = track->getDuration();
switch(codec_type){
case TrackAudio : {
auto audio_track = dynamic_pointer_cast<AudioTrack>(track);
obj["sample_rate"] = audio_track->getAudioSampleRate();
obj["channels"] = audio_track->getAudioChannel();
obj["sample_bit"] = audio_track->getAudioSampleBit();
break;
}
case TrackVideo : {
auto video_track = dynamic_pointer_cast<VideoTrack>(track);
obj["width"] = video_track->getVideoWidth();
obj["height"] = video_track->getVideoHeight();
obj["key_frames"] = video_track->getVideoKeyFrames();
int gop_size = video_track->getVideoGopSize();
int gop_interval_ms = video_track->getVideoGopInterval();
float fps = video_track->getVideoFps();
if (fps <= 1 && gop_interval_ms) {
fps = gop_size * 1000.0 / gop_interval_ms;
}
obj["fps"] = round(fps);
obj["gop_size"] = gop_size;
obj["gop_interval_ms"] = gop_interval_ms;
break;
}
default:
break;
}
item["tracks"].append(obj);
}
item["tracks"] = std::move(tracks);
return item;
}
@ -583,7 +594,7 @@ void getStatisticJson(const function<void(Value &val)> &cb) {
#endif
}
void addStreamProxy(const MediaTuple &tuple, const string &url, int retry_count,
void addStreamProxy(const MediaTuple &tuple, const string &url, int retry_count, bool force,
const ProtocolOption &option, int rtp_type, float timeout_sec, const mINI &args,
const function<void(const SockException &ex, const string &key)> &cb) {
auto key = tuple.shortUrl();
@ -615,11 +626,18 @@ void addStreamProxy(const MediaTuple &tuple, const string &url, int retry_count,
// 开始播放,如果播放失败或者播放中止,将会自动重试若干次,默认一直重试 [AUTO-TRANSLATED:ac8499e5]
// Start playing. If playback fails or is stopped, it will automatically retry several times, by default it will retry indefinitely
player->setPlayCallbackOnce([cb, key](const SockException &ex) {
if (ex) {
s_player_proxy.erase(key);
player->setPlayCallbackOnce([cb, key, force](const SockException &ex) {
if (force) {
// 强制添加成功
cb(SockException(), key);
} else {
// 非强制添加
if (ex) {
// 失败则移除记录
s_player_proxy.erase(key);
}
cb(ex, key);
}
cb(ex, key);
});
// 被主动关闭拉流 [AUTO-TRANSLATED:41a19476]
@ -1264,6 +1282,7 @@ void installWebApi() {
addStreamProxy(tuple,
allArgs["url"],
retry_count,
allArgs["force"],
option,
allArgs["rtp_type"],
allArgs["timeout_sec"],

View File

@ -248,7 +248,7 @@ uint16_t openRtpServer(uint16_t local_port, const mediakit::MediaTuple &tuple, i
Json::Value makeMediaSourceJson(mediakit::MediaSource &media);
ApiArgsType getAllArgs(const mediakit::Parser &parser);
void getStatisticJson(const std::function<void(Json::Value &val)> &cb);
void addStreamProxy(const mediakit::MediaTuple &tuple, const std::string &url, int retry_count,
void addStreamProxy(const mediakit::MediaTuple &tuple, const std::string &url, int retry_count, bool force,
const mediakit::ProtocolOption &option, int rtp_type, float timeout_sec, const toolkit::mINI &args,
const std::function<void(const toolkit::SockException &ex, const std::string &key)> &cb);

View File

@ -321,7 +321,7 @@ static void pullStreamFromOrigin(const vector<string> &urls, size_t index, size_
option.enable_hls = option.enable_hls || (args.schema == HLS_SCHEMA);
option.enable_mp4 = false;
addStreamProxy(args, url, retry_count, option, Rtsp::RTP_TCP, timeout_sec, mINI{}, [=](const SockException &ex, const string &key) mutable {
addStreamProxy(args, url, retry_count, false, option, Rtsp::RTP_TCP, timeout_sec, mINI{}, [=](const SockException &ex, const string &key) mutable {
if (!ex) {
return;
}

View File

@ -46,6 +46,10 @@ void TsPlayerImp::onPlayResult(const SockException &ex) {
}
void TsPlayerImp::onShutdown(const SockException &ex) {
if (!ex) {
// http-ts拉流如果为eof正常断开那么强制为异常状态
const_cast<SockException &>(ex).reset(Err_other, ex.what());
}
while (_demuxer) {
try {
// shared_from_this()可能抛异常 [AUTO-TRANSLATED:6af9bd3c]

View File

@ -32,7 +32,7 @@ PlayerProxy::PlayerProxy(
setOnClose(nullptr);
setOnConnect(nullptr);
setOnDisconnect(nullptr);
_reconnect_delay_min = reconnect_delay_min > 0 ? reconnect_delay_min : 2;
_reconnect_delay_max = reconnect_delay_max > 0 ? reconnect_delay_max : 60;
_reconnect_delay_step = reconnect_delay_step > 0 ? reconnect_delay_step : 3;
@ -51,15 +51,14 @@ void PlayerProxy::setOnClose(function<void(const SockException &ex)> cb) {
}
void PlayerProxy::setOnDisconnect(std::function<void()> cb) {
_on_disconnect = cb ? std::move(cb) : [] () {};
_on_disconnect = cb ? std::move(cb) : []() {};
}
void PlayerProxy::setOnConnect(std::function<void(const TranslationInfo&)> cb) {
_on_connect = cb ? std::move(cb) : [](const TranslationInfo&) {};
void PlayerProxy::setOnConnect(std::function<void(const TranslationInfo &)> cb) {
_on_connect = cb ? std::move(cb) : [](const TranslationInfo &) {};
}
void PlayerProxy::setTranslationInfo()
{
void PlayerProxy::setTranslationInfo() {
_transtalion_info.byte_speed = _media_src ? _media_src->getBytesSpeed() : -1;
_transtalion_info.start_time_stamp = _media_src ? _media_src->getCreateStamp() : 0;
_transtalion_info.stream_info.clear();
@ -72,22 +71,21 @@ void PlayerProxy::setTranslationInfo()
back.codec_type = track->getTrackType();
back.codec_name = track->getCodecName();
switch (back.codec_type) {
case TrackAudio : {
case TrackAudio: {
auto audio_track = dynamic_pointer_cast<AudioTrack>(track);
back.audio_sample_rate = audio_track->getAudioSampleRate();
back.audio_channel = audio_track->getAudioChannel();
back.audio_sample_bit = audio_track->getAudioSampleBit();
break;
}
case TrackVideo : {
case TrackVideo: {
auto video_track = dynamic_pointer_cast<VideoTrack>(track);
back.video_width = video_track->getVideoWidth();
back.video_height = video_track->getVideoHeight();
back.video_fps = video_track->getVideoFps();
break;
}
default:
break;
default: break;
}
}
}
@ -112,6 +110,7 @@ void PlayerProxy::play(const string &strUrlTmp) {
}
if (err) {
NOTICE_EMIT(BroadcastPlayerProxyFailedArgs, Broadcast::kBroadcastPlayerProxyFailed, *strongSelf, err);
strongSelf->_status = std::make_shared<std::string>(std::string("play failed: ") + err.what());
}
if (strongSelf->_on_play) {
strongSelf->_on_play(err);
@ -120,7 +119,8 @@ void PlayerProxy::play(const string &strUrlTmp) {
if (!err) {
// 取消定时器,避免hls拉流索引文件因为网络波动失败重连成功后出现循环重试的情况 [AUTO-TRANSLATED:91e5f0c8]
// Cancel the timer to avoid the situation where the hls stream index file fails to reconnect due to network fluctuations and then retries in a loop after successful reconnection
// Cancel the timer to avoid the situation where the hls stream index file fails to reconnect due to network fluctuations and then retries in a loop
// after successful reconnection
strongSelf->_timer.reset();
strongSelf->_live_ticker.resetTime();
strongSelf->_live_status = 0;
@ -129,9 +129,10 @@ void PlayerProxy::play(const string &strUrlTmp) {
*piFailedCnt = 0; // 连续播放失败次数清0
strongSelf->onPlaySuccess();
strongSelf->setTranslationInfo();
strongSelf->_on_connect(strongSelf->_transtalion_info);
strongSelf->_on_connect(strongSelf->_transtalion_info);
InfoL << "play " << strUrlTmp << " success";
strongSelf->_status = std::make_shared<std::string>("playing");
} else if (*piFailedCnt < strongSelf->_retry_count || strongSelf->_retry_count < 0) {
// 播放失败,延时重试播放 [AUTO-TRANSLATED:d7537c9c]
// Play failed, retry playing with delay
@ -151,6 +152,11 @@ void PlayerProxy::play(const string &strUrlTmp) {
if (err) {
NOTICE_EMIT(BroadcastPlayerProxyFailedArgs, Broadcast::kBroadcastPlayerProxyFailed, *strongSelf, err);
}
if (strongSelf->_on_play) {
strongSelf->_on_play(err);
strongSelf->_on_play = nullptr;
}
strongSelf->_status = std::make_shared<std::string>(std::string("play shutdown: ") + err.what());
// 注销直接拉流代理产生的流:#532 [AUTO-TRANSLATED:c6343a3b]
// Unregister the stream generated by the direct stream proxy: #532
@ -190,8 +196,10 @@ void PlayerProxy::play(const string &strUrlTmp) {
}
});
try {
_status = std::make_shared<std::string>("connecting");
MediaPlayer::play(strUrlTmp);
} catch (std::exception &ex) {
_status = std::make_shared<std::string>(std::string("play failed: ") + ex.what());
ErrorL << ex.what();
onPlayResult(SockException(Err_other, ex.what()));
return;
@ -291,7 +299,7 @@ float PlayerProxy::getLossRate(MediaSource &sender, TrackType type) {
return getPacketLossRate(type);
}
toolkit::EventPoller::Ptr PlayerProxy::getOwnerPoller(MediaSource &sender) {
toolkit::EventPoller::Ptr PlayerProxy::getOwnerPoller(MediaSource &sender) {
return getPoller();
}
@ -314,10 +322,10 @@ void PlayerProxy::onPlaySuccess() {
// rtmp拉流代理 [AUTO-TRANSLATED:21173335]
// Rtmp stream proxy
if (reset_when_replay || !_muxer) {
auto old = _option.enable_rtmp;
auto old = _option.enable_rtmp;
_option.enable_rtmp = false;
_muxer = std::make_shared<MultiMediaSourceMuxer>(_tuple, getDuration(), _option);
_option.enable_rtmp = old;
_option.enable_rtmp = old;
}
} else {
// 其他拉流代理 [AUTO-TRANSLATED:e5f2e45d]
@ -362,6 +370,12 @@ void PlayerProxy::onPlaySuccess() {
int PlayerProxy::getStatus() {
return _live_status.load();
}
std::string PlayerProxy::getStatusStr() const {
auto status = _status;
return status ? *status : "unknown";
}
uint64_t PlayerProxy::getLiveSecs() {
if (_live_status == 0) {
return _live_secs + _live_ticker.elapsedTime() / 1000;

View File

@ -129,6 +129,7 @@ public:
int totalReaderCount();
int getStatus();
std::string getStatusStr() const;
uint64_t getLiveSecs();
uint64_t getRePullCount();
@ -155,6 +156,7 @@ private:
void setTranslationInfo();
private:
std::shared_ptr<std::string> _status;
int _retry_count;
int _reconnect_delay_min;
int _reconnect_delay_max;