ZLMediaKit/tests/test_server.cpp

391 lines
20 KiB
C++
Raw Normal View History

2019-03-28 14:47:16 +08:00
/*
2023-12-09 16:23:51 +08:00
* Copyright (c) 2016-present The ZLMediaKit project authors. All Rights Reserved.
2017-09-27 16:20:30 +08:00
*
2023-12-09 16:23:51 +08:00
* This file is part of ZLMediaKit(https://github.com/ZLMediaKit/ZLMediaKit).
2017-09-27 16:20:30 +08:00
*
2023-12-09 16:23:51 +08:00
* Use of this source code is governed by MIT-like license that can be found in the
2020-04-04 20:30:09 +08:00
* LICENSE file in the root of the source tree. All contributing project authors
* may be found in the AUTHORS file in the root of the source tree.
2017-09-27 16:20:30 +08:00
*/
2017-04-05 09:39:16 +08:00
2017-08-09 18:39:30 +08:00
#include <map>
2017-04-05 09:39:16 +08:00
#include <signal.h>
#include <iostream>
2017-05-03 00:12:51 +08:00
2018-12-20 10:31:31 +08:00
#include "Util/MD5.h"
2017-04-25 11:35:41 +08:00
#include "Util/logger.h"
2018-12-20 10:31:31 +08:00
#include "Util/SSLBox.h"
2017-04-25 11:35:41 +08:00
#include "Util/onceToken.h"
#include "Network/TcpServer.h"
#include "Poller/EventPoller.h"
2018-12-20 10:31:31 +08:00
#include "Common/config.h"
#include "Rtsp/UDPServer.h"
#include "Rtsp/RtspSession.h"
#include "Rtmp/RtmpSession.h"
#include "Shell/ShellSession.h"
2019-06-28 17:30:13 +08:00
#include "Rtmp/FlvMuxer.h"
2018-10-29 09:54:35 +08:00
#include "Player/PlayerProxy.h"
2018-12-20 10:31:31 +08:00
#include "Http/WebSocketSession.h"
2017-08-09 18:39:30 +08:00
2017-04-05 09:39:16 +08:00
using namespace std;
2018-10-24 17:17:55 +08:00
using namespace toolkit;
using namespace mediakit;
2017-12-10 01:34:43 +08:00
2018-12-19 17:55:16 +08:00
namespace mediakit {
// //////////HTTP配置/////////// [AUTO-TRANSLATED:a281d694]
// //////////HTTP Configuration///////////
2018-12-19 17:55:16 +08:00
namespace Http {
#define HTTP_FIELD "http."
#define HTTP_PORT 80
2019-06-19 10:32:54 +08:00
const string kPort = HTTP_FIELD"port";
2018-12-19 17:55:16 +08:00
#define HTTPS_PORT 443
2019-06-19 10:32:54 +08:00
const string kSSLPort = HTTP_FIELD"sslport";
2018-12-19 17:55:16 +08:00
onceToken token1([](){
mINI::Instance()[kPort] = HTTP_PORT;
mINI::Instance()[kSSLPort] = HTTPS_PORT;
},nullptr);
}//namespace Http
// //////////SHELL配置/////////// [AUTO-TRANSLATED:f023ec45]
// //////////SHELL Configuration///////////
2018-12-19 17:55:16 +08:00
namespace Shell {
#define SHELL_FIELD "shell."
#define SHELL_PORT 9000
2019-06-19 10:32:54 +08:00
const string kPort = SHELL_FIELD"port";
2018-12-19 17:55:16 +08:00
onceToken token1([](){
mINI::Instance()[kPort] = SHELL_PORT;
},nullptr);
} //namespace Shell
// //////////RTSP服务器配置/////////// [AUTO-TRANSLATED:950e1981]
// //////////RTSP Server Configuration///////////
2018-12-19 17:55:16 +08:00
namespace Rtsp {
#define RTSP_FIELD "rtsp."
#define RTSP_PORT 554
#define RTSPS_PORT 322
2019-06-19 10:32:54 +08:00
const string kPort = RTSP_FIELD"port";
const string kSSLPort = RTSP_FIELD"sslport";
2018-12-19 17:55:16 +08:00
onceToken token1([](){
mINI::Instance()[kPort] = RTSP_PORT;
mINI::Instance()[kSSLPort] = RTSPS_PORT;
},nullptr);
} //namespace Rtsp
// //////////RTMP服务器配置/////////// [AUTO-TRANSLATED:8de6f41f]
// //////////RTMP Server Configuration///////////
2018-12-19 17:55:16 +08:00
namespace Rtmp {
#define RTMP_FIELD "rtmp."
#define RTMP_PORT 1935
2019-06-19 10:32:54 +08:00
const string kPort = RTMP_FIELD"port";
2018-12-19 17:55:16 +08:00
onceToken token1([](){
mINI::Instance()[kPort] = RTMP_PORT;
},nullptr);
} //namespace RTMP
} // namespace mediakit
2022-09-18 21:03:05 +08:00
#define REALM "realm_zlmediakit"
static map<string,FlvRecorder::Ptr> s_mapFlvRecorder;
static mutex s_mtxFlvRecorder;
2019-06-19 10:27:52 +08:00
void initEventListener() {
static onceToken s_token([]() {
// 监听kBroadcastOnGetRtspRealm事件决定rtsp链接是否需要鉴权(传统的rtsp鉴权方案)才能访问 [AUTO-TRANSLATED:cf05f5a1]
// Listen for kBroadcastOnGetRtspRealm event to decide if rtsp link needs authentication (traditional rtsp authentication scheme) to access
NoticeCenter::Instance().addListener(nullptr, Broadcast::kBroadcastOnGetRtspRealm, [](BroadcastOnGetRtspRealmArgs) {
DebugL << "RTSP是否需要鉴权事件" << args.getUrl() << " " << args.params;
2023-05-25 16:23:24 +08:00
if (string("1") == args.stream) {
// live/1需要认证 [AUTO-TRANSLATED:c3885602]
// live/1 needs authentication
// 该流需要认证并且设置realm [AUTO-TRANSLATED:624f8df5]
// This stream needs authentication and sets realm
invoker(REALM);
} else {
// 有时我们要查询redis或数据库来判断该流是否需要认证通过invoker的方式可以做到完全异步 [AUTO-TRANSLATED:41d2819d]
// Sometimes we need to query redis or database to determine if the stream needs authentication, which can be done asynchronously through invoker
// 该流我们不需要认证 [AUTO-TRANSLATED:67866699]
// This stream does not need authentication
invoker("");
}
});
2019-06-19 10:27:52 +08:00
// 监听kBroadcastOnRtspAuth事件返回正确的rtsp鉴权用户密码 [AUTO-TRANSLATED:3d135a03]
// Listen for kBroadcastOnRtspAuth event to return the correct rtsp authentication username and password
2019-06-19 10:27:52 +08:00
NoticeCenter::Instance().addListener(nullptr, Broadcast::kBroadcastOnRtspAuth, [](BroadcastOnRtspAuthArgs) {
DebugL << "RTSP播放鉴权:" << args.getUrl() << " " << args.params;
2019-06-19 10:27:52 +08:00
DebugL << "RTSP用户" << user_name << (must_no_encrypt ? " Base64" : " MD5") << " 方式登录";
string user = user_name;
// 假设我们异步读取数据库 [AUTO-TRANSLATED:38545bdf]
// Assuming we read the database asynchronously
2019-06-19 10:27:52 +08:00
if (user == "test0") {
// 假设数据库保存的是明文 [AUTO-TRANSLATED:731d594a]
// Assuming the database stores plaintext
2019-06-19 10:27:52 +08:00
invoker(false, "pwd0");
return;
}
if (user == "test1") {
// 假设数据库保存的是密文 [AUTO-TRANSLATED:293ae3d5]
// Assuming the database stores ciphertext
2019-06-19 10:27:52 +08:00
auto encrypted_pwd = MD5(user + ":" + REALM + ":" + "pwd1").hexdigest();
invoker(true, encrypted_pwd);
return;
}
if (user == "test2" && must_no_encrypt) {
// 假设登录的是test2,并且以base64方式登录此时我们提供加密密码那么会导致认证失败 [AUTO-TRANSLATED:e2b29961]
// Assuming login is test2 and login in base64 format, in this case we provide encrypted password, which will cause authentication failure
// 可以通过这个方式屏蔽base64这种不安全的加密方式 [AUTO-TRANSLATED:f303f620]
// You can shield this insecure encryption method in this way
2019-06-19 10:27:52 +08:00
invoker(true, "pwd2");
return;
}
// 其他用户密码跟用户名一致 [AUTO-TRANSLATED:53aa89d9]
// Other user passwords are the same as usernames
2019-06-19 10:27:52 +08:00
invoker(false, user);
});
// 监听rtsp/rtmp推流事件返回结果告知是否有推流权限 [AUTO-TRANSLATED:6c16b087]
// Listen for rtsp/rtmp push stream event, return result to inform whether there is push stream permission
2019-06-19 10:27:52 +08:00
NoticeCenter::Instance().addListener(nullptr, Broadcast::kBroadcastMediaPublish, [](BroadcastMediaPublishArgs) {
DebugL << "推流鉴权:" << args.getUrl() << " " << args.params;
invoker("", ProtocolOption());//鉴权成功
// invoker("this is auth failed message");//鉴权失败 [AUTO-TRANSLATED:4131434d]
// invoker("this is auth failed message");//Authentication failed
2019-06-19 10:27:52 +08:00
});
// 监听rtsp/rtsps/rtmp/http-flv播放事件返回结果告知是否有播放权限(rtsp通过kBroadcastOnRtspAuth或此事件都可以实现鉴权) [AUTO-TRANSLATED:94fed0b4]
// Listen for rtsp/rtsps/rtmp/http-flv playback event, return result to inform whether there is playback permission (rtsp can implement authentication through kBroadcastOnRtspAuth or this event)
2019-06-19 10:27:52 +08:00
NoticeCenter::Instance().addListener(nullptr, Broadcast::kBroadcastMediaPlayed, [](BroadcastMediaPlayedArgs) {
DebugL << "播放鉴权:" << args.getUrl() << " " << args.params;
2019-06-19 10:27:52 +08:00
invoker("");//鉴权成功
// invoker("this is auth failed message");//鉴权失败 [AUTO-TRANSLATED:4131434d]
// invoker("this is auth failed message");//Authentication failed
2019-06-19 10:27:52 +08:00
});
// shell登录事件通过shell可以登录进服务器执行一些命令 [AUTO-TRANSLATED:a60f6d9f]
// Shell login event, you can log in to the server through shell to execute some commands
2019-06-19 10:27:52 +08:00
NoticeCenter::Instance().addListener(nullptr, Broadcast::kBroadcastShellLogin, [](BroadcastShellLoginArgs) {
DebugL << "shell login:" << user_name << " " << passwd;
invoker("");//鉴权成功
// invoker("this is auth failed message");//鉴权失败 [AUTO-TRANSLATED:4131434d]
// invoker("this is auth failed message");//Authentication failed
2019-06-19 10:27:52 +08:00
});
// 监听rtsp、rtmp源注册或注销事件此处用于测试rtmp保存为flv录像保存在http根目录下 [AUTO-TRANSLATED:c572252d]
// Listen for rtsp/rtmp source registration or cancellation event; this is used to test rtmp saving as flv recording, saved in the http root directory
2019-06-19 10:27:52 +08:00
NoticeCenter::Instance().addListener(nullptr, Broadcast::kBroadcastMediaChanged, [](BroadcastMediaChangedArgs) {
auto tuple = sender.getMediaTuple();
if (sender.getSchema() == RTMP_SCHEMA && tuple.app == "live") {
2019-06-19 10:27:52 +08:00
lock_guard<mutex> lck(s_mtxFlvRecorder);
auto key = tuple.shortUrl();
2019-07-30 21:29:31 +08:00
if (bRegist) {
DebugL << "开始录制RTMP" << sender.getUrl();
2019-06-19 10:27:52 +08:00
GET_CONFIG(string, http_root, Http::kRootPath);
auto path = http_root + "/" + key + "_" + to_string(time(NULL)) + ".flv";
2019-06-19 10:27:52 +08:00
FlvRecorder::Ptr recorder(new FlvRecorder);
try {
recorder->startRecord(EventPollerPool::Instance().getPoller(),
dynamic_pointer_cast<RtmpMediaSource>(sender.shared_from_this()), path);
s_mapFlvRecorder[key] = recorder;
2019-06-19 10:27:52 +08:00
} catch (std::exception &ex) {
WarnL << ex.what();
}
} else {
s_mapFlvRecorder.erase(key);
}
2019-03-22 14:11:39 +08:00
}
2019-06-19 10:27:52 +08:00
});
// 监听播放失败(未找到特定的流)事件 [AUTO-TRANSLATED:eeac767c]
// Listen for playback failure (stream not found) event
NoticeCenter::Instance().addListener(nullptr, Broadcast::kBroadcastNotFoundStream, [](BroadcastNotFoundStreamArgs) {
/**
*
* ZLMediaKit会把其立即转发给播放器(55)
* You can pull the stream again when this event is triggered, which can implement on-demand pulling
* After pulling the stream successfully, ZLMediaKit will immediately forward it to the player (the maximum waiting time is about 5 seconds, if it still fails to pull the stream within 5 seconds, the player will play failure)
* [AUTO-TRANSLATED:5a01351c]
*/
DebugL << "未找到流事件:" << args.getUrl() << " " << args.params;
});
2018-12-19 18:18:24 +08:00
// 监听播放或推流结束时消耗流量事件 [AUTO-TRANSLATED:f87ac02d]
// Listen for playback or push stream end event to consume traffic
2019-06-19 10:27:52 +08:00
NoticeCenter::Instance().addListener(nullptr, Broadcast::kBroadcastFlowReport, [](BroadcastFlowReportArgs) {
DebugL << "播放器(推流器)断开连接事件:" << args.getUrl() << " " << args.params << "\r\n使用流量:" << totalBytes << " bytes,连接时长:" << totalDuration << "";
2018-12-19 18:18:24 +08:00
2019-06-19 10:27:52 +08:00
});
2018-12-19 18:18:24 +08:00
2019-06-19 10:27:52 +08:00
}, nullptr);
}
2017-12-10 01:34:43 +08:00
#if !defined(SIGHUP)
2018-03-21 11:35:00 +08:00
#define SIGHUP 1
#endif
2018-02-09 11:42:55 +08:00
int main(int argc,char *argv[]) {
// 设置日志 [AUTO-TRANSLATED:50372045]
// Set log
Logger::Instance().add(std::make_shared<ConsoleChannel>());
Logger::Instance().add(std::make_shared<FileChannel>());
2017-09-30 13:00:12 +08:00
Logger::Instance().setWriter(std::make_shared<AsyncLogWriter>());
// 加载配置文件,如果配置文件不存在就创建一个 [AUTO-TRANSLATED:761e7479]
// Load configuration file, if the configuration file does not exist, create one
2018-10-24 17:17:55 +08:00
loadIniConfig();
2019-06-19 10:27:52 +08:00
initEventListener();
2017-05-03 00:12:51 +08:00
// 这里是拉流地址支持rtmp/rtsp协议负载必须是H264+AAC [AUTO-TRANSLATED:3f92b7aa]
// This is the pull stream address, supports rtmp/rtsp protocol, and the payload must be H264+AAC
// 如果是其他不识别的音视频将会被忽略(譬如说h264+adpcm转发后会去除音频) [AUTO-TRANSLATED:ad4be3b6]
// If it is other unrecognized audio and video, it will be ignored (for example, h264+adpcm will remove audio after forwarding)
auto urlList = {"rtsp://admin:admin123@192.168.1.64:554/cam/realmonitor?channel=1&subtype=1"
// rtsp链接支持输入用户名密码 [AUTO-TRANSLATED:a0e2ebfa]
// rtsp link supports inputting username and password
/*"rtsp://admin:jzan123456@192.168.0.122/"*/};
map<string, PlayerProxy::Ptr> proxyMap;
int i = 0;
for (auto &url : urlList) {
// PlayerProxy构造函数前两个参数分别为应用名app,流idstreamId [AUTO-TRANSLATED:5b793e17]
// PlayerProxy constructor's first two parameters are application name (app) and stream id (streamId)
// 比如说应用为live流id为0那么直播地址为: [AUTO-TRANSLATED:d1f0844c]
// For example, if the application is live and the stream id is 0, the live address is:
// hls地址 : http://127.0.0.1/live/0/hls.m3u8 [AUTO-TRANSLATED:0a684e23]
// hls address: http://127.0.0.1/live/0/hls.m3u8
// http-flv地址 : http://127.0.0.1/live/0.flv [AUTO-TRANSLATED:ffdf3994]
// http-flv address: http://127.0.0.1/live/0.flv
// rtsp地址 : rtsp://127.0.0.1/live/0 [AUTO-TRANSLATED:784f37f6]
// rtsp address: rtsp://127.0.0.1/live/0
// rtmp地址 : rtmp://127.0.0.1/live/0 [AUTO-TRANSLATED:6f377de8]
// rtmp address: rtmp://127.0.0.1/live/0
// 录像地址为(当然vlc不支持这么多级的rtmp url可以用test_player测试rtmp点播): [AUTO-TRANSLATED:2e45d9a5]
// The recorded address is (of course, vlc does not support so many levels of rtmp url, you can use test_player to test rtmp on-demand):
//http://127.0.0.1/record/live/0/2017-04-11/11-09-38.mp4
//rtsp://127.0.0.1/record/live/0/2017-04-11/11-09-38.mp4
//rtmp://127.0.0.1/record/live/0/2017-04-11/11-09-38.mp4
auto tuple = MediaTuple{DEFAULT_VHOST, "live", std::string("chn") + to_string(i).data(), ""};
PlayerProxy::Ptr player(new PlayerProxy(tuple, ProtocolOption()));
// 指定RTP over TCP(播放rtsp时有效) [AUTO-TRANSLATED:1a062656]
// Specify RTP over TCP (effective when playing rtsp)
(*player)[Client::kRtpType] = Rtsp::RTP_TCP;
// 开始播放,如果播放失败或者播放中止,将会自动重试若干次,重试次数在配置文件中配置,默认一直重试 [AUTO-TRANSLATED:c9ad670c]
// Start playing. If playback fails or is interrupted, it will automatically retry several times. The number of retries is configured in the configuration file, and the default is to retry indefinitely
2019-01-30 17:26:02 +08:00
player->play(url);
// 需要保存PlayerProxy否则作用域结束就会销毁该对象 [AUTO-TRANSLATED:939a84c9]
// You need to save PlayerProxy, otherwise the object will be destroyed when the scope ends
proxyMap.emplace(to_string(i), player);
++i;
}
DebugL << "\r\n"
" PlayerProxy构造函数前两个参数分别为应用名app,流idstreamId\n"
" 比如说应用为live流id为0那么直播地址为:\n"
" hls地址 : http://127.0.0.1/live/0/hls.m3u8\n"
" http-flv地址 : http://127.0.0.1/live/0.flv\n"
" rtsp地址 : rtsp://127.0.0.1/live/0\n"
" rtmp地址 : rtmp://127.0.0.1/live/0";
2019-07-30 21:29:31 +08:00
// 加载证书,证书包含公钥和私钥 [AUTO-TRANSLATED:fce78641]
// Load the certificate, which contains the public key and private key
2019-04-16 17:25:19 +08:00
SSL_Initor::Instance().loadCertificate((exeDir() + "ssl.p12").data());
// 信任某个自签名证书 [AUTO-TRANSLATED:6815fc55]
// Trust a self-signed certificate
2019-04-16 17:25:19 +08:00
SSL_Initor::Instance().trustCertificate((exeDir() + "ssl.p12").data());
// 不忽略无效证书证书(例如自签名或过期证书) [AUTO-TRANSLATED:ee4a34c4]
// Do not ignore invalid certificates (such as self-signed or expired certificates)
2019-04-16 17:25:19 +08:00
SSL_Initor::Instance().ignoreInvalidCertificate(false);
uint16_t shellPort = mINI::Instance()[Shell::kPort];
uint16_t rtspPort = mINI::Instance()[Rtsp::kPort];
uint16_t rtspsPort = mINI::Instance()[Rtsp::kSSLPort];
uint16_t rtmpPort = mINI::Instance()[Rtmp::kPort];
uint16_t httpPort = mINI::Instance()[Http::kPort];
uint16_t httpsPort = mINI::Instance()[Http::kSSLPort];
// 简单的telnet服务器可用于服务器调试但是不能使用23端口否则telnet上了莫名其妙的现象 [AUTO-TRANSLATED:f9324c6e]
// A simple telnet server, which can be used for server debugging, but cannot use port 23, otherwise telnet will have inexplicable phenomena
// 测试方法:telnet 127.0.0.1 9000 [AUTO-TRANSLATED:de0ac883]
// Test method: telnet 127.0.0.1 9000
2019-07-30 21:29:31 +08:00
TcpServer::Ptr shellSrv(new TcpServer());
TcpServer::Ptr rtspSrv(new TcpServer());
TcpServer::Ptr rtmpSrv(new TcpServer());
TcpServer::Ptr httpSrv(new TcpServer());
2019-07-30 21:29:31 +08:00
shellSrv->start<ShellSession>(shellPort);
rtspSrv->start<RtspSession>(rtspPort);//默认554
rtmpSrv->start<RtmpSession>(rtmpPort);//默认1935
// http服务器 [AUTO-TRANSLATED:10d47f45]
// http server
2019-09-17 09:48:20 +08:00
httpSrv->start<HttpSession>(httpPort);//默认80
// 如果支持ssl还可以开启https服务器 [AUTO-TRANSLATED:8ef29f9c]
// If ssl is supported, you can also enable the https server
TcpServer::Ptr httpsSrv(new TcpServer());
// https服务器 [AUTO-TRANSLATED:a2bb64bd]
// https server
2019-09-17 09:48:20 +08:00
httpsSrv->start<HttpsSession>(httpsPort);//默认443
// 支持ssl加密的rtsp服务器可用于诸如亚马逊echo show这样的设备访问 [AUTO-TRANSLATED:ded79b73]
// rtsp server that supports ssl encryption, which can be used for devices such as Amazon Echo Show to access
TcpServer::Ptr rtspSSLSrv(new TcpServer());
rtspSSLSrv->start<RtspSessionWithSSL>(rtspsPort);//默认322
2019-07-30 21:29:31 +08:00
// 服务器支持动态切换端口(不影响现有连接) [AUTO-TRANSLATED:28949b78]
// The server supports dynamic port switching (without affecting existing connections)
NoticeCenter::Instance().addListener(ReloadConfigTag,Broadcast::kBroadcastReloadConfig,[&](BroadcastReloadConfigArgs){
// 重新创建服务器 [AUTO-TRANSLATED:4e192357]
// Recreate the server
if(shellPort != mINI::Instance()[Shell::kPort].as<uint16_t>()){
shellPort = mINI::Instance()[Shell::kPort];
shellSrv->start<ShellSession>(shellPort);
InfoL << "重启shell服务器:" << shellPort;
}
if(rtspPort != mINI::Instance()[Rtsp::kPort].as<uint16_t>()){
rtspPort = mINI::Instance()[Rtsp::kPort];
rtspSrv->start<RtspSession>(rtspPort);
InfoL << "重启rtsp服务器" << rtspPort;
}
if(rtmpPort != mINI::Instance()[Rtmp::kPort].as<uint16_t>()){
rtmpPort = mINI::Instance()[Rtmp::kPort];
rtmpSrv->start<RtmpSession>(rtmpPort);
InfoL << "重启rtmp服务器" << rtmpPort;
}
if(httpPort != mINI::Instance()[Http::kPort].as<uint16_t>()){
httpPort = mINI::Instance()[Http::kPort];
2019-09-17 09:48:20 +08:00
httpSrv->start<HttpSession>(httpPort);
InfoL << "重启http服务器" << httpPort;
}
if(httpsPort != mINI::Instance()[Http::kSSLPort].as<uint16_t>()){
httpsPort = mINI::Instance()[Http::kSSLPort];
2019-09-17 09:48:20 +08:00
httpsSrv->start<HttpsSession>(httpsPort);
InfoL << "重启https服务器" << httpsPort;
2018-02-09 11:42:55 +08:00
}
2017-04-05 09:39:16 +08:00
if(rtspsPort != mINI::Instance()[Rtsp::kSSLPort].as<uint16_t>()){
rtspsPort = mINI::Instance()[Rtsp::kSSLPort];
rtspSSLSrv->start<RtspSessionWithSSL>(rtspsPort);
InfoL << "重启rtsps服务器" << rtspsPort;
}
});
2019-07-30 21:29:31 +08:00
// 设置退出信号处理函数 [AUTO-TRANSLATED:4f047770]
// Set the exit signal processing function
2019-02-11 16:29:27 +08:00
static semaphore sem;
signal(SIGINT, [](int) { sem.post(); });// 设置退出信号
signal(SIGHUP, [](int) { loadIniConfig(); });
sem.wait();
lock_guard<mutex> lck(s_mtxFlvRecorder);
s_mapFlvRecorder.clear();
2020-03-20 11:51:24 +08:00
return 0;
2017-04-05 09:39:16 +08:00
}