Qt音视频开发学习

原文地址:https://www.zhihu.com/column/c_1273256355848605696

目录

vlc

解码播放

回调处理

录像存储

读取和控制

事件订阅

ffmpeg

ffmpeg解码处理

保存裸流

录像存储

控制播放

常用命令

mpv

解码播放

录像存储

读取和控制

事件订阅

通用接口

Onvif

设备搜索

信息获取

云台控制

事件订阅

抓拍图片

时间设置

USB摄像头解码

qcamera方案

ffmpeg方案

linux方案

通用功能


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解码处理

解码过程:

  1. 注册解码库相关(av_register_all、avformat_network_init等)
  2. 初始化各种参数比如缓存大小等(av_dict_set)
  3. 打开视频流或者文件(avformat_alloc_context、avformat_open_input)
  4. 获取流信息(avformat_find_stream_info)
  5. 获取视频流并初始化视频解码器(av_find_best_stream、avcodec_find_decoder)
  6. 获取音频流并初始化音频解码器(av_find_best_stream、avcodec_find_decoder、avcodec_open2)
  7. 预分配帧内存(av_frame_alloc)
  8. 循环读取音视频帧(av_read_frame、av_packet_unref)
  9. 解码视频(avcodec_decode_video2或者avcodec_send_packet、avcodec_receive_frame)
  10. 解码音频(avcodec_decode_audio4)
  11. 处理结束释放资源(sws_freeContext、av_frame_free、av_free)

保存裸流

ffmpeg保存视频文件,就是直接保存的裸流数据,裸流数据一般是H264格式的数据。先打开文件,然后在循环解码的地方直接将解码好的数据write到文件即可,如果采用的是定时存储的话,那就开个定时器,到了点就先关闭文件,然后重新打开新的名字的文件,这里要注意的是,rtmp视频流的话,需要添加pps sps等信息,所以在每帧写入文件前,要先用AVBitStreamFilter采用h264_mp4toannexb处理下才行。

录像存储

ffmpeg解码保存成MP4文件,有两种处理方式,一种是先保存成裸流,然后开个后台线程,当裸流文件保存完成以后,自动触发H264转MP4的命令执行,也可以很快的完成转换,另外一种方法就是直接解码的时候保存成MP4文件,两种方法都可以,一般建议后者。

保存成MP4文件流程:

  1. 调用avformat_alloc_output_context2开辟一个格式上下文AVFormatContext用来处理视频流输出。
  2. 调用avformat_new_stream开辟一个视频流AVStream用来输出MP4文件。
  3. 重新设置输出视频流的各种参数。
  4. 调用avio_open打开输出文件。
  5. 调用avformat_write_header写入头部标识。
  6. 循环解码后调用av_write_frame写入数据到文件。
  7. 结束后调用av_write_trailer写入结束标识。
  8. 关闭解码输出,关闭文件,释放资源。

控制播放

ffmpeg做视频流解码的时候暂停,如果打开的是本地视频文件,暂停你只需要停止解码即可,但是视频流会发现根本没用,一旦你停止了解码,下次重新解码的时候,还是以前的图片,他是从你最后暂停开始的地方重新解码的。如果想要暂停视频流,正确的做法是照常解码,只是不处理和绘制图片就行,说白了其实就是伪暂停,看起来是暂停了,其实后台还在不断的解码中。

用ffmpeg播放本地文件的时候,如果不加延时,你会发现刷刷几秒钟就播放完了,具体看电脑的性能,性能好的电脑也就几秒钟播放一个5分钟的视频。

延时:在打开流后记住开始的时间,然后解码中取出对应流(视频流或者音频流等)的基准时间time_base,调用av_rescale_q计算出pts时间,然后用av_gettime() - startTime得到当前的时间,用pts_time - now_time得到时间差值,如果是正数,则这个时间就是需要延时的微秒数,注意是微秒数而不是毫秒数哦,直接调用av_usleep来延时即可。

常用命令

  1. 列出支持的格式:ffmpeg -formats
  2. 剪切一段媒体文件:ffmpeg -i input.mp4 -ss 00:00:50.0 -codec copy -t 20 output.mp4
  3. 提取一个视频文件中的音频文件:ffmpeg -i input.mp4 -vn -acodec copy output.m4a
  4. 视频静音,即只保留视频:ffmpeg -i input.mp4 -an -vcodec copy output.mp4
  5. 从MP4文件中抽取视频流导出为裸的H264数据:ffmpeg -i output.mp4 -an -vcodec copy -bsf:v h264_mp4toannexb output.h264
  6. 使用AAC音频数据和H264视频生成MP4文件:ffmpeg -i test.aac -i test.h264 -acodec copy -bsf:a aac_adtstoasc -vcodec copy -f mp4 output.mp4
  7. 音频格式转换:ffmpeg -i input.wav -acodec libfdk_aac output.aac
  8. 将一个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有标准播放器该有的所有功能,你可以播放各种视频,以及通过常用快捷键来控制播放。

