/* * MIT License * * Copyright (c) 2016-2019 xiongziliang <771730766@qq.com> * * This file is part of ZLMediaKit(https://github.com/xiongziliang/ZLMediaKit). * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include #include #include #include #include #include "jsoncpp/json.h" #include "Util/util.h" #include "Util/logger.h" #include "Util/onceToken.h" #include "Util/NoticeCenter.h" #ifdef ENABLE_MYSQL #include "Util/SqlPool.h" #endif //ENABLE_MYSQL #include "Common/config.h" #include "Common/MediaSource.h" #include "Http/HttpRequester.h" #include "Http/HttpSession.h" #include "Network/TcpServer.h" #include "Player/PlayerProxy.h" #include "Kf/DbUtil.h" #include "Kf/Globals.h" #include "Util/MD5.h" #include "WebApi.h" #include #include #include #include #include "WebHook.h" #if !defined(_WIN32) #include "FFmpegSource.h" #endif//!defined(_WIN32) using namespace Json; using namespace toolkit; using namespace mediakit; //chenxiaolei 让response返回的值更丰富些, 以便开发新增的一些api typedef std::vector> MultipartPartList; typedef map ApiArgsType; struct ArgsContentExt { ApiArgsType allArgs; Json::Value jsonArgs; string rawArgs; MultipartPartList partList; }; #define API_ARGS TcpSession &sender, \ HttpSession::KeyValue &headerIn, \ HttpSession::KeyValue &headerOut, \ ApiArgsType &allArgs, \ Json::Value &jsonArgs, \ ArgsContentExt &argsContent, \ Json::Value &val #define API_REGIST(field, name, ...) \ s_map_api.emplace("/index/"#field"/"#name,[](API_ARGS,const HttpSession::HttpResponseInvoker &invoker){ \ static auto lam = [&](API_ARGS) __VA_ARGS__ ; \ lam(sender,headerIn, headerOut, allArgs, jsonArgs, argsContent, val); \ invoker("200 OK", headerOut, val.toStyledString()); \ }); #define API_REGIST_INVOKER(field, name, ...) \ s_map_api.emplace("/index/"#field"/"#name,[](API_ARGS,const HttpSession::HttpResponseInvoker &invoker) __VA_ARGS__); //异步http api lambad定义 typedef std::function AsyncHttpApi; //api列表 static map s_map_api; namespace API { typedef enum { InvalidArgs = -300, SqlFailed = -200, AuthFailed = -100, OtherFailed = -1, Success = 0 } ApiErr; #define API_FIELD "api." const string kApiDebug = API_FIELD"apiDebug"; const string kSecret = API_FIELD"secret"; static onceToken token([]() { mINI::Instance()[kApiDebug] = "1"; mINI::Instance()[kSecret] = "035c73f7-bb6b-4889-a715-d9eb2d1925cc"; }); }//namespace API class ApiRetException: public std::runtime_error { public: ApiRetException(const char *str = "success" ,int code = API::Success):runtime_error(str){ _code = code; } ~ApiRetException() = default; int code(){ return _code; } private: int _code; }; class AuthException : public ApiRetException { public: AuthException(const char *str):ApiRetException(str,API::AuthFailed){} ~AuthException() = default; }; class InvalidArgsException: public ApiRetException { public: InvalidArgsException(const char *str):ApiRetException(str,API::InvalidArgs){} ~InvalidArgsException() = default; }; class SuccessException: public ApiRetException { public: SuccessException():ApiRetException("success",API::Success){} ~SuccessException() = default; }; //获取HTTP请求中url参数、content参数 static ArgsContentExt getAllArgs(const Parser &parser) { ApiArgsType allArgs; Value jsonArgs; string rawArgs = parser.Content(); MultipartPartList partList; if (parser["Content-Type"].find("application/x-www-form-urlencoded") == 0) { auto contentArgs = parser.parseArgs(rawArgs); for (auto &pr : contentArgs) { allArgs[pr.first] = HttpSession::urlDecode(pr.second); } } else if (parser["Content-Type"].find("application/json") == 0) { try { stringstream ss(rawArgs); ss >> jsonArgs; auto keys = jsonArgs.getMemberNames(); for (auto key = keys.begin(); key != keys.end(); ++key) { allArgs[*key] = jsonArgs[*key].asString(); } } catch (std::exception &ex) { WarnL << ex.what(); } } else if (parser["Content-Type"].find("multipart/form-data") == 0) { //chenxiaolei 让api支持附件上传,目的是后面的上传通道配置接口(csv) //InfoL << "form-data: " << rawArgs; auto contentType = parser["Content-Type"]; auto boundary = FindField((contentType + "\r\n").c_str(), "boundary=", "\r\n"); string partStartFlag = "--" + boundary; string endFlag = "--" + boundary + "--"; map attr; _StrPrinter partContent; const char *start = rawArgs.c_str(); int i = 0; while (true) { auto line = FindField(start, NULL, "\r\n"); bool isEnd = line.compare(endFlag) == 0; bool isNewStart = line.compare(partStartFlag) == 0; if (isEnd || isNewStart) { if (attr.size() > 0) { auto _partContent = string(partContent); _partContent= trim(_partContent,"\r\n"); attr.emplace("PartContent", _partContent); partList.push_back(attr); allArgs[attr["PartName"]] = _partContent; } if (isEnd) { break; } attr = map(); partContent = _StrPrinter(); } else if (line.find("Content-Disposition") == 0) { attr.emplace("Content-Disposition", FindField(line.data(), "Content-Disposition: ", ";")); attr.emplace("PartName", FindField(line.data(), "name=\"", "\"")); attr.emplace("OriginalFileName", FindField(line.data(), "filename=\"", "\"")); } else if (line.find("Content-Type") == 0) { attr.emplace("Content-Type", FindField((line + "\r\n").data(), "Content-Type: ", "\r\n")); } else { partContent << line << "\r\n"; } start = start + line.size() + 2; i++; } } else if (!parser["Content-Type"].empty()) { WarnL << "invalid Content-Type:" << parser["Content-Type"]; } auto &urlArgs = parser.getUrlArgs(); for (auto &pr : urlArgs) { allArgs[pr.first] = HttpSession::urlDecode(pr.second); } //chenxiaolei 让response返回的值更丰富些, 以便开发新增的一些api struct ArgsContentExt ret; ret.allArgs = std::move(allArgs); ret.jsonArgs = jsonArgs; ret.rawArgs = rawArgs; ret.partList = partList; return ret; } static inline void addHttpListener(){ GET_CONFIG(bool, api_debug, API::kApiDebug); //注册监听kBroadcastHttpRequest事件 NoticeCenter::Instance().addListener(nullptr, Broadcast::kBroadcastHttpRequest, [](BroadcastHttpRequestArgs) { auto it = s_map_api.find(parser.Url()); if (it == s_map_api.end()) { consumed = false; return; } //该api已被消费 consumed = true; //执行API Json::Value val; val["code"] = API::Success; HttpSession::KeyValue headerOut; //chenxiaolei 让response返回的值更丰富些, 以便开发新增的一些api auto argsContentExt = getAllArgs(parser); auto allArgs = argsContentExt.allArgs; auto jsonArgs = argsContentExt.jsonArgs; HttpSession::KeyValue &headerIn = parser.getValues(); headerOut["Content-Type"] = "application/json; charset=utf-8"; if(api_debug){ auto newInvoker = [invoker,parser,allArgs](const string &codeOut, const HttpSession::KeyValue &headerOut, const string &contentOut){ stringstream ss; for(auto &pr : allArgs ){ ss << pr.first << " : " << pr.second << "\r\n"; } DebugL << "\r\n# request:\r\n" << parser.Method() << " " << parser.FullUrl() << "\r\n" << "# content:\r\n" << parser.Content() << "\r\n" << "# args:\r\n" << ss.str() << "# response:\r\n" << contentOut << "\r\n"; invoker(codeOut,headerOut,contentOut); }; ((HttpSession::HttpResponseInvoker &)invoker) = newInvoker; } try { it->second(sender, headerIn, headerOut, allArgs, jsonArgs, argsContentExt, val, invoker); } catch(ApiRetException &ex){ val["code"] = ex.code(); val["msg"] = ex.what(); invoker("200 OK", headerOut, val.toStyledString()); } #ifdef ENABLE_MYSQL catch(SqlException &ex){ val["code"] = API::SqlFailed; val["msg"] = StrPrinter << "操作数据库失败:" << ex.what() << ":" << ex.getSql(); WarnL << ex.what() << ":" << ex.getSql(); invoker("200 OK", headerOut, val.toStyledString()); } #endif// ENABLE_MYSQL catch (std::exception &ex) { val["code"] = API::OtherFailed; val["msg"] = ex.what(); invoker("200 OK", headerOut, val.toStyledString()); } }); } template bool checkArgs(Args &&args,First &&first){ return !args[first].empty(); } template bool checkArgs(Args &&args,First &&first,KeyTypes && ...keys){ return !args[first].empty() && checkArgs(std::forward(args),std::forward(keys)...); } //chenxiaolei 判断录像父目录中是否存在录像,避免一些空目录,为[按月查询是否有录像]的接口支持 bool directoryMp4RecordExists(const std::string &directory) { DIR *dr = opendir(directory.data()); if (dr != NULL) { struct dirent *de; bool exist = false; while ((de = readdir(dr)) != NULL) { if (std::regex_match(de->d_name, std::regex(("\\d{2}-\\d{2}-\\d{2}\\.mp4\\.json")))) { exist = true; break; } } closedir(dr); return exist; } return false; } #define CHECK_ARGS(...) \ if(!checkArgs(allArgs,##__VA_ARGS__)){ \ throw InvalidArgsException("缺少必要参数:" #__VA_ARGS__); \ } #define CHECK_SECRET() \ if(sender.get_peer_ip() != "127.0.0.1"){ \ CHECK_ARGS("secret"); \ if(api_secret != allArgs["secret"]){ \ throw AuthException("secret错误"); \ } \ } static unordered_map s_proxyMap; static recursive_mutex s_proxyMapMtx; #if !defined(_WIN32) static unordered_map s_ffmpegMap; static recursive_mutex s_ffmpegMapMtx; #endif//#if !defined(_WIN32) //chenxiaolei 数据库配置的通道使用的proxyMap unordered_map m_s_proxyMap; recursive_mutex m_s_proxyMapMtx; //chenxiaolei 数据库配置的通道使用的proxyMap unordered_map m_s_ffmpegMap; recursive_mutex m_s_ffmpegMapMtx; //chenxiaolei 推流实时快照截图支持 struct Snapshot_Info { string url; string storePath; }; std::shared_ptr snapshotTimer; recursive_mutex m_s_snapshotTimerMtx; unordered_map snapshotInfoMap; //chenxiaolei 快照截图方法 void snapShot(Snapshot_Info info) { //InfoL << "snapshot: " <async([info]() { //先把之前的删除掉,当目标流中间某段时间不可用,删除保证快照是最新的,不会一直是之前最后一次正常时的截图 File::delete_file(info.storePath.data()); char cmd[1024] = {0}; snprintf(cmd, sizeof(cmd), "nohup ffmpeg -probesize 32768 %s -i %s -y -t 0.001 -ss 1 -f image2 -r 1 -s 720*480 -strftime 1 %s >/dev/null 2>&1", (strncmp(info.url.c_str(), "rtsp", 4) == 0 ? "-rtsp_transport tcp " : ""), info.url.data(), info.storePath.data()); InfoL << "snapshot_cmd: " << cmd; system(cmd); }); } //chenxiaolei 每30秒进行一次快照截图 void processSnapShot() { snapshotTimer.reset(new Timer(30, []() { unordered_map::iterator p; for (p = snapshotInfoMap.begin(); p != snapshotInfoMap.end(); p++) { snapShot(p->second); } return true; }, nullptr)); } //chenxiaolei 配置生效方法 void processProxyCfg(const Json::Value &proxyData, const bool initialize = false) { bool vActive = proxyData["active"].asBool(); std::string proxyKey = proxyData["proxyKey"].asString(); if (!vActive) { //停用了,就停止进行快照截图 if (snapshotInfoMap.find(proxyKey) != snapshotInfoMap.end()) { lock_guard lck(m_s_snapshotTimerMtx); snapshotInfoMap.erase(proxyKey); } return; } std::string vName = proxyData["name"].asString(); std::string vHost = proxyData.get("vhost", DEFAULT_VHOST).asString(); std::string vUrl = proxyData["source_url"].asString(); std::string vApp = proxyData["app"].asString(); std::string vStream = proxyData["stream"].asString(); std::string vFFmpegCmd = proxyData["ffmpeg_cmd"].asString(); bool vEnableHls = proxyData.get("enable_hls", 1).asInt(); bool vOnDemand = proxyData.get("on_demand", 1).asInt(); //按需拉流,用户播放时才为频道拉流(不录像才有效) int vRecordMp4 = proxyData.get("record_mp4", 0).asInt(); //最长录像天数(0表示不录像) int vRtspTransport = proxyData.get("rtsp_transport", 1).asInt(); //最长录像天数(0表示不录像) bool realOnDemand = vRecordMp4 ? false : vOnDemand; InfoL << "\n========================================================================\n" << "========== " << (initialize ? "[初始化]" : "[按需重拉]") << "应用频道代理规则 : " << vName << " ==========\n" << "========================================================================\n" << "active : " << vActive << "\n" << "url : " << vUrl << "\n" << "vhost : " << vHost << "\n" << "app : " << vApp << "\n" << "stream : " << vStream << "\n" << "enable_hls : " << vEnableHls << "\n" << "record_mp4 : " << vRecordMp4 << "\n" << "ffmpeg_cmd : " << vFFmpegCmd << "\n" << "ffmpeg_cmd : " << vFFmpegCmd << "\n" << "on_demand : " << vOnDemand << "\n\n"; //创建存放截图的dir GET_CONFIG(bool, enableVhost, General::kEnableVhost); GET_CONFIG(string, rootPath, Http::kRootPath); string snapshotStorePath; if (enableVhost) { snapshotStorePath = rootPath + "/snapshot/" + DEFAULT_VHOST + +"/" + vApp + "/"; } else { snapshotStorePath = rootPath + "/snapshot/" + vApp + "/"; } File::createfile_path(snapshotStorePath.data(), S_IRWXO | S_IRWXG | S_IRWXU); string targetRtmpChannelUrl = "rtmp://127.0.0.1:" + mINI::Instance()[Rtmp::kPort] + "/" + vApp + "/" + vStream; //加入定时快照截图队列, 按需直播的按接入地址来做截图(没有拉流时从接入地址进行快照,避免按需直播失效), 否者转发出的RTMP流来截 Snapshot_Info s = {realOnDemand ? vUrl : targetRtmpChannelUrl, snapshotStorePath + vStream + ".png"}; lock_guard lck(m_s_snapshotTimerMtx); snapshotInfoMap[proxyKey] = s; if (realOnDemand) { //立即截图 snapShot(s); } /* 如果关闭录像 或者初始化并开启按需拉流时, 直接退出, 拉流交给kBroadcastNotFoundStream事件*/ if (initialize && realOnDemand) { InfoL << "规则\"" << vName << "\"执行按需拉流模式: " << DEFAULT_VHOST << "/" << vApp << "/" << vStream; return; } if (initialize) InfoL << "规则\"" << vName << "\"执行长连拉流模式: " << DEFAULT_VHOST << "/" << vApp << "/" << vStream; MediaInfo _media_info; _media_info.parse(vUrl); //如果是hls, 就使用ffmpeg拉流 if (_media_info._schema == HTTP_SCHEMA) { lock_guard lck(m_s_ffmpegMapMtx); if (m_s_ffmpegMap.find(proxyKey) != m_s_ffmpegMap.end()) { InfoL << "启用FFmpeg进行推送(忽略,之前已经在拉流了) : " << DEFAULT_VHOST << "/" << vApp << "/" << vStream; return; } FFmpegSource::Ptr ffmpeg = std::make_shared(); m_s_ffmpegMap[proxyKey] = ffmpeg; ffmpeg->setOnClose([proxyKey]() { lock_guard lck(m_s_ffmpegMapMtx); m_s_ffmpegMap.erase(proxyKey); }); ffmpeg->play(vUrl, targetRtmpChannelUrl, 10000, vFFmpegCmd, [](const SockException &ex) {}); InfoL << "启用FFmpeg进行推送的 : " << DEFAULT_VHOST << "/" << vApp << "/" << vStream; } else { lock_guard lck(m_s_proxyMapMtx); if (m_s_proxyMap.find(proxyKey) != m_s_proxyMap.end()) { InfoL << "启用PlayerProxy进行推送(忽略,之前已经在拉流了) : " << DEFAULT_VHOST << "/" << vApp << "/" << vStream; return; } PlayerProxy::Ptr player(new PlayerProxy(DEFAULT_VHOST, vApp, vStream, true, true, vEnableHls, vRecordMp4)); m_s_proxyMap[proxyKey] = player; //指定RTP over TCP(播放rtsp时有效) (*player)[kRtpType] = vRtspTransport == 1 ? Rtsp::RTP_TCP : Rtsp::RTP_UDP; player->setPlayCallbackOnce([proxyKey, s](const SockException &ex) { /*if(ex){ lock_guard lck(m_s_proxyMapMtx); m_s_proxyMap.erase(proxyKey); }*/ if (!ex) { //立即截图 snapShot(s); } }); //被主动关闭拉流 player->setOnClose([proxyKey]() { lock_guard lck(m_s_proxyMapMtx); m_s_proxyMap.erase(proxyKey); }); //开始播放,如果播放失败或者播放中止,将会自动重试若干次,重试次数在配置文件中配置,默认一直重试 player->play(vUrl); InfoL << "启用PlayerProxy进行推送的 : " << DEFAULT_VHOST << "/" << vApp << "/" << vStream; } } //chenxiaolei 配置生效方法 void processProxyCfgs(const Json::Value &cfg_root) { for (unsigned int index = 0; index < cfg_root.size(); ++index) { Json::Value proxyData = cfg_root[index]; processProxyCfg(proxyData, true); } processSnapShot(); } /** * 安装api接口 * 所有api都支持GET和POST两种方式 * POST方式参数支持application/json和application/x-www-form-urlencoded方式 */ void installWebApi() { addHttpListener(); GET_CONFIG(string,api_secret,API::kSecret); //获取线程负载 //测试url http://127.0.0.1/index/api/getThreadsLoad API_REGIST_INVOKER(api, getThreadsLoad, { EventPollerPool::Instance().getExecutorDelay([invoker, headerOut](const vector &vecDelay) { Value val; auto vec = EventPollerPool::Instance().getExecutorLoad(); int i = API::Success; for (auto load : vec) { Value obj(objectValue); obj["load"] = load; obj["delay"] = vecDelay[i++]; val["data"].append(obj); } //chenxiaolei 统一每一个接口都必须返回 code val["code"] = API::Success; invoker("200 OK", headerOut, val.toStyledString()); }); }) //chenxiaolei 登录, 判断 secret 是否正确 //测试url http://127.0.0.1/index/api/login API_REGIST(api, login, { CHECK_ARGS("secret"); \ if (api_secret != allArgs["secret"]) { val["code"] = API::AuthFailed; val["msg"] = "无效的 secret"; } else { val["code"] = API::Success; val["secret"] = api_secret; val["msg"] = "success"; } }) //获取服务器配置 //测试url http://127.0.0.1/index/api/getServerConfig API_REGIST(api, getServerConfig, { CHECK_SECRET(); Value obj; for (auto &pr : mINI::Instance()) { obj[pr.first] = (string &) pr.second; } //chenxiaolei 统一每一个接口都必须返回 code, data改为 jsonObj (从 arr改) val["data"] = obj; val["code"] = API::Success; }) //设置服务器配置 //测试url(比如关闭http api调试) http://127.0.0.1/index/api/setServerConfig?api.apiDebug=0 //你也可以通过http post方式传参,可以通过application/x-www-form-urlencoded或application/json方式传参 API_REGIST(api, setServerConfig, { CHECK_SECRET(); auto &ini = mINI::Instance(); int changed = API::Success; for (auto &pr : allArgs) { if (ini.find(pr.first) == ini.end()) { //没有这个key continue; } if (ini[pr.first] == pr.second) { continue; } ini[pr.first] = pr.second; //替换成功 ++changed; } if (changed > 0) { NoticeCenter::Instance().emitEvent(Broadcast::kBroadcastReloadConfig); ini.dumpFile(); } val["changed"] = changed; //chenxiaolei 统一每一个接口都必须返回 code val["code"] = API::Success; }) //chenxiaolei 删除通道配置 //http://127.0.0.1:8099/index/api/deleteChannelConfig API_REGIST(api, deleteChannelConfig, { CHECK_SECRET(); CHECK_ARGS("id"); int id = atoi(allArgs["id"].c_str()); Json::Value channel = searchChannel(id); int rc = deleteChannel(id, [channel]() { if (!channel.isNull()) { auto proxyKey = channel["proxyKey"].asString(); lock_guard lck(m_s_proxyMapMtx); m_s_proxyMap.erase(proxyKey); } }); //SQLITE_OK if (rc == 0) { val["code"] = API::Success; val["msg"] = "success"; } else { val["code"] = API::SqlFailed; val["msg"] = "SQL错误码:" + std::to_string(rc); } }) //chenxiaolei 保存通道配置(有id更新,没有就创建) //http://127.0.0.1:8099/index/api/saveChannelConfig API_REGIST(api, saveChannelConfig, { CHECK_SECRET(); //"id", "name"名称, "vhost", "app", "stream", "source_url"接入地址, "active",是否开启 "enable_hls", "record_mp4"录像最大天数,0是不录, "rtsp_transport"RTSP协议, "on_demand"按需播放 CHECK_ARGS("name", "app", "stream", "source_url", "active", "enable_hls", "record_mp4"); int rc = 0; auto idStr = allArgs["id"]; int id=idStr.empty() ? 0: atoi(idStr.c_str()); rc = saveChannel(id,jsonArgs,[](bool isCreate,Json::Value originalChannel, Json::Value channel){ if(isCreate){ processProxyCfg(channel, true); }else{ //先把原来的尝试删除了 if (!originalChannel.isNull()) { auto proxyKey = originalChannel["proxyKey"].asString(); lock_guard lck(m_s_proxyMapMtx); m_s_proxyMap.erase(proxyKey); } processProxyCfg(channel, true); } }); //SQLITE_OK if (rc == 0) { val["code"] = API::Success; val["msg"] = "success"; } else { val["code"] = API::SqlFailed; val["msg"] = "SQL错误码:" + std::to_string(rc); } }) //chenxiaolei 批量保存通道配置(有id更新,没有就创建) //http://127.0.0.1:8099/index/api/saveChannelConfigs API_REGIST(api, saveChannelConfigs, { CHECK_SECRET(); //CHECK_ARGS("data"); Json::Value configArray = jsonArgs["data"]; for (Json::Value::ArrayIndex i = 0; i != configArray.size(); i++) { auto cConfig = configArray[i]; int rc = 0; string idStr = cConfig["id"].asString(); int id= idStr.empty() ? 0: atoi(idStr.c_str()); rc = saveChannel(id,cConfig,[](bool isCreate,Json::Value originalChannel, Json::Value channel){ if(isCreate){ processProxyCfg(channel, true); }else{ //先把原来的尝试删除了 if (!originalChannel.isNull()) { auto proxyKey = originalChannel["proxyKey"].asString(); lock_guard lck(m_s_proxyMapMtx); m_s_proxyMap.erase(proxyKey); } processProxyCfg(channel, true); } }); } val["code"] = API::Success; val["msg"] = "success"; }) //chenxiaolei 创建通道配置 //http://127.0.0.1:8099/index/api/createChannelConfig API_REGIST(api, createChannelConfig, { CHECK_SECRET(); //"id", "name"名称, "vhost", "app", "stream", "source_url"接入地址, "active",是否开启 "enable_hls", "record_mp4"录像最大天数,0是不录, "rtsp_transport"RTSP协议, "on_demand"按需播放 CHECK_ARGS("name", "app", "stream", "source_url", "active", "enable_hls", "record_mp4"); int rc = createChannel(jsonArgs, [](Json::Value channel) { processProxyCfg(channel, true); }); //SQLITE_OK if (rc == 0) { val["code"] = API::Success; val["msg"] = "success"; } else { val["code"] = API::SqlFailed; val["msg"] = "SQL错误码:" + std::to_string(rc); } }) //chenxiaolei 更新通道配置 //http://127.0.0.1:8099/index/api/updateChannelConfig API_REGIST(api, updateChannelConfig, { CHECK_SECRET(); //"id", "name"名称, "vhost", "app", "stream", "source_url"接入地址, "active",是否开启 "enable_hls", "record_mp4"录像最大天数,0是不录, "rtsp_transport"RTSP协议, "on_demand"按需播放 CHECK_ARGS("id", "name", "app", "stream", "source_url", "active", "enable_hls", "record_mp4"); int id = atoi(allArgs["id"].c_str()); Json::Value originalChannel = searchChannel(id); int rc = updateChannel(id, jsonArgs, [originalChannel](Json::Value channel) { //先把原来的尝试删除了 if (!originalChannel.isNull()) { auto proxyKey = originalChannel["proxyKey"].asString(); lock_guard lck(m_s_proxyMapMtx); m_s_proxyMap.erase(proxyKey); } processProxyCfg(channel, true); }); //SQLITE_OK if (rc == 0) { val["code"] = API::Success; val["msg"] = "success"; } else { val["code"] = API::SqlFailed; val["msg"] = "SQL错误码:" + std::to_string(rc); } }) API_REGIST_INVOKER(api, uploadChannelConfigs, { EventPollerPool::Instance().getPoller()->async([invoker, argsContent, headerOut]() { Value val; auto partList = argsContent.partList; if (partList.size() > 0) { map firstPart = partList.front(); if (firstPart["Content-Type"] != "text/csv") { val["code"] = API::InvalidArgs; val["msg"] = "上传文件Content-Type无效,请检查是否为text/csv"; } else { auto partContent = partList.front()["PartContent"]; Json::Value configArray=channelsCsvStrToJson(partContent); for (Json::Value::ArrayIndex i = 0; i != configArray.size(); i++) { auto cConfig = configArray[i]; int rc = 0; string idStr = cConfig["id"].asString(); int id= idStr.empty() ? 0: atoi(idStr.c_str()); rc = saveChannel(id,cConfig,[](bool isCreate,Json::Value originalChannel, Json::Value channel){ if(isCreate){ processProxyCfg(channel, true); }else{ //先把原来的尝试删除了 if (!originalChannel.isNull()) { auto proxyKey = originalChannel["proxyKey"].asString(); lock_guard lck(m_s_proxyMapMtx); m_s_proxyMap.erase(proxyKey); } processProxyCfg(channel, true); } }); } val["code"] = API::Success; val["msg"] = "上传成功," + (configArray.isNull()? "0":to_string(configArray.size()-1)) + "条记录变化"; } } invoker("200 OK", headerOut, val.toStyledString()); }, false); }) //chenxiaolei 下载通道配置(列表 csv) API_REGIST_INVOKER(api, downloadChannelConfigs, { headerOut["Content-Type"] = "text/csv;charset=UTF-8"; headerOut["Content-disposition"] = "attachment;filename=zlmedia_channels.csv"; EventPollerPool::Instance().getPoller()->async([invoker, headerOut]() { auto channels = searchChannels(); auto strSend = channelsJsonToCsvStr(channels) ; invoker("200 OK", headerOut, strSend); }, false); }) //chenxiaolei 获取通道配置(列表) //http://127.0.0.1:8099/index/api/searchChannelConfigs API_REGIST(api, searchChannelConfigs, { CHECK_SECRET(); auto searchText = allArgs["searchText"]; auto enableMp4 = allArgs["enableMp4"]; auto active = allArgs["active"]; auto page = allArgs["page"]; auto pageSize = allArgs["pageSize"]; if (searchText.empty()) { searchText = ""; } if (page.empty()) { page = 1; } if (pageSize.empty()) { pageSize = 4; } auto channels = searchChannels(searchText, enableMp4, active, page, pageSize); auto channelsData = channels.isNull() ? Json::Value(ValueType::arrayValue) : channels; val["data"] = channelsData; val["total"] = countChannels(searchText, enableMp4, active); val["page"] = page; val["pageSize"] = pageSize; val["code"] = API::Success; val["msg"] = "success"; }) //chenxiaolei 获取通道配置(单个详情) //http://127.0.0.1:8099/index/api/searchChannelConfig API_REGIST(api, searchChannelConfig, { CHECK_SECRET(); CHECK_ARGS("id"); int id = atoi(allArgs["id"].c_str()); Json::Value ret = searchChannel(id); if (ret.isNull()) { val["code"] = API::InvalidArgs; val["msg"] = "未找到指定通道"; } else { string sIp = headerIn["Host"]; string sPort = "80"; auto mh_pos = sIp.find(":"); if (mh_pos != string::npos) { sPort = sIp.substr(mh_pos + 1); sIp = sIp.substr(0, mh_pos); } Json::Value playAddrs; if (ret["enable_hls"].asInt()) { playAddrs["hls"] = "http://" + sIp + ":" + sPort + "/" + ret["app"].asString() + "/" + ret["stream"].asString() + "/hls.m3u8"; } playAddrs["rtmp"] = "rtmp://" + sIp + ":" + mINI::Instance()[Rtmp::kPort] + "/" + ret["app"].asString() + "/" + ret["stream"].asString(); playAddrs["rtsp"] = "rtsp://" + sIp + ":" + mINI::Instance()[Rtsp::kPort] + "/" + ret["app"].asString() + "/" + ret["stream"].asString(); playAddrs["flv"] = "http://" + sIp + ":" + sPort + "/" + ret["app"].asString() + "/" + ret["stream"].asString() + ".flv"; playAddrs["ws"] = "ws://" + sIp + ":" + sPort + "/" + ret["app"].asString() + "/" + ret["stream"].asString() + ".flv"; playAddrs["snapshot"] = "http://" + sIp + ":" + sPort + "/snapshot/" + ret["app"].asString() + "/" + ret["stream"].asString() + ".png"; ret["play_addrs"] = playAddrs; val["data"] = ret; val["code"] = API::Success; val["msg"] = "success"; } }) //chenxiaolei 根据 vhost,app,stream,获取通道配置(单个详情) //http://127.0.0.1:8099/index/api/searchChannelConfigByVas API_REGIST(api, searchChannelConfigByVas, { CHECK_SECRET(); CHECK_ARGS("vhost", "app", "stream"); Json::Value ret = searchChannel(allArgs["vhost"], allArgs["app"], allArgs["stream"]); if (ret.isNull()) { val["code"] = API::InvalidArgs; val["msg"] = "未找到指定通道"; } else { string sIp = headerIn["Host"]; string sPort = "80"; auto mh_pos = sIp.find(":"); if (mh_pos != string::npos) { sPort = sIp.substr(mh_pos + 1); sIp = sIp.substr(0, mh_pos); } Json::Value playAddrs; if (ret["enable_hls"].asInt()) { playAddrs["hls"] = "http://" + sIp + ":" + sPort + "/" + ret["app"].asString() + "/" + ret["stream"].asString() + "/hls.m3u8"; } playAddrs["rtmp"] = "rtmp://" + sIp + ":" + mINI::Instance()[Rtmp::kPort] + "/" + ret["app"].asString() + "/" + ret["stream"].asString(); playAddrs["rtsp"] = "rtsp://" + sIp + ":" + mINI::Instance()[Rtsp::kPort] + "/" + ret["app"].asString() + "/" + ret["stream"].asString(); playAddrs["flv"] = "http://" + sIp + ":" + sPort + "/" + ret["app"].asString() + "/" + ret["stream"].asString() + ".flv"; playAddrs["snapshot"] = "http://" + sIp + ":" + sPort + "/snapshot/" + ret["app"].asString() + "/" + ret["stream"].asString() + ".png"; ret["play_addrs"] = playAddrs; val["data"] = ret; val["code"] = API::Success; val["msg"] = "success"; } }) //chenxiaolei 获取录像列表(按月) //http://127.0.0.1:8099/index/api/queryRecordMonthly?app=pzstll&stream=stream_4&period=201906 API_REGIST(api, queryRecordMonthly, { CHECK_SECRET(); CHECK_ARGS("app", "stream", "period"); GET_CONFIG(string, recordAppName, Record::kAppName); GET_CONFIG(string, recordPath, Record::kFilePath); GET_CONFIG(bool, enableVhost, General::kEnableVhost); auto _vhost = allArgs["vhost"]; auto _app = allArgs["app"]; auto _stream = allArgs["stream"]; auto _period = allArgs["period"]; if (!std::regex_match(_period, std::regex("\\d{6}"))) { throw InvalidArgsException("period参数格式错误(YYYYMM)"); } if (_vhost.empty()) { _vhost = DEFAULT_VHOST; } string mp4StreamPath; if (enableVhost) { mp4StreamPath = recordPath + "/" + _vhost + "/" + recordAppName + "/" + _app + "/" + _stream; } else { mp4StreamPath = recordPath + "/" + recordAppName + "/" + _app + "/" + _stream; } Json::Value nVal; nVal["vhost"] = _vhost.data(); nVal["app"] = _app.data(); nVal["stream"] = _stream.data(); nVal["app"] = _app.data(); int dayOfMonth = getNumberOfDays(_period); char *dayOfMonthRecordExists = new char[dayOfMonth]; for (int i = 0; i < dayOfMonth; i++) { int d = i + 1; string dStr = to_string(d); string fDay = (d < 10) ? ("0" + dStr) : dStr; dayOfMonthRecordExists[i] = directoryMp4RecordExists(mp4StreamPath + "/" + _period + fDay) ? '1' : '0'; } WarnL << "_period:" << _period; WarnL << "dayOfMonth:" << dayOfMonth; WarnL << "dayOfMonthRecordExists:" << dayOfMonthRecordExists; //flag 由查询月的天数长度的0,1字符串组成, 0就是没有录像,1就是有录像, 如: 001010000000000000000000000000, 意思就是3号,5号有录像 nVal["flag"] = std::string(dayOfMonthRecordExists, dayOfMonth); val["data"] = nVal; val["code"] = API::Success; val["msg"] = "success"; }) //chenxiaolei 获取录像列表(按天) //http://127.0.0.1:8099/index/api/queryRecordDaily?app=pzstll&stream=stream_4&period=20190618 API_REGIST(api, queryRecordDaily, { CHECK_SECRET(); CHECK_ARGS("app", "stream", "period"); GET_CONFIG(string, recordAppName, Record::kAppName); GET_CONFIG(string, recordPath, Record::kFilePath); GET_CONFIG(bool, enableVhost, General::kEnableVhost); auto _vhost = allArgs["vhost"]; auto _app = allArgs["app"]; auto _stream = allArgs["stream"]; auto _period = allArgs["period"]; if (!std::regex_match(_period, std::regex("\\d{8}"))) { throw InvalidArgsException("period参数格式错误(YYYYMMDD)"); } if (_vhost.empty()) { _vhost = DEFAULT_VHOST; } string mp4FilePath; if (enableVhost) { mp4FilePath = recordPath + "/" + _vhost + "/" + recordAppName + "/" + _app + "/" + _stream + "/" + _period; } else { mp4FilePath = recordPath + "/" + recordAppName + "/" + _app + "/" + _stream + "/" + _period; } Json::Value nVal; nVal["vhost"] = _vhost.data(); nVal["app"] = _app.data(); nVal["stream"] = _stream.data(); nVal["app"] = _app.data(); struct dirent **namelist; int n; n = scandir(mp4FilePath.c_str(), &namelist, 0, alphasort); if (n < 0) { val["code"] = API::InvalidArgs; val["msg"] = "未找到录像文件"; } else { string sIp = headerIn["Host"]; string sPort = "80"; auto mh_pos = sIp.find(":"); if (mh_pos != string::npos) { sPort = sIp.substr(mh_pos + 1); sIp = sIp.substr(0, mh_pos); } int index = 0; while (index < n) { printf("d_name: %s\n", namelist[index]->d_name); if (std::regex_match(namelist[index]->d_name, std::regex("\\d{2}-\\d{2}-\\d{2}\\.mp4\\.json"))) { Json::Value recordInfo; // will contain the root value after parsing. std::ifstream stream(mp4FilePath + "/" + namelist[index]->d_name, std::ifstream::binary); stream >> recordInfo; //nVal["list"].append(de->d_name); recordInfo["mp4Full"] = "http://" + sIp + ":" + sPort + "/" + recordInfo["mp4"].asString(); nVal["list"].append(recordInfo); } free(namelist[index]); index++; } free(namelist); val["code"] = API::Success; val["msg"] = "success"; } val["data"] = nVal; }); //chenxiaolei 保活配置的通道转发的的直播拉流,解决某些场景下,按需播放的通道依靠kBroadcastNotFoundStream事件导致播放时总是先要黑屏1,2秒的问题, 前端可以在加载播放器之前提前调用此接口预先把流拉上 //TODO 此接口有点问题,打不到预想效果,待后面处理 //http://127.0.0.1:8099/index/api/touchChannelProxyStream?app=pzstll&stream=stream_33 API_REGIST(api, touchChannelProxyStream, { CHECK_SECRET(); CHECK_ARGS("app", "stream"); auto _vhost = allArgs["vhost"]; auto _app = allArgs["app"]; auto _stream = allArgs["stream"]; auto _schema = allArgs["schema"]; if (_vhost.empty()) { _vhost = DEFAULT_VHOST; } if (_schema.empty()) { _schema = "RTMP"; } Json::Value nVal; nVal["vhost"] = _vhost.data(); nVal["app"] = _app.data(); nVal["stream"] = _stream.data(); nVal["app"] = _app.data(); string sIp = headerIn["Host"]; string sPort = "80"; auto mh_pos = sIp.find(":"); if (mh_pos != string::npos) { sPort = sIp.substr(mh_pos + 1); sIp = sIp.substr(0, mh_pos); } Json::Value tProxyData = searchChannel(_vhost, _app, _stream); if (!tProxyData.isNull()) { InfoL << "为频道重新拉流:" << _schema << "/" << _vhost << "/" << _app << "/" << _stream; processProxyCfg(tProxyData, false); nVal["hls"] = "http://" + sIp + ":" + sPort + "/" + _app + "/" + _stream + "/hls.m3u8"; nVal["rtmp"] = "rtmp://" + sIp + ":" + mINI::Instance()[Rtmp::kPort] + "/" + _app + "/" + _stream; nVal["rtsp"] = "rtsp://" + sIp + ":" + mINI::Instance()[Rtsp::kPort] + "/" + _app + "/" + _stream; nVal["flv"] = "http://" + sIp + ":" + sPort + "/" + _app + "/" + _stream + ".flv"; nVal["ws"] = "ws://" + sIp + ":" + sPort + "/" + _app + "/" + _stream + ".flv"; nVal["snapshot"] = "http://" + sIp + ":" + sPort + "/snapshot/" + _app + "/" + _stream + ".png"; val["code"] = API::Success; val["msg"] = "success"; } else { val["code"] = API::InvalidArgs; val["msg"] = "频道未找到: " + _schema + "/" + _vhost + "/" + _app + "/" + _stream + ""; } val["data"] = nVal; }); //获取服务器api列表 //测试url http://127.0.0.1/index/api/getApiList API_REGIST(api,getApiList,{ CHECK_SECRET(); for(auto &pr : s_map_api){ val["data"].append(pr.first); } //chenxiaolei 统一每一个接口都必须返回 code val["code"] = API::Success; }); #if !defined(_WIN32) //重启服务器,只有Daemon方式才能重启,否则是直接关闭! //测试url http://127.0.0.1/index/api/restartServer API_REGIST(api,restartServer,{ CHECK_SECRET(); EventPollerPool::Instance().getPoller()->doDelayTask(1000,[](){ //尝试正常退出 ::kill(getpid(), SIGINT); //3秒后强制退出 EventPollerPool::Instance().getPoller()->doDelayTask(3000,[](){ exit(0); return 0; }); return 0; }); val["msg"] = "服务器将在一秒后自动重启"; //chenxiaolei 统一每一个接口都必须返回 code val["code"] = API::Success; }); #endif//#if !defined(_WIN32) //获取流列表,可选筛选参数 //测试url0(获取所有流) http://127.0.0.1/index/api/getMediaList //测试url1(获取虚拟主机为"__defaultVost__"的流) http://127.0.0.1/index/api/getMediaList?vhost=__defaultVost__ //测试url2(获取rtsp类型的流) http://127.0.0.1/index/api/getMediaList?schema=rtsp API_REGIST(api,getMediaList,{ CHECK_SECRET(); //获取所有MediaSource列表 val["code"] = API::Success; val["msg"] = "success"; MediaSource::for_each_media([&](const string &schema, const string &vhost, const string &app, const string &stream, const MediaSource::Ptr &media){ if(!allArgs["schema"].empty() && allArgs["schema"] != schema){ return; } if(!allArgs["vhost"].empty() && allArgs["vhost"] != vhost){ return; } if(!allArgs["app"].empty() && allArgs["app"] != app){ return; } Value item; item["schema"] = schema; item["vhost"] = vhost; item["app"] = app; item["stream"] = stream; val["data"].append(item); }); }); //主动关断流,包括关断拉流、推流 //测试url http://127.0.0.1/index/api/close_stream?schema=rtsp&vhost=__defaultVhost__&app=live&stream=obs&force=1 API_REGIST(api,close_stream,{ CHECK_SECRET(); CHECK_ARGS("schema","vhost","app","stream"); //踢掉推流器 auto src = MediaSource::find(allArgs["schema"], allArgs["vhost"], allArgs["app"], allArgs["stream"]); if(src){ bool flag = src->close(allArgs["force"].as()); val["code"] = flag ? 0 : -1; val["msg"] = flag ? "success" : "close failed"; }else{ val["code"] = -2; val["msg"] = "can not find the stream"; } }); //获取所有TcpSession列表信息 //可以根据本地端口和远端ip来筛选 //测试url(筛选某端口下的tcp会话) http://127.0.0.1/index/api/getAllSession?local_port=1935 API_REGIST(api,getAllSession,{ CHECK_SECRET(); Value jsession; uint16_t local_port = allArgs["local_port"].as(); string &peer_ip = allArgs["peer_ip"]; SessionMap::Instance().for_each_session([&](const string &id,const TcpSession::Ptr &session){ if(local_port != API::Success && local_port != session->get_local_port()){ return; } if(!peer_ip.empty() && peer_ip != session->get_peer_ip()){ return; } jsession["peer_ip"] = session->get_peer_ip(); jsession["peer_port"] = session->get_peer_port(); jsession["local_ip"] = session->get_local_ip(); jsession["local_port"] = session->get_local_port(); jsession["id"] = id; jsession["typeid"] = typeid(*session).name(); val["data"].append(jsession); }); //chenxiaolei 统一每一个接口都必须返回 code val["code"] = API::Success; }); //断开tcp连接,比如说可以断开rtsp、rtmp播放器等 //测试url http://127.0.0.1/index/api/kick_session?id=123456 //TODO 此接口有些问题,HTTP-FLV的 TCP 无法断开 API_REGIST(api,kick_session,{ CHECK_SECRET(); CHECK_ARGS("id"); //踢掉tcp会话 auto session = SessionMap::Instance().get(allArgs["id"]); if(!session){ val["code"] = API::OtherFailed; val["msg"] = "can not find the target"; return; } session->safeShutdown(); val["code"] = API::Success; val["msg"] = "success"; }); static auto addStreamProxy = [](const string &vhost, const string &app, const string &stream, const string &url, bool enable_rtsp, bool enable_rtmp, bool enable_hls, bool enable_mp4, int rtp_type, const function &cb){ auto key = getProxyKey(vhost,app,stream); lock_guard lck(s_proxyMapMtx); if(s_proxyMap.find(key) != s_proxyMap.end()){ //已经在拉流了 cb(SockException(Err_success),key); return; } //添加拉流代理 PlayerProxy::Ptr player(new PlayerProxy(vhost,app,stream,enable_rtsp,enable_rtmp,enable_hls,enable_mp4)); s_proxyMap[key] = player; //指定RTP over TCP(播放rtsp时有效) (*player)[kRtpType] = rtp_type; //开始播放,如果播放失败或者播放中止,将会自动重试若干次,默认一直重试 player->setPlayCallbackOnce([cb,key](const SockException &ex){ if(ex){ lock_guard lck(s_proxyMapMtx); s_proxyMap.erase(key); } cb(ex,key); }); //被主动关闭拉流 player->setOnClose([key](){ lock_guard lck(s_proxyMapMtx); s_proxyMap.erase(key); }); player->play(url); }; //动态添加rtsp/rtmp拉流代理 //测试url http://127.0.0.1/index/api/addStreamProxy?vhost=__defaultVhost__&app=proxy&enable_rtsp=1&enable_rtmp=1&stream=0&url=rtmp://127.0.0.1/live/obs API_REGIST_INVOKER(api,addStreamProxy,{ CHECK_SECRET(); CHECK_ARGS("vhost","app","stream","url","enable_rtsp","enable_rtmp"); addStreamProxy(allArgs["vhost"], allArgs["app"], allArgs["stream"], allArgs["url"], allArgs["enable_rtsp"],/* 是否rtsp转发 */ allArgs["enable_rtmp"],/* 是否rtmp转发 */ allArgs["enable_hls"],/* 是否hls转发 */ allArgs["enable_mp4"],/* 是否MP4录制 */ allArgs["rtp_type"], [invoker,val,headerOut](const SockException &ex,const string &key){ if(ex){ const_cast(val)["code"] = API::OtherFailed; const_cast(val)["msg"] = ex.what(); }else{ const_cast(val)["data"]["key"] = key; } invoker("200 OK", headerOut, val.toStyledString()); }); //chenxiaolei 统一每一个接口都必须返回 code val["code"] = API::Success; }); //关闭拉流代理 //测试url http://127.0.0.1/index/api/delStreamProxy?key=__defaultVhost__/proxy/0 API_REGIST(api,delStreamProxy,{ CHECK_SECRET(); CHECK_ARGS("key"); lock_guard lck(s_proxyMapMtx); val["data"]["flag"] = s_proxyMap.erase(allArgs["key"]) == 1; //chenxiaolei 统一每一个接口都必须返回 code val["code"] = API::Success; }); #if !defined(_WIN32) static auto addFFmepgSource = [](const string &src_url, const string &dst_url, int timeout_ms, const function &cb){ auto key = MD5(dst_url).hexdigest(); lock_guard lck(s_ffmpegMapMtx); if(s_ffmpegMap.find(key) != s_ffmpegMap.end()){ //已经在拉流了 cb(SockException(Err_success),key); return; } FFmpegSource::Ptr ffmpeg = std::make_shared(); s_ffmpegMap[key] = ffmpeg; ffmpeg->setOnClose([key](){ lock_guard lck(s_ffmpegMapMtx); s_ffmpegMap.erase(key); }); //chenxiaolei 支持单独为每一次 play 单独配置 ffmpeg 参数 ffmpeg->play(src_url, dst_url, timeout_ms, "", [cb, key](const SockException &ex) { if (ex) { lock_guard lck(s_ffmpegMapMtx); s_ffmpegMap.erase(key); } cb(ex, key); }); }; //动态添加rtsp/rtmp拉流代理 //测试url http://127.0.0.1/index/api/addFFmpegSource?src_url=http://live.hkstv.hk.lxdns.com/live/hks2/playlist.m3u8&dst_url=rtmp://127.0.0.1/live/hks2&timeout_ms=10000 API_REGIST_INVOKER(api,addFFmpegSource,{ CHECK_SECRET(); CHECK_ARGS("src_url","dst_url","timeout_ms"); auto src_url = allArgs["src_url"]; auto dst_url = allArgs["dst_url"]; int timeout_ms = allArgs["timeout_ms"]; addFFmepgSource(src_url,dst_url,timeout_ms,[invoker,val,headerOut](const SockException &ex,const string &key){ if(ex){ const_cast(val)["code"] = API::OtherFailed; const_cast(val)["msg"] = ex.what(); }else{ const_cast(val)["data"]["key"] = key; } invoker("200 OK", headerOut, val.toStyledString()); }); }); //关闭拉流代理 //测试url http://127.0.0.1/index/api/delFFmepgSource?key=key API_REGIST(api,delFFmepgSource,{ CHECK_SECRET(); CHECK_ARGS("key"); lock_guard lck(s_ffmpegMapMtx); val["data"]["flag"] = s_ffmpegMap.erase(allArgs["key"]) == 1; //chenxiaolei 统一每一个接口都必须返回 code val["code"] = API::Success; }); #endif ////////////以下是注册的Hook API//////////// API_REGIST(hook,on_publish,{ //开始推流事件 throw SuccessException(); }); API_REGIST(hook,on_play,{ //开始播放事件 throw SuccessException(); }); API_REGIST(hook,on_flow_report,{ //流量统计hook api throw SuccessException(); }); API_REGIST(hook,on_rtsp_realm,{ //rtsp是否需要鉴权,默认需要鉴权 val["code"] = API::Success; val["realm"] = "zlmediakit_reaml"; }); API_REGIST(hook,on_rtsp_auth,{ //rtsp鉴权密码,密码等于用户名 //rtsp可以有双重鉴权!后面还会触发on_play事件 CHECK_ARGS("user_name"); val["code"] = API::Success; val["encrypted"] = false; val["passwd"] = allArgs["user_name"].data(); }); API_REGIST(hook,on_stream_changed,{ //媒体注册或反注册事件 throw SuccessException(); }); #if !defined(_WIN32) API_REGIST_INVOKER(hook,on_stream_not_found_ffmpeg,{ //媒体未找到事件,我们都及时拉流hks作为替代品,目的是为了测试按需拉流 /* CHECK_SECRET(); CHECK_ARGS("vhost","app","stream"); //通过FFmpeg按需拉流 GET_CONFIG(int,rtmp_port,Rtmp::kPort); GET_CONFIG(int,timeout_sec,Hook::kTimeoutSec); string dst_url = StrPrinter << "rtmp://127.0.0.1:" << rtmp_port << "/" << allArgs["app"] << "/" << allArgs["stream"] << "?vhost=" << allArgs["vhost"]; addFFmepgSource("http://live.hkstv.hk.lxdns.com/live/hks2/playlist.m3u8",*//** ffmpeg拉流支持任意编码格式任意协议 **//* dst_url, (1000 * timeout_sec) - 500, [invoker,val,headerOut](const SockException &ex,const string &key){ if(ex){ const_cast(val)["code"] = API::OtherFailed; const_cast(val)["msg"] = ex.what(); }else{ const_cast(val)["data"]["key"] = key; } invoker("200 OK", headerOut, val.toStyledString()); });*/ }); #endif//!defined(_WIN32) API_REGIST_INVOKER(hook,on_stream_not_found,{ //媒体未找到事件,我们都及时拉流hks作为替代品,目的是为了测试按需拉流 CHECK_SECRET(); CHECK_ARGS("vhost","app","stream"); //通过内置支持的rtsp/rtmp按需拉流 addStreamProxy(allArgs["vhost"], allArgs["app"], allArgs["stream"], /** 支持rtsp和rtmp方式拉流 ,rtsp支持h265/h264/aac,rtmp仅支持h264/aac **/ "rtsp://184.72.239.149/vod/mp4:BigBuckBunny_115k.mov", true,/* 开启rtsp转发 */ true,/* 开启rtmp转发 */ true,/* 开启hls转发 */ false,/* 禁用MP4录制 */ 0,//rtp over tcp方式拉流 [invoker,val,headerOut](const SockException &ex,const string &key){ if(ex){ const_cast(val)["code"] = API::OtherFailed; const_cast(val)["msg"] = ex.what(); }else{ const_cast(val)["data"]["key"] = key; } invoker("200 OK", headerOut, val.toStyledString()); }); }); API_REGIST(hook,on_record_mp4,{ //录制mp4分片完毕事件 throw SuccessException(); }); API_REGIST(hook,on_shell_login,{ //shell登录调试事件 throw SuccessException(); }); API_REGIST(hook,on_stream_none_reader,{ //无人观看流默认关闭 val["close"] = true; }); static auto checkAccess = [](const string ¶ms){ //我们假定大家都要权限访问 return true; }; API_REGIST(hook,on_http_access,{ //在这里根据allArgs["params"](url参数)来判断该http客户端是否有权限访问该文件 if(!checkAccess(allArgs["params"])){ //无访问权限 val["err"] = "无访问权限"; //仅限制访问当前目录 val["path"] = ""; //标记该客户端无权限1分钟 val["second"] = 60; return; } //可以访问 val["err"] = ""; //只能访问当前目录 val["path"] = ""; //该http客户端用户被授予10分钟的访问权限,该权限仅限访问当前目录 val["second"] = 10 * 60; }); } void unInstallWebApi(){ { lock_guard lck(s_proxyMapMtx); s_proxyMap.clear(); } #if !defined(_WIN32) { lock_guard lck(s_ffmpegMapMtx); s_ffmpegMap.clear(); } #endif }