原文地址:https://www.zhihu.com/column/c_1273256355848605696
目录
vlc
解码播放
vlc作为解码的内核,主要是因为vlc使用简单方便,直接传入一个句柄即可,简单几行代码就可以实现一个视频流播放
用vlc做视频监控解码的人都会遇到一个问题,那就是鼠标事件被接管拦截了,不能识别鼠标事件,比如双击最大化等。
解决办法
- 设置句柄以后直接将控件/接受视频渲染的控件禁用掉。
回调处理
用句柄来显示视频,方便是很方便,但是有个缺点就是不能拿到实时视频的每张图片的数据。比如绘制各种OSD标签、图形处理等。
解决办法
- 回调
录像存储
vlc的录像功能是内置封装好的,在打开文件的前面设置相应的命令参数即可,如果只是要求整个过程保存成一个视频文件
只要调用libvlc_media_add_option函数设置:sout=#duplicate{dst=file{dst=d:/1.mp4},dst=display}即可
定时保存成单个文件。vlc要动态保存多个文件,这就需要模拟执行录像、停止录像的功能来实现,主要的流程就是通过var_CreateGetString函数拿到录像文件存储路径变量,然后var_SetString设置该变量,最后调用var_ToggleBool来模拟单击了录像,停止录像只需要再次执行一次即可,所以要存储成多个视频文件,只需要动态改变录像文件存储路径这个变量即可。
读取和控制
用vlc做读取和控制这块有两种处理方式,一种是在线程中来定时读取,比如读取播放进度、当前各种状态、当前音量、静音等,还有一种方式是采用事件回调的形式,默认建议事件回调的机制,能够拿到很多事件消息,效率也更高。你只需要在打开视频以前调用libvlc_event_attach订阅自己感兴趣的事件,在不需要的时候比如关闭的时候调用libvlc_event_detach注销订阅的事件即可。
事件订阅
事件订阅可以拿到文件长度、播放进度、播放状态改变等信息,vlc的事件订阅机制封装的比较友好,只需要先创建一个事件管理器,然后逐个订阅自己感兴趣的需要的事件,只有订阅了的事件才能在事件回调中拿到,所有事件的枚举在libvlc_events.h头文件中可以查阅到,在调用libvlc_event_attach订阅事件的时候,第三个参数指定事件回调函数,第四个参数传入用户数据,一般是用来传入类的指针,这样在事件回调的时候,可以直接拿到并转换为类指针,然后使用类中的方法,Qt5中的信号是public的,所以可以直接在回调函数中emit发送信号,而Qt4中的信号是protected的,没法直接emit,所以需要做一个通用的中转函数,用来重新分发信号,通过参数type来控制类型,QVariantList来传入参数集合。
ffmpeg
ffmpeg解码处理
解码过程:
- 注册解码库相关(av_register_all、avformat_network_init等)
- 初始化各种参数比如缓存大小等(av_dict_set)
- 打开视频流或者文件(avformat_alloc_context、avformat_open_input)
- 获取流信息(avformat_find_stream_info)
- 获取视频流并初始化视频解码器(av_find_best_stream、avcodec_find_decoder)
- 获取音频流并初始化音频解码器(av_find_best_stream、avcodec_find_decoder、avcodec_open2)
- 预分配帧内存(av_frame_alloc)
- 循环读取音视频帧(av_read_frame、av_packet_unref)
- 解码视频(avcodec_decode_video2或者avcodec_send_packet、avcodec_receive_frame)
- 解码音频(avcodec_decode_audio4)
- 处理结束释放资源(sws_freeContext、av_frame_free、av_free)
保存裸流
ffmpeg保存视频文件,就是直接保存的裸流数据,裸流数据一般是H264格式的数据。先打开文件,然后在循环解码的地方直接将解码好的数据write到文件即可,如果采用的是定时存储的话,那就开个定时器,到了点就先关闭文件,然后重新打开新的名字的文件,这里要注意的是,rtmp视频流的话,需要添加pps sps等信息,所以在每帧写入文件前,要先用AVBitStreamFilter采用h264_mp4toannexb处理下才行。
录像存储
ffmpeg解码保存成MP4文件,有两种处理方式,一种是先保存成裸流,然后开个后台线程,当裸流文件保存完成以后,自动触发H264转MP4的命令执行,也可以很快的完成转换,另外一种方法就是直接解码的时候保存成MP4文件,两种方法都可以,一般建议后者。
保存成MP4文件流程:
- 调用avformat_alloc_output_context2开辟一个格式上下文AVFormatContext用来处理视频流输出。
- 调用avformat_new_stream开辟一个视频流AVStream用来输出MP4文件。
- 重新设置输出视频流的各种参数。
- 调用avio_open打开输出文件。
- 调用avformat_write_header写入头部标识。
- 循环解码后调用av_write_frame写入数据到文件。
- 结束后调用av_write_trailer写入结束标识。
- 关闭解码输出,关闭文件,释放资源。
控制播放
ffmpeg做视频流解码的时候暂停,如果打开的是本地视频文件,暂停你只需要停止解码即可,但是视频流会发现根本没用,一旦你停止了解码,下次重新解码的时候,还是以前的图片,他是从你最后暂停开始的地方重新解码的。如果想要暂停视频流,正确的做法是照常解码,只是不处理和绘制图片就行,说白了其实就是伪暂停,看起来是暂停了,其实后台还在不断的解码中。
用ffmpeg播放本地文件的时候,如果不加延时,你会发现刷刷几秒钟就播放完了,具体看电脑的性能,性能好的电脑也就几秒钟播放一个5分钟的视频。
延时:在打开流后记住开始的时间,然后解码中取出对应流(视频流或者音频流等)的基准时间time_base,调用av_rescale_q计算出pts时间,然后用av_gettime() - startTime得到当前的时间,用pts_time - now_time得到时间差值,如果是正数,则这个时间就是需要延时的微秒数,注意是微秒数而不是毫秒数哦,直接调用av_usleep来延时即可。
常用命令
- 列出支持的格式:ffmpeg -formats
- 剪切一段媒体文件:ffmpeg -i input.mp4 -ss 00:00:50.0 -codec copy -t 20 output.mp4
- 提取一个视频文件中的音频文件:ffmpeg -i input.mp4 -vn -acodec copy output.m4a
- 视频静音,即只保留视频:ffmpeg -i input.mp4 -an -vcodec copy output.mp4
- 从MP4文件中抽取视频流导出为裸的H264数据:ffmpeg -i output.mp4 -an -vcodec copy -bsf:v h264_mp4toannexb output.h264
- 使用AAC音频数据和H264视频生成MP4文件:ffmpeg -i test.aac -i test.h264 -acodec copy -bsf:a aac_adtstoasc -vcodec copy -f mp4 output.mp4
- 音频格式转换:ffmpeg -i input.wav -acodec libfdk_aac output.aac
- 将一个MP4的文件转换为一个GIF动图:ffmpeg -i input.mp4 -vf scale=100:-1 -t 5 -r 10 image.gif
QProcess他可以直接调用可执行程序或者直接执行命令,然后能够拦截输出打印的信息,管道的形式read出来,这样就非常直观了,可以在调用可执行文件执行的时候,将打印信息全部输出。
mpv
解码播放
mpv是一款基于MPlayer和MPlayer2的多平台开源播放器,是一个开源的,跨平台视频播放器,带有极简的 GUI 界面以及丰富的命令行控制。其在Linux上拥有广泛的输出设备支持,内置ffmpeg解码器,支持绝大部分的视频和音频格式,支持本地播放和网络播放,支持ass特效字幕,GPU解码能力十分出色。MPV有标准播放器该有的所有功能,你可以播放各种视频,以及通过常用快捷键来控制播放。
解码播放流程:
- 调用mpv_create创建实例。
- 调用mpv_set_option设置播放句柄。
- 调用mpv_set_property设置一些属性比如启用键盘输入等。
- 调用mpv_set_option设置一些参数比如硬解码、超时时间等。
- 调用mpv_initialize初始化实例。
- 调用mpv_command_async执行命令loadfile打开文件播放。
- 调用mpv_terminate_destroy释放实例。
录像存储
mpv除了支持主流的win、linux、mac三大操作系统外,还支持手机安卓IOS、各种嵌入式linux。
支持保存成一个视频文件和定时存储成不同的视频文件,开启了定时存储的话,到了时间先停止录像,然后再重新开始录像,重新设置新的视频文件名称,先设置属性stream-record为空字符串则表示停止录像,然后再重新设置属性stream-record为新的文件名称即可。
读取和控制
mpv是通过读取和设置属性来处理,支持定时器或者线程中函数方法去读取状态,也支持事件回调去拿到对应的状态改变,
常用的一些属性:
- 视频原始宽度高度 width height
- 视频缩放后宽度高度 dwidth dheight
- 保存视频文件 stream-record 为空则表示停止录像
- 视频宽高比 video-aspect
- 暂停播放 pause yes表示暂停no表示继续
- 视频文件时长 duration
- 静音 mute yes表示静音no表示非静音
- 音量 volume int值0-100
- 获取播放进度 time-pos
- 设置播放进度 seek
- 当前音轨 aid
- 音轨数量 track-list/count
- 截图 screenshot-to-file
事件订阅
在使用libmpv的过程中,通过对mpv事件订阅,可以更准确和准时的得知一些事件,比如文件打开成功,播放状态的改变等,而不需要定时器去读取状态,尤其是打开成功这个事件,如果不采用事件订阅,有时候视频流会卡主一阵子,比如不存在的视频流或者网络不好的情况下,有两种办法可以规避这个情况,在vlc和ffmpeg解码中也是如此,一种方法是将这个打开直接放到线程中执行,本来解码处理就是一个完整的线程类,所以直接通过标志位的更改来在线程中执行初始化,毫无压力不卡主,还有一个办法就是采用事件回调,得到打开成功以后,再去执行其他的处理比如读取视频的宽度高度等信息,这些信息一般都是需要打开文件成功以后才能读取到的。
mpv也支持事件订阅,通过mpv_observe_property函数将需要订阅的属性事件更改加入事件订阅队列,这个函数有四个参数,第一个参数指mpv对象(通过mpv_create产生的),第二个参数指用户数据,如果不需要的话直接填0,一般都不需要,第三个参数指属性名称,至于属性名称是啥叫啥,可以官网查阅手册(http://mpv.io/manual/master/#properties),第四个参数指属性的格式类型。一般来说都会对这几个属性事件的更改订阅:duration(文件长度)、time-pos(当前播放进度)。事件订阅好以后执行mpv_set_wakeup_callback函数设置事件回调函数处理即可。
通用接口
官方提供的demo例子,直接用qt封装好了多个接口(https://github.com/mpv-player/mpv-examples/tree/master/libmpv)
主要接口如下:
- 通用获取属性接口函数 get_property_variant
- 通用设置属性接口函数 set_property_variant
- 通用设置参数接口函数 set_option_variant
- 通用执行命令接口函数 command_variant
Onvif
为了统一一个大概的标准,能够对各个厂家的监控设备进行常用的一些操作,比如搜索、获取信息、云台控制、事件订阅、抓拍图片等。
设备搜索
onvif设备搜索是最基本的功能,想要对设备进行进一步的处理,必须先搜索到设备,默认onvif搜索只能搜索到同一个网段的设备,要跨网段的话,需要手动指定设备的IP地址或者onvif地址进行搜索。
onvif主要的功能:
- 搜索设备,获取设备的信息比如厂家、型号等。
- 获取设备的多个配置文件信息profile。
- 获取对应配置文件的视频流地址rtsp,以及分辨率等参数。
- 云台控制,上下左右移动,焦距放大缩小,相对和绝对移动。
- 获取预置位信息,触发预置位。
- 订阅事件,接收设备的各种消息尤其是报警事件比如IO口的报警。
- 抓图,获取设备当前的图片。
- 获取、创建、删除用户信息。
- 获取和设备网络配置信息比如IP地址等。
- 获取和设置NTP时间同步以及设置设备时间。
- 获取和设置视频参数和图片参数(亮度、色彩、饱和度)。
- 重启设备。
onvif的处理流程:
- 绑定组播IP(239.255.255.250)和端口(3702),发送固定的xml格式的数据搜索设备。
- 接收到的xml格式的数据解析,得到设备的Onvif地址。
- 对Onvif地址发送对应的数据,收到数据取出对应的节点数据。
- 请求Onvif地址获取Media地址和Ptz地址,Media地址用来获取详细的配置文件,Ptz地址用来云台控制。
- ptz控制是对Ptz地址发送对应的数据即可。
- 设置了用户认证的需要组织用户token信息一块发送,每次都需要作鉴权处理。
- 接收到的数据不是标准的xml数据,没法按照正常的节点解析来处理,只能用QXmlQuery来做。
- 每个厂家设备返回的数据未必完全一致,基本上都不一致,需要进行模糊查找节点值。
- 特意采用底层协议解析,因为soap太臃肿函数名称太另类,特意做的轻量级的。
- 两个必备工具,Onvif Device Manager 和 Onvif Device Test Tool。
信息获取
搜到这些设备以后,第一件事情就是要对设备信息获取一下,比如获取视频流地址,配置套件信息、码流信息、分辨率大小等,这些信息的获取根据具体的需要去获取,也没有必要全部获取,毕竟很可能大部分的信息用不到,按需编码永远都是第一原则,第二原则才是考虑拓展性和稳定性。onvif设备信息的获取需要注意的是,现在市场上绝大部分的摄像机都有密码验证的限定,先不管他默认是admin还是12345,起码有用户验证的机制摆在那,这样相对来说安全很多,不然谁也可以通过onvif协议拿到对应的信息,就没有安全性可言。
云台控制
最常用的功能排第一的是拿到视频流地址,排第二的就是云台控制了,云台控制的含义就是对带云台的摄像机进行上下左右的移动,一般云台摄像机都是带有一个小电机,一旦收到485或者网络来的正确的指令以后就触发单片机程序,然后单片机程序驱动电机进行转动,所以相对来说云台摄像机比普通的摄像机更耗电,当然价格也更贵。
云台控制的发送命令除了用户信息玩主要就三个核心参数xyz,通过这三个参数的组合来实现云台和焦距的控制,云台的转动主要就是改变xy的值,焦距的控制通过改变z的值来实现。
云台控制说明:
- x、y、z 范围都在0-1之间。
- x为负数,表示左转,x为正数,表示右转。
- y为负数,表示下转,y为正数,表示上转。
- z为正数,表示拉近,z为负数,表示拉远。
- 通过x和y的组合,来实现云台的控制。
- 通过z的组合,来实现焦距控制。
事件订阅
接收摄像机的报警信息一般有两种处理方式,一种是订阅,订阅以后摄像机会在请求后一直阻塞等待,如果有新的报警信息则立即返回,否则需要到超时时间才会断开连接请求;还有一种是定时器主动轮询,不断的去询问是否有新的报警事件。关于订阅要阻塞等待的问题,这就涉及到另一个问题,一般Qt默认的并发请求最大6个,这就意味着订阅机制下,最大只能有6个摄像机的报警事件订阅存在,超过就不行,除非有空闲的连接请求断开了,所以很多开发者会选择用其他的http post工具比如curl去处理。
默认摄像机IO输入或者开关量输入是关闭的,需要手动开启,一般都是登录到摄像机的web页面找到开关量的地方,一般摄像机会有两组开关量输入,而且开关量报警有常开常闭两种,都需要自己手动选择,如果是常开的话意味着闭合是属于报警,反之亦然。
事件订阅流程:
- 发送getEvent(CreatePullPointSubscription)订阅事件服务。
- 订阅服务成功以后,发送PullMessages订阅事件。
- 如果有事件,会立即回复数据,在处理完数据以后,要重新发送PullMessages订阅事件。
- 如此往复,一旦有事件会在请求后回复数据,该请求默认是长连接。
- 发送PullMessages的时候带有超时时间,一旦到了超时时间,也需要重新发送PullMessages。
抓拍图片
通过onvif抓图,不需要打开实时视频流,基本上不占用什么资源。
抓拍图片流程:
- 发送GetSnapshotUri获取到对应token的抓图路径。
- 通过get方式请求这个路径,注意这里建议带上用户信息。
- 返回的数据就是图片的数据,接收完成以后用QImage的loadFromData载入即可。
时间设置
用onvif进行时间设置主要由两种,一种是通过设置NTP服务地址以后,主动调用NTP同步来进行,另外一种就是发送日期时间的数据包给设备,让他自己解析处理,这里要注意的是,数据包中的日期时间是UTC格式的,即伦敦时间,所以在使用的时候需要自己本地先转换成UTC时间在发送,Qt内置了转换成UTC时间的方法 QDateTime::currentDateTime().toUTC()。
USB摄像头解码
用Qt来加载USB摄像头解码,主要就三种方案,一种是Qt自带的qcamera类,一种是ffmpeg来处理,一种是v4l2框架,每种方案都各自有优缺点和对应的应用场景,比如Qt自带的就非常适合windows下的只需要显示摄像头的场景,代码简单Qt内置使用方便,如果是需要拿到每张图片自己还要做分析处理比如人脸识别啥的,用ffmpeg是最好的,速度快资源占用低,可以自己GPU绘制,如果是嵌入式linux的话,那用v4l2框架处理是最适合的,原生的通用api框架。
qcamera方案
QCamera方案处理流程: 1. 实例化QCamera对象,绑定stateChanged信号用于调整尺寸大小。 2. 实例化QCameraViewfinder画布,将QCamera对象设置画布。默认采用类似句柄的形式显示画面,被系统接管绘制。 3. 实例化QCameraImageCapture对象用来抓图,如果有需要的话。
ffmpeg方案
ffmpeg方案处理流程: 1. 引入avdevice.h头文件,调用avdevice_register_all();注册本地设备处理。 2. 调用av_dict_set设置分辨率(video_size)、帧率(framerate)等参数。 3. 调用av_find_input_format设置输入格式。 4. 调用avformat_open_input打开文件。 5. 调用av_find_best_stream找到视频流地址。 6. 调用avcodec_find_decoder设置视频解码器。 7. 调用av_read_frame循环解码读取帧数据。 8. 调用avcodec_send_packet avcodec_receive_frame解码数据。 9. 处理完以后调用av_frame_free avcodec_close等释放资源。
linux方案
linux方案处理流程: 1. 调用封装的函数findCamera实时查找摄像头设备文件名。 2. 调用::open函数打开设备文件。 3. 调用封装的函数initCamera初始化摄像头参数(图片格式、分辨率等)。 4. 调用::select函数从缓冲区取出一个缓冲帧。 5. 缓冲帧数据是yuyv格式的,需要转换rgb24再转成QImage。 6. 拿到图片进行绘制、人脸分析等。 7. 关闭设备文件。
通用功能
通用视频控件基本功能:
- 调用setUrl函数设置要播放的视频文件或者流地址。
- 调用open方法打开视频、close方法关闭视频。
- 调用pause方法暂停播放、next方法继续播放。
- 调用getVolume函数获取音量、setVolume函数设置音量。
- 调用getMute函数获取静音状态、setMute函数设置静音。
- 调用getLength函数获取文件长度。
- 调用getPosition函数获取当前播放位置、setPosition设置播放位置。
- 播放成功发出receivePlayStart信号。
- 播放失败发出receivePlayError信号。
- 播放结束发出receivePlayFinsh信号。
- 执行截图动作后,触发snapImage信号。
- 解析出一张图片后,触发receiveImage信号。
- 播放文件发出fileLengthReceive总时长信号。
- 音量调节或者静音状态变动后发出fileVolumeReceive信号。
- 播放位置变动后发出filePositionReceive信号。
- 接收到拖曳文件发出fileDrag信号。
- 悬浮条工具栏点击按钮后发出btnClicked信号。
- 设置是否保存文件、保存间隔、保存文件夹。
- 设置是否拉伸填充图片、是否深拷贝图片。
- 设置是否断线重连、超时时间。
- 设置悬浮条可见、背景颜色、按下颜色等。
- 设置边框粗细和颜色、背景文字和图片。
- 设置两路OSD的字号、文本、颜色、位置、格式等。
- 设置是否采用回调、硬解码名称、流通信协议等。
通道管理基本功能:
- 设置地址集合(可以是配置文件读取也可以是数据库读取)、名称集合、控件集合。
- 所有通道或者指定通道的打开和关闭。
- 指定通道的抓拍截图。
- 设置视频通道数、超时时间。
- 设置打开视频的间隔、重连视频的间隔。
- 指定视频存储间隔和存储文件夹。