解码播放流程:

  1. 调用mpv_create创建实例。
  2. 调用mpv_set_option设置播放句柄。
  3. 调用mpv_set_property设置一些属性比如启用键盘输入等。
  4. 调用mpv_set_option设置一些参数比如硬解码、超时时间等。
  5. 调用mpv_initialize初始化实例。
  6. 调用mpv_command_async执行命令loadfile打开文件播放。
  7. 调用mpv_terminate_destroy释放实例。

录像存储

mpv除了支持主流的win、linux、mac三大操作系统外,还支持手机安卓IOS、各种嵌入式linux。

支持保存成一个视频文件和定时存储成不同的视频文件,开启了定时存储的话,到了时间先停止录像,然后再重新开始录像,重新设置新的视频文件名称,先设置属性stream-record为空字符串则表示停止录像,然后再重新设置属性stream-record为新的文件名称即可。

读取和控制

mpv是通过读取和设置属性来处理,支持定时器或者线程中函数方法去读取状态,也支持事件回调去拿到对应的状态改变,

常用的一些属性:

  1. 视频原始宽度高度 width height
  2. 视频缩放后宽度高度 dwidth dheight
  3. 保存视频文件 stream-record 为空则表示停止录像
  4. 视频宽高比 video-aspect
  5. 暂停播放 pause yes表示暂停no表示继续
  6. 视频文件时长 duration
  7. 静音 mute yes表示静音no表示非静音
  8. 音量 volume int值0-100
  9. 获取播放进度 time-pos
  10. 设置播放进度 seek
  11. 当前音轨 aid
  12. 音轨数量 track-list/count
  13. 截图 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)

主要接口如下:

  1. 通用获取属性接口函数 get_property_variant
  2. 通用设置属性接口函数 set_property_variant
  3. 通用设置参数接口函数 set_option_variant
  4. 通用执行命令接口函数 command_variant

Onvif

为了统一一个大概的标准,能够对各个厂家的监控设备进行常用的一些操作,比如搜索、获取信息、云台控制、事件订阅、抓拍图片等。

设备搜索

onvif设备搜索是最基本的功能,想要对设备进行进一步的处理,必须先搜索到设备,默认onvif搜索只能搜索到同一个网段的设备,要跨网段的话,需要手动指定设备的IP地址或者onvif地址进行搜索。

onvif主要的功能:

  1. 搜索设备,获取设备的信息比如厂家、型号等。
  2. 获取设备的多个配置文件信息profile。
  3. 获取对应配置文件的视频流地址rtsp,以及分辨率等参数。
  4. 云台控制,上下左右移动,焦距放大缩小,相对和绝对移动。
  5. 获取预置位信息,触发预置位。
  6. 订阅事件,接收设备的各种消息尤其是报警事件比如IO口的报警。
  7. 抓图,获取设备当前的图片。
  8. 获取、创建、删除用户信息。
  9. 获取和设备网络配置信息比如IP地址等。
  10. 获取和设置NTP时间同步以及设置设备时间。
  11. 获取和设置视频参数和图片参数(亮度、色彩、饱和度)。
  12. 重启设备。

onvif的处理流程:

  1. 绑定组播IP(239.255.255.250)和端口(3702),发送固定的xml格式的数据搜索设备。
  2. 接收到的xml格式的数据解析,得到设备的Onvif地址。
  3. 对Onvif地址发送对应的数据,收到数据取出对应的节点数据。
  4. 请求Onvif地址获取Media地址和Ptz地址,Media地址用来获取详细的配置文件,Ptz地址用来云台控制。
  5. ptz控制是对Ptz地址发送对应的数据即可。
  6. 设置了用户认证的需要组织用户token信息一块发送,每次都需要作鉴权处理。
  7. 接收到的数据不是标准的xml数据,没法按照正常的节点解析来处理,只能用QXmlQuery来做。
  8. 每个厂家设备返回的数据未必完全一致,基本上都不一致,需要进行模糊查找节点值。
  9. 特意采用底层协议解析,因为soap太臃肿函数名称太另类,特意做的轻量级的。
  10. 两个必备工具,Onvif Device Manager 和 Onvif Device Test Tool。

信息获取

搜到这些设备以后,第一件事情就是要对设备信息获取一下,比如获取视频流地址,配置套件信息、码流信息、分辨率大小等,这些信息的获取根据具体的需要去获取,也没有必要全部获取,毕竟很可能大部分的信息用不到,按需编码永远都是第一原则,第二原则才是考虑拓展性和稳定性。onvif设备信息的获取需要注意的是,现在市场上绝大部分的摄像机都有密码验证的限定,先不管他默认是admin还是12345,起码有用户验证的机制摆在那,这样相对来说安全很多,不然谁也可以通过onvif协议拿到对应的信息,就没有安全性可言。

