服务端上传视频处理
背景
之前我们完成了服务端系统对终端的控制功能,到此时,终端的功能实现已经基本就位,接下来,需要完成服务端对上传视频的处理,这个工作相对来说会比较简单,首先,我们不需要要求并行工作,其次,我之前完成的测试代码可以起到很好的借鉴作用。
这个部分和服务端控制部分同理,依旧需要使用Java native方法,不同的是,这次的两个功能更像是函数,不再涉及任何状态信息(上一部分还需要涉及端口描述符的开闭状态)
关键点
这一部分,我们的工作有如下三个关键点
如何将视频和音频分开
这个地方我们的解决方案非常简单,使用ffmpeg这个软件即可完成我们的工作,这个软件采用命令行工作方式,所以我们只需要简单的exec就可以完成音视频的分离工作
如何将音视频切成需要的样子
这个问题我们在前面的文章中已经讨论过了,具体实现沿用从前的办法
问题修正
在本次实现过程中,我也发现了从前代码中的一些问题,在我们这个环境下必须解决
-
截取、传输相关问题:
web端是直接上传视频文件,并本地处理成图片、音频片段,交给各种服务进行处理,这其中并不需要传输过程,而过去我们为了方便,将截取和传输的过程紧密关联了起来,造成了现阶段web端实现的麻烦,难以实现代码复用,所以我们取消文件发送和音视频截取的高度相关
过去的相关现象,是由于我直接在生成新文件的地方将新文件的文件名加入了传输链表(在捕获类的方法中),这样做的代价是在我不需要文件传输的时候,那个根本用不着的链表里面也多了一大堆没用的东西
所以,这里我采取的解决方法是继续修改捕获类,让捕获类方法的调用者决定是否将文件名加入链表,具体方法是为可能产生新文件的
public
方法添加了一个string&
类型的输出参数,调用者必须提供一个string
对象,产生的结果是否有效由先前定义的返回值决定,是否利用这个返回值由调用者决定,具体如下- run_tick
/** * @brief VoiCap::run_tick * 文件输入的倒采样点方法,从一个缓冲区移动到另一个缓冲区,不存在计算量和实时性的问题 * * 我们使用变量noise_level来表示,如果_quiet_thresh个波内没有明显超过噪声等级( > noise_level*_env_factor )的波形, * 我们认为接下来进入安静时间并停止采样,停止采样后,第一个超过该值的波形将使采样继续 * * noise_level动态确定:当样本点被视为环境噪声时,我们记录下这个样本点的值,当环境噪音数量累计达到_recalc_ratio的时候, * 我们用累计值的平均值作为修正后的环噪等级 *---------------------------------------2018-05-30------------------------------------------------- * @param file_name_maybe 可能产生的新文件名,是否真的赋过值由返回值决定 * @return 0 没有文件 1有文件 -1发生错误 */ int VoiCap::run_tick(string& file_name_maybe) { StkFrames frames; StkFloat level; int fcnt = _buffer.nextWindow(frames, *input); level = _buffer.getLevel(); total_cnt += fcnt; if(level < noise_level*_env_factor || fcnt < FREQUANCY_UNL || fcnt > FREQUANCY_UPL)//多进行一次频率的判断 { //环噪相关计数增加 sum_noise += level; noise_cnt++; cont_noise_cnt++; //信号相关计数清0 sum_sig = 0.0; sig_cnt = 0; //如果达到重算阈 if(noise_cnt == _recalc_thresh) { //重新计算环噪 noise_level = sum_noise / _recalc_thresh; /*Debug*/ //printf("new noise level: %f\n",noise_level); //计数清0 noise_cnt = 0; cont_noise_cnt = 0; sum_noise = .0; } else if(noise_cnt >= _quiet_thresh) { //停止采样,如果时机合适,进行重定向 if(need_redirect) { cout << "redircecting output file " << file_cnt << endl; need_redirect = false; return redirect(file_name_maybe); } } } else { //信号相关计数增加,环噪相关数据就不用清0了?? //2018年05月12日 不用清零才怪!!这里属于功能复用不合理:重新计算noise_level要求不清0,判定quiet_thresh要求清零,办法就是再加一个成员 sum_sig += level; sig_cnt++; cont_noise_cnt = 0; if(sig_cnt == _raise_thresh) { //达到提升阈,重新计算 noise_level = sum_sig / _raise_thresh; printf("new noise level: %f\n",noise_level); //计数清0 sig_cnt = 0; sum_sig = .0; } } frame_cnt += fcnt; output->tick(frames); /*debug*/ //cout << "time: " << total_cnt/Stk::sampleRate() << " level: " << level << " noise_level: " << noise_level <<endl; if( frame_cnt >= 50 * (uint32_t) Stk::sampleRate() ) { need_redirect = true; } return 0; }
- redirect
/** * @brief VoiCap::redirect * 当输出变更条件触发时,变更输出对象 * -------------2018-05-30------------ * @param buf 添加了输出参数,用来将新生成的文件名传出去 * @return 输出变更是否成功 */ int VoiCap::redirect(string& buf) { try { output->closeFile(); /*2018-05-15修改 在文件切换的时候完成链表更新*/ buf = prefix + to_string(file_cnt) + ".wav"; //name_list.put(file_name.c_str()); (2018-05-30:不要了) output->openFile(prefix + to_string(++file_cnt), 1, FileWrite::FILE_WAV, Stk::STK_SINT16); frame_cnt = 0; } catch (StkError &) { return -1; } return 0; }
- storePic
/** * @brief PicCap::storePic * 所有子类都通过这个方法将图片存入文件 * @param img _in_ 图片指针,可能为空,代表图片获取失败 * -----------2018-05-30-------------- * @param file_name 输出参数,决定是否返回 * @return 存储是否成功,如果存储成功,输入参数指向的对象会被删除 */ bool PicCap::storePic(Mat *img, string& file_name) { if(!cap) return false; //cap没打开,不用玩了 if(current_stored) return false; //图片已经存过了,不用玩了 bool res = false; file_name = prefix + to_string(proc_cnt++) + ".jpg"; try { res = imwrite(file_name, *img); /* * 2018年05月15日修改确定的功能 * 如果存储没出问题,那就直接把文件加入传输列表 */ //name_list.put(file_name.c_str()); 放弃这种方式(2018-05-30) } catch (const cv::Exception& ex) { fprintf(stderr, "Exception converting image to PNG format: %s\n", ex.what()); } delete img; //释放图片占用的空间 current_stored = true; //修改后的返回方式(2018-05-30) return res; }
- pic_thread
static void* pic_thread(void* raw_args) { func_args* args = (func_args*) raw_args; CamCap* cap; string file_nm; while(1) { sem_wait(&args->start_sem); printf("pic cap started...\n"); cap = new CamCap("",0); while(cur_status == STAT_STARTED) { Mat* pic = cap->getPic(); if(cap->storePic(pic,file_nm)) { name_list.put(file_nm.c_str()); } } delete cap; printf("pic cap fin...\n"); } }
- voi_thread
static void* voi_thread(void* raw_args) { func_args* args = (func_args*) raw_args; MicCap* cap; string file_nm; while(1) { sem_wait(&args->start_sem); printf("voi cap started...\n"); cap = new MicCap("",0); cap->start(); while(cur_status == STAT_STARTED) { if(cap->run_tick(file_nm) == 1) { name_list.put(file_nm.c_str()); } } cap->finish(file_nm); name_list.put(file_nm.c_str()); delete cap; printf("voi cap fin...\n"); } }
-
图片截取类型的结束条件问题
和音频的处理类不同,视频的处理类我在最初的时候并没有添加总帧数统计,这就产生了无法判断结束条件的问题,当是的测试代码并没有体现出这个问题。这个地方从前是直接抛出异常杀死进程,但在web应用环境中,这显然是不可取的,所以我们必须解决这个问题,方法是添加相关的变量来表示视频处理终止条件
class VideoCap : public PicCap { public: VideoCap(const string& path, const string& fileName); bool hasNextPic(); cv::Mat* getPic(); private: string inputFile; //对应视频文件名 /*-----------2018-05-30-----------*/ uint32_t total_frames; //视频总帧数 uint32_t passed_frames; //已经取出的帧数 };
Mat* VideoCap::getPic() { if(!cap) return NULL; if(!current_stored) return NULL; /* * TODO * 通过cap的get方法可以get到视频的FPS:CAP_PROP_FPS * 同时也可以得到视频总共帧的数量:CAP_PROP_FRAME_COUNT * 按帧截取思路如下: * - 根据视频帧数计算出视频的帧数比需要的帧数快几倍 * - 连续读出倍数张图片,保存其中一张 */ Mat* ret = new Mat(); int fps = cap->get(CAP_PROP_FPS); int padding = fps/FPS - 1; for(int i = 0; i < padding && hasNextPic(); i++) { cap->grab(); passed_frames++; } cap->retrieve(*ret); current_stored = false; return ret; } bool VideoCap::hasNextPic() { return passed_frames < total_frames; }
实现
-
Java定义
package srv_work; import java.io.IOException; public class PVlib { static{ System.loadLibrary("pilib"); } /** * 将视频转换成音频和图片 * @param vPath 视频的路径(文件) * @param pPath 音频存放的路径(文件夹) * @param sPath 图片存放的路径(文件夹) * @return */ public static int split_video(String vPath, String pPath, String sPath) throws IOException, InterruptedException { String tmpPath = "tmp.wav"; //分离音视频 Process ffmpeg = Runtime.getRuntime().exec("ffmpeg -i -y"+ vPath +" "+tmpPath); ffmpeg.waitFor(); //分别处理 lib_getPic(vPath, pPath); lib_getVoi(tmpPath, sPath); return 0; } /** * 这两个方法完成的工作就是终端上的线程完成的工作 * @param srcPath 源路径,对应一个文件 * @param destDir 目的路径,对应一个目录 * @return */ public native static int lib_getPic(String srcPath, String destDir); public native static int lib_getVoi(String srcPath, String destDir); }
-
C++本地方法
#include "PVlib.h" #include "cam/piccap.h" #include "mic/voicap.h" #include <iostream> JNIEXPORT jint JNICALL Java_srv_1work_PVlib_lib_1getPic (JNIEnv *env, jclass cls, jstring jsrc, jstring jdest) { const char* src = env->GetStringUTFChars(jsrc, 0); const char* dest = env->GetStringUTFChars(jdest, 0); jint res_cnt = 0; std::cout << "voi cap started...\n"; std::string file_nm; PicCap *cap = new VideoCap(dest, src); while(cap->hasNextPic()) { Mat* pic = cap->getPic(); cap->storePic(pic,file_nm); res_cnt++; } delete cap; env->ReleaseStringUTFChars(jsrc, src); env->ReleaseStringUTFChars(jdest, dest); std::cout << "voi cap fin...\n"; return res_cnt; } JNIEXPORT jint JNICALL Java_srv_1work_PVlib_lib_1getVoi (JNIEnv *env, jclass cls, jstring jsrc, jstring jdest) { const char* src = env->GetStringUTFChars(jsrc, 0); const char* dest = env->GetStringUTFChars(jdest, 0); jint res_cnt = 0; std::cout << "voi cap started...\n"; std::string file_nm; AudioCap *cap = new AudioCap(dest, src); cap->start(); while(cap->hasNextTick()) { cap->run_tick(file_nm); } cap->finish(file_nm); delete cap; env->ReleaseStringUTFChars(jsrc, src); env->ReleaseStringUTFChars(jdest, dest); std::cout << "voi cap fin...\n"; return res_cnt; }
小结
至此,服务端和终端基础设施搭建完成,下面只需要专心服务端业务逻辑的搭建和给予深度学习的识别系统的搭建与调试