diff --git a/conf/config.ini b/conf/config.ini index 02fc096c..d87c9547 100644 --- a/conf/config.ini +++ b/conf/config.ini @@ -4,16 +4,21 @@ apiDebug=1 #一些比较敏感的http api在访问时需要提供secret,否则无权限调用 #如果是通过127.0.0.1访问,那么可以不提供secret secret=035c73f7-bb6b-4889-a715-d9eb2d1925cc +#截图保存路径根目录,截图通过http api(/index/api/makeSnap)生成和获取 +snapRoot=./www/snap/ [ffmpeg] #FFmpeg可执行程序绝对路径 bin=/usr/local/bin/ffmpeg #FFmpeg拉流再推流的命令模板,通过该模板可以设置再编码的一些参数 cmd=%s -re -i %s -c:a aac -strict -2 -ar 44100 -ab 48k -c:v libx264 -f flv %s +#FFmpeg生成截图的命令,可以通过修改该配置改变截图分辨率或质量 +snap=%s -i %s -y -f mjpeg -t 0.001 %s #FFmpeg日志的路径,如果置空则不生成FFmpeg日志 #可以为相对(相对于本可执行程序目录)或绝对路径 log=./ffmpeg/ffmpeg.log + [general] #是否启用虚拟主机 enableVhost=0 diff --git a/server/FFmpegSource.cpp b/server/FFmpegSource.cpp index 12e96345..ec9e918d 100644 --- a/server/FFmpegSource.cpp +++ b/server/FFmpegSource.cpp @@ -13,21 +13,25 @@ #include "Common/MediaSource.h" #include "Util/File.h" #include "System.h" +#include "Thread/WorkThreadPool.h" namespace FFmpeg { #define FFmpeg_FIELD "ffmpeg." const string kBin = FFmpeg_FIELD"bin"; const string kCmd = FFmpeg_FIELD"cmd"; const string kLog = FFmpeg_FIELD"log"; +const string kSnap = FFmpeg_FIELD"snap"; onceToken token([]() { #ifdef _WIN32 string ffmpeg_bin = System::execute("where ffmpeg"); //windows下先关闭FFmpeg日志(目前不支持日志重定向) - mINI::Instance()[kCmd] = "%s -re -i \"%s\" -loglevel quiet -c:a aac -strict -2 -ar 44100 -ab 48k -c:v libx264 -f flv %s "; + mINI::Instance()[kCmd] = "%s -re -i %s -loglevel quiet -c:a aac -strict -2 -ar 44100 -ab 48k -c:v libx264 -f flv %s"; + mINI::Instance()[kSnap] = "%s -i %s -loglevel quiet -y -f mjpeg -t 0.001 %s"; #else string ffmpeg_bin = System::execute("which ffmpeg"); - mINI::Instance()[kCmd] = "%s -re -i \"%s\" -c:a aac -strict -2 -ar 44100 -ab 48k -c:v libx264 -f flv %s "; + mINI::Instance()[kCmd] = "%s -re -i %s -c:a aac -strict -2 -ar 44100 -ab 48k -c:v libx264 -f flv %s"; + mINI::Instance()[kSnap] = "%s -i %s -y -f mjpeg -t 0.001 %s"; #endif //默认ffmpeg命令路径为环境变量中路径 mINI::Instance()[kBin] = ffmpeg_bin.empty() ? "ffmpeg" : ffmpeg_bin; @@ -232,3 +236,31 @@ void FFmpegSource::onGetMediaSource(const MediaSource::Ptr &src) { _listener = src->getListener(); src->setListener(shared_from_this()); } + +void FFmpegSnap::makeSnap(const string &play_url, const string &save_path, float timeout_sec, const function &cb) { + GET_CONFIG(string,ffmpeg_bin,FFmpeg::kBin); + GET_CONFIG(string,ffmpeg_snap,FFmpeg::kSnap); + GET_CONFIG(string,ffmpeg_log,FFmpeg::kLog); + + std::shared_ptr process = std::make_shared(); + auto delayTask = EventPollerPool::Instance().getPoller()->doDelayTask(timeout_sec * 1000,[process,cb](){ + if(process->wait(false)){ + //FFmpeg进程还在运行,超时就关闭它 + process->kill(2000); + } + return 0; + }); + + WorkThreadPool::Instance().getPoller()->async([process,play_url,save_path,delayTask,cb](){ + char cmd[1024] = {0}; + snprintf(cmd, sizeof(cmd),ffmpeg_snap.data(),ffmpeg_bin.data(),play_url.data(),save_path.data()); + process->run(cmd,ffmpeg_log.empty() ? "" : File::absolutePath("",ffmpeg_log)); + //等待FFmpeg进程退出 + process->wait(true); + //FFmpeg进程退出了可以取消定时器了 + delayTask->cancel(); + //执行回调函数 + cb(process->exit_code() == 0); + }); +} + diff --git a/server/FFmpegSource.h b/server/FFmpegSource.h index bbafcb68..b32ec8f8 100644 --- a/server/FFmpegSource.h +++ b/server/FFmpegSource.h @@ -23,6 +23,23 @@ using namespace std; using namespace toolkit; using namespace mediakit; +namespace FFmpeg { + extern const string kSnap; +} + +class FFmpegSnap { +public: + /// 创建截图 + /// \param play_url 播放url地址,只要FFmpeg支持即可 + /// \param save_path 截图jpeg文件保存路径 + /// \param timeout_sec 生成截图超时时间(防止阻塞太久) + /// \param cb 生成截图成功与否回调 + static void makeSnap(const string &play_url, const string &save_path, float timeout_sec, const function &cb); +private: + FFmpegSnap() = delete; + ~FFmpegSnap() = delete; +}; + class FFmpegSource : public std::enable_shared_from_this , public MediaSourceEvent{ public: typedef shared_ptr Ptr; diff --git a/server/WebApi.cpp b/server/WebApi.cpp index 47bb2842..97846243 100644 --- a/server/WebApi.cpp +++ b/server/WebApi.cpp @@ -50,10 +50,13 @@ typedef enum { #define API_FIELD "api." const string kApiDebug = API_FIELD"apiDebug"; const string kSecret = API_FIELD"secret"; +const string kSnapRoot = API_FIELD"snapRoot"; static onceToken token([]() { mINI::Instance()[kApiDebug] = "1"; mINI::Instance()[kSecret] = "035c73f7-bb6b-4889-a715-d9eb2d1925cc"; + mINI::Instance()[kSnapRoot] = "./www/snap/"; + }); }//namespace API @@ -174,7 +177,7 @@ static inline void addHttpListener(){ size = body->remainSize(); } - if(size < 4 * 1024){ + if(size && size < 4 * 1024){ string contentOut = body->readData(size)->toString(); DebugL << "\r\n# request:\r\n" << parser.Method() << " " << parser.FullUrl() << "\r\n" << "# content:\r\n" << parser.Content() << "\r\n" @@ -817,6 +820,63 @@ void installWebApi() { val["data"]["paths"] = paths; }); + GET_CONFIG(string, snap_root, API::kSnapRoot); + + //获取截图缓存或者实时截图 + //http://127.0.0.1/index/api/getSnap?url=rtmp://127.0.0.1/record/robot.mp4&timeout_sec=10&expire_sec=3 + api_regist2("/index/api/getSnap", [](API_ARGS2){ + CHECK_SECRET(); + CHECK_ARGS("url", "timeout_sec", "expire_sec"); + auto file_prefix = MD5(allArgs["url"]).hexdigest() + "_"; + string file_path; + int expire_sec = allArgs["expire_sec"]; + File::scanDir(File::absolutePath(snap_root,""),[&](const string &path, bool isDir){ + if(!isDir){ + auto pos = path.find(file_prefix); + if(pos != string::npos){ + //找到截图 + auto tm = FindField(path.data() + pos + file_prefix.size(), nullptr, ".jpeg"); + if(atoll(tm.data()) + expire_sec < time(NULL)){ + //截图已经过期,删除之,后面重新生成 + File::delete_file(path.data()); + }else{ + //截图未过期 + file_path = path; + } + return false; + } + } + return true; + }); + + if(!file_path.empty()){ + //返回上次生成的截图 + StrCaseMap headerOut; + headerOut["Content-Type"] = HttpFileManager::getContentType(".jpeg"); + invoker.responseFile(headerIn,headerOut,file_path); + return; + } + + //无截图或者截图已经过期 + file_path = File::absolutePath(StrPrinter << file_prefix << time(NULL) << ".jpeg" ,snap_root); +#if !defined(_WIN32) + //创建文件夹 + File::create_path(file_path.c_str(), S_IRWXO | S_IRWXG | S_IRWXU); +#else + File::create_path(file_path.c_str(),0); +#endif + FFmpegSnap::makeSnap(allArgs["url"],file_path,allArgs["timeout_sec"],[invoker,headerIn,file_path](bool success){ + if(!success){ + //生成截图失败,可能残留空文件 + File::delete_file(file_path.data()); + } + + StrCaseMap headerOut; + headerOut["Content-Type"] = HttpFileManager::getContentType(".jpeg"); + invoker.responseFile(headerIn, headerOut, file_path); + }); + }); + ////////////以下是注册的Hook API//////////// api_regist1("/index/hook/on_publish",[](API_ARGS1){ //开始推流事件