云台控制

最常用的功能排第一的是拿到视频流地址,排第二的就是云台控制了,云台控制的含义就是对带云台的摄像机进行上下左右的移动,一般云台摄像机都是带有一个小电机,一旦收到485或者网络来的正确的指令以后就触发单片机程序,然后单片机程序驱动电机进行转动,所以相对来说云台摄像机比普通的摄像机更耗电,当然价格也更贵。

云台控制的发送命令除了用户信息玩主要就三个核心参数xyz,通过这三个参数的组合来实现云台和焦距的控制,云台的转动主要就是改变xy的值,焦距的控制通过改变z的值来实现。

云台控制说明:

  1. x、y、z 范围都在0-1之间。
  2. x为负数,表示左转,x为正数,表示右转。
  3. y为负数,表示下转,y为正数,表示上转。
  4. z为正数,表示拉近,z为负数,表示拉远。
  5. 通过x和y的组合,来实现云台的控制。
  6. 通过z的组合,来实现焦距控制。

事件订阅

接收摄像机的报警信息一般有两种处理方式,一种是订阅,订阅以后摄像机会在请求后一直阻塞等待,如果有新的报警信息则立即返回,否则需要到超时时间才会断开连接请求;还有一种是定时器主动轮询,不断的去询问是否有新的报警事件。关于订阅要阻塞等待的问题,这就涉及到另一个问题,一般Qt默认的并发请求最大6个,这就意味着订阅机制下,最大只能有6个摄像机的报警事件订阅存在,超过就不行,除非有空闲的连接请求断开了,所以很多开发者会选择用其他的http post工具比如curl去处理。

默认摄像机IO输入或者开关量输入是关闭的,需要手动开启,一般都是登录到摄像机的web页面找到开关量的地方,一般摄像机会有两组开关量输入,而且开关量报警有常开常闭两种,都需要自己手动选择,如果是常开的话意味着闭合是属于报警,反之亦然。

事件订阅流程:

  1. 发送getEvent(CreatePullPointSubscription)订阅事件服务。
  2. 订阅服务成功以后,发送PullMessages订阅事件。
  3. 如果有事件,会立即回复数据,在处理完数据以后,要重新发送PullMessages订阅事件。
  4. 如此往复,一旦有事件会在请求后回复数据,该请求默认是长连接。
  5. 发送PullMessages的时候带有超时时间,一旦到了超时时间,也需要重新发送PullMessages。

抓拍图片

通过onvif抓图,不需要打开实时视频流,基本上不占用什么资源。

抓拍图片流程:

  1. 发送GetSnapshotUri获取到对应token的抓图路径。
  2. 通过get方式请求这个路径,注意这里建议带上用户信息。
  3. 返回的数据就是图片的数据,接收完成以后用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. 关闭设备文件。

通用功能

通用视频控件基本功能:

  1. 调用setUrl函数设置要播放的视频文件或者流地址。
  2. 调用open方法打开视频、close方法关闭视频。
  3. 调用pause方法暂停播放、next方法继续播放。
  4. 调用getVolume函数获取音量、setVolume函数设置音量。
  5. 调用getMute函数获取静音状态、setMute函数设置静音。
  6. 调用getLength函数获取文件长度。
  7. 调用getPosition函数获取当前播放位置、setPosition设置播放位置。
  8. 播放成功发出receivePlayStart信号。
  9. 播放失败发出receivePlayError信号。
  10. 播放结束发出receivePlayFinsh信号。
  11. 执行截图动作后,触发snapImage信号。
  12. 解析出一张图片后,触发receiveImage信号。
  13. 播放文件发出fileLengthReceive总时长信号。
  14. 音量调节或者静音状态变动后发出fileVolumeReceive信号。
  15. 播放位置变动后发出filePositionReceive信号。
  16. 接收到拖曳文件发出fileDrag信号。
  17. 悬浮条工具栏点击按钮后发出btnClicked信号。
  18. 设置是否保存文件、保存间隔、保存文件夹。
  19. 设置是否拉伸填充图片、是否深拷贝图片。
  20. 设置是否断线重连、超时时间。
  21. 设置悬浮条可见、背景颜色、按下颜色等。
  22. 设置边框粗细和颜色、背景文字和图片。
  23. 设置两路OSD的字号、文本、颜色、位置、格式等。
  24. 设置是否采用回调、硬解码名称、流通信协议等。

通道管理基本功能:

  1. 设置地址集合(可以是配置文件读取也可以是数据库读取)、名称集合、控件集合。
  2. 所有通道或者指定通道的打开和关闭。
  3. 指定通道的抓拍截图。
  4. 设置视频通道数、超时时间。
  5. 设置打开视频的间隔、重连视频的间隔。
  6. 指定视频存储间隔和存储文件夹。

你可能感兴趣的:(Qt)