第3章
CHAPTER 3


FFmpeg二次开发采集



并预览本地摄像头








5min




FFmpeg中有一个和多媒体设备交互的类库,即Libavdevice,使用这个库可以读取计算机(或者其他设备)的多媒体设备中的数据,或者将数据输出到指定的多媒体设备上,也可以使用Qt或SDL将捕获到的摄像头数据显示到窗口中。

3.1FFmpeg的命令行方式处理摄像头

使用ffmpeg hide_banner devices命令可以查看本机支持的输入/输出设备,以Windows和Linux为例。显示的信息中D表示支持解码,可以作为输入; E表示支持编码,可以作为输出,具体信息如下: 



//chapter3/help-others.txt

Devices:

D = Demuxing supported

E = Muxing supported

---

D  dshow  DirectShow capture

D  lavfi  Libavfilter virtual input device

E sdl,sdl2SDL2 output device

D  vfwcap VfW video capture

Devices:

D. = Demuxing supported

E = Muxing supported

--

DE alsa   ALSA audio output

E caca   caca (color ASCII art) output device

DE fbdev  Linux framebuffer

D  iec61883libiec61883 (new DV1394) A/V input device

D  jack   JACK Audio Connection Kit

D  kmsgrabKMS screen capture

D  lavfi  Libavfilter virtual input device

D  libcdio






D  libdc1394dc1394 v.2 A/V grab

D  openal OpenAL audio capture device

E opengl OpenGL output

DE oss    OSS (Open Sound System) playback

DE pulse  Pulse audio output

E sdl,sdl2SDL2 output device

DE sndio  sndio audio playback

DE video4linux2,v4l2 Video4Linux2 output device

E vout_rpiRpi (mmal) video output device

D  x11grabX11 screen capture, using XCB

E xv     XV (XVideo) output device









Windows平台下使用vfwcap的效果比使用dshow的效果要差一些,可以通过ffmpeg h demuxer=dshow 命令查看支持的操作参数,支持查看设备列表、选项列表,以及可以设置设备输出的视频分辨率、帧率等,具体的输出信息如下: 



//chapter3/help-others.txt

Demuxer dshow [DirectShow capture]:

dshow indev AVOptions:

-video_size<image_size> .D....... set video size given a string such as 640x480 or hd720.

-pixel_format      <pix_fmt>.D....... set video pixel format (default none)

-framerate<string>.D....... set video frame rate

-sample_rate<int>.D....... set audio sample rate (from 0 to INT_MAX) (default 0)

-sample_size<int>.D....... set audio sample size (from 0 to 16) (default 0)

-channels<int>.D....... set number of audio channels, such as 1 or 2 (from 0 to INT_MAX) (default 0)

-audio_buffer_size <int>.D....... set audio device buffer latency size in milliseconds (default is the device's default) (from 0 to INT_MAX) (default 0)

-list_devices<boolean>.D....... list available devices (default false)

-list_options<boolean>.D....... list available options for specified device (default false)

-video_device_number<int>.D....... set video device number for devices with same name (starts at 0) (from 0 to INT_MAX) (default 0)

-audio_device_number <int>.D....... set audio device number for devices with same name (starts at 0) (from 0 to INT_MAX) (default 0)

-crossbar_video_input_pin_number <int>.D....... set video input pin number for crossbar device (from -1 to INT_MAX) (default -1)

-crossbar_audio_input_pin_number <int>.D....... set audio input pin number for crossbar device (from -1 to INT_MAX) (default -1)

-show_video_device_dialog <boolean>.D....... display property dialog for video capture device (default false)

-show_audio_device_dialog <boolean>.D....... display property dialog for audio capture device (default false)

-show_video_crossbar_connection_dialog <boolean>.D....... display property dialog for crossbar connecting pins filter on video device (default false)

-show_audio_crossbar_connection_dialog <boolean>.D....... display property dialog for crossbar connecting pins filter on audio device (default false)







-show_analog_tv_tuner_dialog <boolean>.D....... display property dialog for analog tuner filter (default false)

-show_analog_tv_tuner_audio_dialog <boolean>.D....... display property dialog for analog tuner audio filter (default false)

-audio_device_load <string>.D....... load audio capture filter device (and properties) from file

-audio_device_save <string>.D....... save audio capture filter device (and properties) to file

-video_device_load <string>.D....... load video capture filter device (and properties) from file

-video_device_save <string>.D....... save video capture filter device (and properties) to file



Windows平台下查询支持的所有设备列表,命令如下: 



ffmpeg -list_devices true -f dshow -i dummy



命令执行后,笔者本地的输出结果如图31所示(注: 有可能出现中文乱码的情况)。列表所显示的设备名称很重要,因为输入时需要使用f dshow i video="设备名"的方式。



图31Windows平台列举设备列表


获取摄像头数据后,可以保存为本地文件或者将实时流发送到流媒体服务器。例如从摄像头读取数据并编码为H.264,最后保存成mycamera.flv的命令如下: 



ffmpeg -f dshow -i video="Lenovo EasyCamera" -vcodec libx264  mycamera001.flv



使用ffplay.exe可以直接播放摄像头的数据,命令如下: 



ffplay -f dshow -i video="Lenovo EasyCamera"



如果设备名称正确,则会直接打开本机的摄像头,效果如图32所示。



图32FFplay播放摄像头


可以查看摄像头的流信息,执行效果如图33所示,具体命令如下: 



ffmpeg  -hide_banner  -f dshow -i "video=Lenovo EasyCamera"





图33查看摄像头的流信息


查询本机dshow设备、查看USB 2.0摄像头的信息,只能输出yuyv422压缩编码数据(可以理解为rawvideo编码器,像素格式为yuyv422),经过解码后获得yuyv422编码的图像数据,所以,在这种情况下可以不解码。

Linux下大多使用video4linux2/v4l2设备,通过 ffmpeg h demuxer=v4l2 命令可以查看相关的操作参数,输出信息如下: 



//chapter3/help-others.txt

Demuxer video4linux2,v4l2 [Video4Linux2 device grab]:

V4L2 indev AVOptions:

-standard<string>.D....... set TV standard, used only by analog frame grabber

-channel<int>.D....... set TV channel, used only by frame grabber (from -1 to INT_MAX) (default -1)

-video_size<image_size>.D....... set frame size

-pixel_format<string>.D....... set preferred pixel format

-input_format<string>.D....... set preferred pixel format (for raw video) or codec name

-framerate<string>.D....... set frame rate

-list_formats<int>.D....... list available formats and exit (from 0 to INT_MAX) (default 0)

all.D....... show all available formats

raw.D....... show only non-compressed formats

compressed.D....... show only compressed formats

-list_standards<int>.D....... list supported standards and exit (from 0 to 1) (default 0)

all.D....... show all supported standards







-timestamps<int>.D....... set type of timestamps for grabbed frames (from 0 to 2) (default default)

default.D....... use timestamps from the kernel

abs.D....... use absolute timestamps (wall clock)

mono2abs .D....... force conversion from monotonic to absolute timestamps

-ts<int>.D....... set type of timestamps for grabbed frames (from 0 to 2) (default default)

default.D....... use timestamps from the kernel

abs.D....... use absolute timestamps (wall clock)

mono2abs.D....... force conversion from monotonic to absolute timestamps

-use_libv4l2<boolean>.D....... use libv4l2 (v4l-utils) conversion functions (default false)



当前机器上挂载了CSI接口的相机,可以查看其支持的格式,执行效果如图34所示,该相机支持多种非压缩编码格式,例如JFIF JPEG、MotionJPEG、H.264等,具体命令如下: 



ffmpeg -hide_banner -f v4l2 -list_formats all -i /dev/video0





图34Linux查看摄像头支持的格式


Linux平台下读取摄像头并编码为H.264的命令如下: 



#ffmpeg -f dshow -i video="USB2.0 PC CAMERA" -vcodec libx264 xxxx.h264

ffmpeg -f v4l2 -i video=/dev/video0 -vcodec libx264 xxxx.h264



在实时流推送中如果需要提高libx264的编码速度,则可以添加preset:v ultrafast 和 tune:v zerolatency两个选项。

Windows平台下使用gdigrab设备可以录制桌面屏幕,可以查看gdigrab支持的选项,命令如下: 



ffmpeg -h demuxer=gdigrab



该命令的输出信息如下: 



//chapter3/help-others.txt

Demuxer gdigrab [GDI API Windows frame grabber]:

GDIgrab indev AVOptions:

-draw_mouse<int>.D....... draw the mouse pointer (from 0 to 1) (default 1)

-show_region<int>.D....... draw border around capture area (from 0 to 1) (default 0)

-framerate<video_rate>.D....... set video frame rate (default "ntsc")

-video_size<image_size>.D....... set video frame size

-offset_x<int>.D....... capture area x offset (from INT_MIN to INT_MAX) (default 0)

-offset_y<int>.D....... capture area y offset (from INT_MIN to INT_MAX) (default 0)



录屏并编码为H.264的命令如下: 



ffmpeg -f gdigrab -i desktop -vcodec h264 xxxx.h264



录屏并显示鼠标,从屏幕左上角(100,200)的640×480区域录屏,帧率为25的命令如下: 



ffmpeg -f gdigrab -draw_mouse -framerate 25 -offset_x 100 -offset_y 200 \

-video_size 640x480 -i desktop out1.mpg



Linux平台下与之类似,使用 x11grab 设备,可以查看支持的选项,命令如下: 



$ ffmpeg -h demuxer=x11grab



该命令的输出信息如下: 



//chapter3/help-others.txt

Demuxer x11grab [X11 screen capture, using XCB]:

xcbgrab indev AVOptions:

-x<int>.D...... Initial x coordinate. (from 0 to INT_MAX) (default 0)

-y<int>.D...... Initial y coordinate. (from 0 to INT_MAX) (default 0)

-grab_x<int>.D...... Initial x coordinate. (from 0 to INT_MAX) (default 0)

-grab_y<int>.D...... Initial y coordinate. (from 0 to INT_MAX) (default 0)

-video_size<string> .D...... A string describing frame size, such as 640x480 or hd720. (default "vga")

-framerate<string>.D......  (default "ntsc")

-draw_mouse<int>.D...... Draw the mouse pointer. (from 0 to 1) (default 1)

-follow_mouse<int>.D...... Move the grabbing region when the mouse pointer reaches within specified amount of pixels to the edge of region. (from -1 to INT_MAX) (default 0)

centered.D...... Keep the mouse pointer at the center of grabbing region when following.

-show_region<int>.D...... Show the grabbing region. (from 0 to 1) (default 0)

-region_border<int>.D...... Set the region border thickness. (from 1 to 128) (default 3)



Linux平台下录屏并捕获鼠标的命令如下: 



ffmpeg -f x11grab -draw_mouse -framerate 25 - x 100 -y 200 \

-video_size 640x480 -i :0.0 out3.mpg



3.2FFmpeg的SDK方式读取本地摄像头

使用FFmpeg采集并预览本地摄像头的流程如图35所示。



图35FFmpeg采集并预览摄像头


使用Libavdevice时需要包含头文件,代码如下: 



#include "libavdevice/avdevice.h"



然后在程序中需要注册Libavdevice,代码如下: 



avdevice_register_all();



接下来就可以使用Libavdevice的功能了,使用Libavdevice读取数据和直接打开视频文件比较类似。因为系统的设备会被FFmpeg当成一种输入的格式(AVInputFormat)。使用FFmpeg的API可以根据指定参数打开输入流,并返回输入封装的上下文AVFormatContext,函数的代码如下: 



//chapter3/code3.2.txt

int avformat_open_input(AVFormatContext **ps,

const char *url,//打开的地址,文件或设备等

AVInputFormat *fmt,//指定输入格式,若为空,则自动检测

AVDictionary **options);//指定解复用器的私有选项



通常,打开一个文件或者直播流的代码如下: 



//chapter3/code3.2.txt

AVFormatContext *input_fmt_ctx = NULL;

const char* file_path = "test.mp4";

//也可以打开网络直播流

//const char* file_path = "rtmp://192.168.1.100:1935/live/test";

avformat_open_input(&input_fmt_ctx, file_path, NULL, NULL);



使用Libavdevice时,唯一的不同在于首先需要查找用于输入的设备,例如可以使用av_find_input_format()函数来查找音视频设备。在Windows平台上使用vfw设备作为输入设备,然后在URL中指定打开第0个设备(在笔者的计算机上为摄像头设备),代码如下: 



//chapter3/code3.2.txt

AVFormatContext *pFormatCtx = avformat_alloc_context();

AVInputFormat *ifmt = av_find_input_format("vfwcap");

avformat_open_input(&pFormatCtx, 0, ifmt,NULL);



在Windows平台上除了可以使用vfw设备作为输入设备之外,还可以使用DirectShow作为输入设备,代码如下: 



//chapter3/code3.2.txt

AVFormatContext *pFormatCtx = avformat_alloc_context();

AVInputFormat *ifmt = av_find_input_format("dshow");

avformat_open_input(&pFormatCtx,"video=Integrated Camera",ifmt,NULL) ;



在Linux平台上可以使用v4l2设备作为输入设备,代码如下: 



//chapter3/code3.2.txt

//查找设备前要先调用avdevice_register_all函数

AVInputFormat *in_fmt = in_fmt = av_find_input_format("video4linux2");

if (in_fmt == NULL) {

printf("can't find_input_format\n");

return ;

}



AVFormatContext *fmt_ctx = NULL;

if (avformat_open_input(&fmt_ctx, "/dev/video0", in_fmt, NULL) < 0) {

printf("can't open_input_file\n");

return ;

}



另外,使用选项av_dict_set(&options, "f", "v4l2", 0)和指定参数ifmt的效果相同。通常在Linux平台下可以不设置,默认为支持v4l2且自动识别,而在Windows平台下的vfwp、dshow需要明确指定,代码如下: 



AVInputFormat *ifmt = av_find_input_format("v4l2");//加快探测流的速度

avformat_open_input(&fmt_ctx, "/dev/video0", ifmt, NULL);



上述代码等效于下面的代码: 



AVDictionary *options = NULL;

av_dict_set(&options, "f", "v4l2", 0);

avformat_open_input(&fmt_ctx,"/dev/video0",NULL,&options);



如果需要指定输入格式,则可以通过AVOption设置,并且参数不一样,描述信息如下: 



-pixel_format  <string>  .D.... set preferred pixel format

-input_format  <string>  .D.... set preferred pixel format (for raw video) or codec name



当选择像素格式时,一定是非压缩的原始数据,两个参数均可,代码如下: 



av_dict_set(&options, "pixel_format", "rgb24", 0);

av_dict_set(&options, "input_format", "rgb24", 0);  //同上



当选择压缩编码格式,必须只能使用input_format,代码如下: 



av_dict_set(&options, "input_format", "h264", 0);

av_dict_set(&options, "input_format", "mjpeg", 0);



可以调用avformat_open_input()函数来打开摄像头设备,通过这个函数的参数指定打开的设备路径“/dev/video0”,使用的驱动“video4linux2”,将相应的格式pix_fmt指定为yuyv422,以及将分辨率指定为640×480,代码如下: 



//chapter3/code3.2.txt

AVFormatContext *fmt_ctx = NULL;

AVDictionary *options = NULL;

char *devicename = "/dev/video0";

avdevice_register_all();

AVInputFormat *iformat = av_find_input_format("video4linux2");

av_dict_set(&options,"video_size","640x480",0);

av_dict_set(&options,"pixel_format","yuyv422", 0);

avformat_open_input(&fmt_ctx, devicename, iformat, &options);

avformat_close_input(&fmt_ctx);



可以调用av_read_frame()函数来读取一帧YUV数据,代码如下: 



//chapter3/code3.2.txt

int ret = 0;

AVPacket pkt;

while((ret = av_read_frame(fmt_ctx, &pkt)) == 0) {

av_log(NULL, AV_LOG_INFO, "packet size is %d(%p)\n",

pkt.size, pkt.data);






av_packet_unref(&pkt);//释放包

}



可以调用fwrite()函数将读取到的YUV数据保存到文件中,代码如下: 



//chapter3/code3.2.txt

char *out = "out.yuv";

FILE *outfile = fopen(out, "wb+");

fwrite(pkt.data, 1, pkt.size, outfile); //614400

fflush(outfile);

fclose(outfile);



打开本地摄像头,循环读取帧数据并存储到文件中的完整代码如下: 



//chapter3/record-video.c

#include <stdio.h>

#include "libavutil/avutil.h"

#include "libavdevice/avdevice.h"

#include "libavformat/avformat.h"

#include "libavcodec/avcodec.h"





//打开摄像头

static AVFormatContext* open_dev(){

int ret = 0;

char errors[1024] = {0, };



//上下文环境

AVFormatContext *fmt_ctx = NULL;

AVDictionary *options = NULL;



//设备名称

char *devicename = "/dev/video0";



//注册Libavdevice库

avdevice_register_all();



//查找设备

AVInputFormat *iformat = av_find_input_format("video4linux2");



av_dict_set(&options,"video_size","640x480",0);//分辨率

av_dict_set(&options,"pixel_format","yuyv422", 0);//YUV帧格式



//打开设备

if((ret = avformat_open_input(&fmt_ctx, devicename, iformat, &options)) < 0 ){

av_strerror(ret, errors, 1024);

fprintf(stderr, "Failed to open audio device, [%d]%s\n", ret, errors);

return NULL;







}



return fmt_ctx;

}



//读取并录制摄像头数据

void rec_video() {

int ret = 0;

AVFormatContext *fmt_ctx = NULL;

int count = 0;



//packet:数据包

AVPacket pkt;



//设置日志级别

av_log_set_level(AV_LOG_Debug);



//create file:创建本地文件

char *out = "out.yuv";

FILE *outfile = fopen(out, "wb+");



//打开设备

fmt_ctx = open_dev();



//从摄像头中读取YUV帧数据

while((ret = av_read_frame(fmt_ctx, &pkt)) == 0 &&

count++ < 100) {

av_log(NULL, AV_LOG_INFO,

"packet size is %d(%p)\n",

pkt.size, pkt.data);



fwrite(pkt.data, 1, pkt.size, outfile); //写入本地文件

fflush(outfile);

av_packet_unref(&pkt); //release pkt:释放AVPacket

}



__ERROR:

if(outfile){

//关闭文件

fclose(outfile);

}



//关闭设备并释放上下文环境

if(fmt_ctx) {

avformat_close_input(&fmt_ctx);

}



av_log(NULL, AV_LOG_Debug, "finish!\n");

return;








}





int main(int argc, char *argv[])

{

rec_video();

return 0;

}



在Linux系统中使用编译该文件的命令如下: 



gcc record_video.c -lavformat -lavutil -lavdevice -lavcodec -o record_video



运行该程序会打开摄像头并循环读取帧,然后存储到本地文件out.yuv中,命令如下: 



./record_video



使用FFplay可以播放生成的YUV文件,注意指定分辨率和YUV格式,命令如下: 



 ffplay -s 640x480 -pix_fmt yuyv422 out.yuv



3.3FFmpeg+SDL2读取并显示本地摄像头

使用FFmpeg读取摄像头数据之后,可以调用SDL2库来渲染。

3.3.1SDL2简介

SDL2使用GNU通用公共许可证为授权方式,即动态链接(Dynamic Link)其库并不需要开放本身的源代码。虽然SDL时常被比喻为“跨平台的DirectX”,但事实上SDL被定位成以精简的方式来完成基础的功能,大幅度简化了控制图像、声音、输入/输出等工作所需的代码。但更高级的绘图功能或是音效功能则需搭配OpenGL和OpenAL等API来完成。另外,它本身也没有方便创建图形用户界面的函数。SDL2在结构上是将不
同操作系统的库包装成相同的函数,例如SDL在Windows平台上是DirectX的再包装,而在使用X11的平台上(包括Linux)则是调用Xlib库来输出图像。虽然SDL2本身是使用C语言写成的,但是它几乎可以被所有的编程语言所使用,例如C++、Perl、Python和Pascal等,甚至是Euphoria、Pliant这类较不流行的编程语言也都可行。SDL2库分为Video、Audio、CDROM、Joystick和Timer等若干子系统,除此之外,还有一些单独的官方扩充函数库。这些库由官方网站提供,并包含在官方文档中,它们共同组成了SDL的“标准库”。SDL的整体结构如图36所示。



图36SDL库的层次结构


3.3.2VS 2015搭建SDL2开发环境

本节将介绍如何在VS 2015下配置SDL2.0.8开发库的详细步骤。

1. 下载SDL2

进入SDL2官网,链接网址为https://github.com/libsdlorg/SDL/releases/。选择SDL2的Development Libraries中的SDL2devel2.0.12VC.zip(链接网址为https://github.com/libsdlorg/SDL/releases/tag/release2.0.12),如图37所示。下载并解压以供其他程序调用,在项目配置中可以使用SDL库的相对路径。



图37SDL库的下载网址


2. VS 2015项目配置

(1) 打开VS 2015,新建Win32控制台项目,将项目命名为SDLtest1,然后单击右下方的“确定”按钮,如图38所示。



图38新建VS 2015的控制台项目


(2) 右击项目名称(SDLtest1),在弹出的菜单中单击“属性”按钮,然后在弹出的属性页中配置包含目录和库目录,注意笔者这里使用SDL2库的相对路径,选择的平台为Win32,如图39所示。



图39配置VS 2015项目的包含目录和库目录


(3) 在项目SDLtest1属性页中选择“链接器”下的“输入”,编辑右侧的“附加依赖项”,在附加依赖项中添加SDL2.lib和SDL2main.lib(注意中间以英文分号分隔),然后单击右下方的“确定”按钮,如图310所示。



图310配置VS 2015项目的附加依赖项



3.  测试案例

项目配置成功后,可以调用SDL_Init()函数来测试是否配置成功,代码如下: 



//chapter3/SDLtest1/SDLtest1.cpp

//SDLtest.cpp : 定义控制台应用程序的入口点

#include "stdafx.h"

#include <iostream>



#define SDL_MAIN_HANDLED //如果没有此宏,则会报错

#include <SDL.h>



int main(){

if (SDL_Init(SDL_INIT_VIDEO) != 0){

std::cout << "SDL_Init Error: " << SDL_GetError() << std::endl;

return 1;

}

else{

std::cout << "SDL_Init OK "  << std::endl;

}






SDL_Quit();

return 0;

}



需要注意这个宏语句(#define SDL_MAIN_HANDLED),如果没有定义这个宏,则会报错(并且要放到SDL.h之前),错误信息如下: 



无法解析的外部符号main,该符号在函数"int cdecl invoke_main(void)" (?invoke_main@@YAHXZ) 中被引用



这是因为在SDL库的内部重新定义了main,因此main()函数需要写成如下形式: 



int main(int argc,char* argv[])



而添加 #define SDL_MAIN_HANDLED 这个宏之后,即使main()函数的参数列表为空,也不会报错。

编译并运行该程序会提示找不到SDL2.dll,如图311所示。将SDL2devel2.0.12VC\lib\x86目录下的SDL2.dll复制到SDLtest1.exe同目录下,如图312所示。重新编译并运行该程序,若不报错,则表示配置成功,如图313所示。



图311运行时找不到SDL2.dll文件




图312复制SDL2.dll文件




图313SDL2库配置成功


3.3.3Qt 5.9平台搭建SDL2开发环境

笔者本地的Qt版本为5.9.8,配置SDL2开发环境的具体步骤如下。

(1) 下载SDL2的mingw版本,文件名为SDL2devel2.0.12mingw.tar.gz,链接网址为https://github.com/libsdlorg/SDL/releases/tag/release2.0.12。

(2) 打开Qt Creator,新建Qt Console Application类型的项目,单击右下方的Choose 按钮,如图314所示。



图314新建Qt控制台项目


(3) 在Project Location页面输入项目名称(SDLQtDemo1)和路径,如图315所示。



图315输入Qt项目名称和路径


(4) 在Kit Selection页面选中Desktop Qt 5.9.8 MinGW 32bit,然后单击右下方的“下一步”按钮,如图316所示。




注意: 
读者也可以选择其他的编译套件,但不同的编译套件对应着不同的SDL2开发包,例如MinGW 32位编译套件对应SDL2devel2.0.12mingw.tar.gz,并且运行时需要对应32位的动态链接库。






图316选择MinGW 32位编译套件


(5) 解压SDL2devel2.0.12mingw.tar.gz后有两个重要的子目录,如图317所示。i686w64mingw32对应的是32位的开发库,x86_64w64mingw32对应的是64位的开发库。



图317解压SDL2devel2.0.12mingw.tar.gz



(6) 配置Qt项目(SDLQtDemo1),打开SDLQtDemo1.pro配置文件,如图318所示,代码如下: 



//chapter3/SDLQtDemo1/SDLQtDemo1.pro.txt

INCLUDEPATH+=../../SDL2-devel-2.0.12-mingw/i686-w64-mingw32/include/SDL2/

LIBS += -L../../SDL2-devel-2.0.12-mingw/i686-w64-mingw32/lib/ -lSDL2  -lSDL2main






图318修改Qt的项目配置文件




需要注意的是这里使用的是相对路径,如图319所示。




图319SDL2的相对路径


(7) 修改main.cpp,注释掉原来的源码,新增代码如下: 



//chapter3/SDLQtDemo1/main.cpp

#include <iostream>

#define SDL_MAIN_HANDLED //如果没有此宏,则会报错

#include <SDL.h>



int main(){

if (SDL_Init(SDL_INIT_VIDEO) != 0){

std::cout << "SDL_Init Error: " << SDL_GetError() << std::endl;

return 1;

}

else{

std::cout << "SDL_Init OK "  << std::endl;

}

SDL_Quit();

return 0;

}



(8) 编译并运行该项目,输出的错误信息如下: 



/.../F5Codes/chapter6/build-SDLQtDemo1-Desktop_Qt_5_9_8_MinGW_32位-Debug/Debug/SDLQtDemo1.exe exited with code -1073741515



这是因为SDLQtDemo1.exe程序运行时找不到SDL2.dll动态链接库。将SDL2devel2.0.12mingw\i686w64mingw32\bin目录下的SDL2.dll文件复制到chapter6\buildSDLQtDemo1Desktop_Qt_5_9_8_MinGW_32bitDebug\debug目录下。重新编译并运行该项目会输出 SDL_Init OK,如图320所示。



图320成功配置并运行SDL2项目



3.3.4Linux平台搭建SDL2开发环境

笔者本地环境为Ubuntu 18.04,安装并配置SDL2的具体步骤如下。

(1) 安装依赖项,命令如下: 



//chapter3/help-others.txt

sudo apt-get update && sudo apt-get -y install \

autoconf   automake   build-essential  cmake \

git-core   pkg-config   texinfo   wget   yasm   zlib1g-dev



(2) 安装SDL2库(只包含.so动态链接库),命令如下: 



sudo apt-get install libsdl2-2.0 libsdl2-dev libsdl2-mixer-dev libsdl2-image-dev libsdl2-ttf-dev libsdl2-gfx-dev



(3) 检验是否安装成功,命令如下: 



sdl2-config --exec-prefix --version -cflag



需要注意的是此处安装的SDL2库是没有头文件的,只包含系统运行时需要依赖的动态链接库(.so),而在实际开发过程中没有头文件是不行的,所以需要自己编译SDL2并且安装。

(4) 下载并解压SDL2库的源码SDL2devel2.0.12.tar.gz,具体的下载网址为https://github.com/libsdlorg/SDL/releases/tag/release2.0.12。

(5) 编译并安装SDL2,命令如下: 



//chapter3/help-others.txt

#解压下载的文件,然后进入SDL2解压目录







#配置configure的可执行命令

sudo chmod +x configure

#配置configure的参数命令

//chapter6/other-help.txt

./configure --enable-static --enable-shared

#编译:

make

#安装

make install



(6) 查看SDL2是否安装成功,命令如下: 



//chapter3/help-others.txt

#在/usr/local/lib下面查看是否存在libSDL2.a

ls /usr/local/lib

#在/usr/local/include下面查看是否存在SDL2文件夹

ls /usr/local/include



(7) 配置LD_LIBRARY_PATH环境变量,命令如下: 



export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib



3.3.5SDL2播放YUV视频文件

SDL2的核心对象主要包括窗口(SDL_Window)、表面(SDL_Surface)、渲染器(SDL_Renderer)、纹理(SDL_Texture)和事件(SDL_Event)等。使用SDL2进行渲染的基本流程如图321所示,具体步骤如下。




图321SDL2渲染流程及API


(1) 创建窗口。

(2) 创建渲染器。

(3) 清空缓冲区。

(4) 绘制要显示的内容。

(5) 最终将缓冲区内容渲染到Window窗口上。

使用SDL2播放YUV视频文件完全遵循上述SDL_Renderer的渲染流程,而YUV视频文件不能直接渲染,需要循环读取视频帧,然后将帧数据更新到纹理上进行渲染。

1.  SDL2播放YUV视频文件的流程

使用SDL2播放YUV视频文件的函数调用步骤及相关API,代码如下: 



//chapter3/SDLQtDemo1/main.cpp

/* SDL2播放YUV视频文件,函数调用步骤如下

 *

 * [初始化SDL2库]

 * SDL_Init(): 初始化SDL2

 * SDL_CreateWindow(): 创建窗口(Window)

 * SDL_CreateRenderer(): 基于窗口创建渲染器(Render)

 * SDL_CreateTexture(): 创建纹理(Texture)

 *

 * [循环渲染数据]

 * SDL_UpdateTexture(): 设置纹理的数据

 * SDL_RenderCopy(): 纹理复制给渲染器

 * SDL_RenderPresent(): 显示

 * SDL_DestroyTexture(texture);

 *

 * [释放资源]

 * SDL_DestroyTexture(texture):  销毁纹理

 * SDL_DestroyRenderer(render): 销毁渲染器

 * SDL_DestroyWindow(win): 销毁窗口

 * SDL_Quit(): 释放SDL2库

 */



2.  使用SDL2开发YUV视频播放器的完整案例

先介绍该案例程序中用到几个的重要变量类型,SDL_Window就是使用SDL时弹出的那个窗口; SDL_Texture用于显示YUV数据,一个SDL_Texture对应一帧YUV数据(案例中提供的YUV视频格式为YUV420p); SDL_Renderer用于将SDL_Texture渲染至SDL_Window; SDL_Rect用于确定SDL_Texture显示的位置。为了简单起见,程序中定义了几个全局变量,变量g_bpp代表1个视频像素占用的位数,例如1个YUV420P格式的视频像素占用12位; 变量g_pixel_w和g_pixel_h代表视频的宽和高,在本案例中提供的测试视频(ande10_yuv420p_352x288.yuv)的宽和高分别为352和288; 变量g_screen_w和g_screen_h代表屏幕的宽和高,在本案例中被初始化为400和300,程序运行中可以通过拖曳窗口的右下角来改变窗口的大小; 变量g_buffer_YUV420p是一字节数组,用于存储1帧YUV420p的视频数据,在播放视频的过程中会循环调用SDL_UpdateTexture()函数以将该数组中存储的视频数据更新到纹理(SDL_Texture)中。refresh_video_SDL2()函数用于定时刷新,在本案例中通过SDL_CreateThread()函数创建了一条线程,将线程的入口函数指定数为refresh_video_SDL2()函数,固定的刷新周期为40ms。本案例的代码如下: 




注意: 
本案例的完整工程及代码可参考chapter3/SDLQtDemo1工程,代码位于main2.cpp文件中。





//chapter3/SDLQtDemo1/main2.cpp

#define SDL_MAIN_HANDLED//如果没有此宏,则会报错,并且要放到SDL.h之前

#include <iostream>

#include <SDL.h>

#include <vector>

using namespace std;



//刷新事件

#define REFRESH_EVENT  (SDL_USEREVENT + 1)



int g_thread_exit = 0;

const int g_bpp = 12;  //YUV420p,1像素占用的位数

const int g_pixel_w = 352,g_pixel_h = 288;//在本案例中YUV420p视频的宽和高

int g_screen_w = 400, g_screen_h = 300;

//1帧视频占用的字节数

unsigned char g_buffer_YUV420p[g_pixel_w * g_pixel_w * g_bpp / 8];



//增加画面刷新机制

int refresh_video_SDL2(void *opaque){

while (g_thread_exit == 0) {

SDL_Event event;

event.type = REFRESH_EVENT;

SDL_PushEvent(&event);

SDL_Delay(40);

}

return 0;

}



int TestYUVPlayer001( ){

if(SDL_Init(SDL_INIT_VIDEO)) {

printf( "Could not initialize SDL - %s\n", SDL_GetError());

return -1;

}



SDL_Window *screen;

//SDL 2.0 对多窗口的支持

screen = SDL_CreateWindow("SDL2-YUVPlayer",

SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,

g_screen_w, g_screen_h,SDL_WINDOW_OPENGL|SDL_WINDOW_RESIZABLE);







if(!screen) {

printf("SDL: could not create window - exiting:%s\n",SDL_GetError());

return -1;

}

//创建渲染器

SDL_Renderer* sdlRenderer = SDL_CreateRenderer(screen, -1, 0);

//创建纹理:格式为YUV420p,宽和高为352x288

SDL_Texture* sdlTexture =

SDL_CreateTexture(sdlRenderer,SDL_PIXELFORMAT_IYUV,

SDL_TEXTUREACCESS_STREAMING, g_pixel_w, g_pixel_h);



FILE *fpYUV420p = NULL; //打开YUV420p视频文件

fpYUV420p = fopen("./ande10_yuv420p_352x288.yuv", "rb+");



if(fpYUV420p == NULL){

printf("cannot open this file\n");

return -1;

}

SDL_Rect sdlRect;

SDL_Thread *refresh_thread =//创建独立线程,用于定时刷新

SDL_CreateThread(refresh_video_SDL2,NULL,NULL);

SDL_Event event;

while(1){

SDL_WaitEvent(&event);//等待事件

if(event.type == REFRESH_EVENT){

fread(g_buffer_YUV420p, 1,

g_pixel_w * g_pixel_h * g_bpp / 8, fpYUV420p);

//将1帧YUV420p的数据更新到纹理中

SDL_UpdateTexture(sdlTexture,NULL,g_buffer_YUV420p,g_pixel_w);



//重新定义窗口大小

sdlRect.x = 0;

sdlRect.y = 0;

sdlRect.w = g_screen_w;

sdlRect.h = g_screen_h;

//Render:渲染三部曲

SDL_RenderClear( sdlRenderer );

SDL_RenderCopy( sdlRenderer, sdlTexture, NULL, &sdlRect);

SDL_RenderPresent( sdlRenderer );

//注意这里不再需要延迟,因为有独立的线程来刷新

//SDL_Delay(40); //休眠40ms

//if(feof(fpYUV420p) != 0 )break;//如果遇到文件尾,则自动退出循环

}else if(event.type==SDL_WINDOWEVENT){

//If Resize:以拖曳方式更改窗口大小

SDL_GetWindowSize(screen,&g_screen_w,&g_screen_h);

}else if(event.type == SDL_QUIT){//退出事件

break;//如果关闭事件,则退出循环

}

}






g_thread_exit = 1;//如果跳出循环,则将退出标志量修改为1

//释放资源

if (sdlTexture){

SDL_DestroyTexture(sdlTexture);

sdlTexture = nullptr;

}

if (sdlRenderer){

SDL_DestroyRenderer(sdlRenderer);

sdlRenderer = nullptr;

}

if (screen){

SDL_DestroyWindow(screen);

screen = nullptr;

}

if (fpYUV420p){

fclose(fpYUV420p);

fpYUV420p = nullptr;

}

SDL_Quit();



return 0;

}






图322SDL2播放YUV420p视频


编译并运行该程序,将ande10_yuv420p_352x288.yuv这两个音频文件复制到buildSDLQtDemo1Desktop_Qt_5_9_8_MinGW_32bitDebug目录下,可以通过拖曳改变窗口大小,效果如图322所示。

3.  SDL2画面刷新机制

实现SDL2的事件与渲染机制之后,增加画面刷新机制就可以作为一个播放器使用了。在上述案例中,通过while循环执行SDL_RenderPresent(renderer)就可以令视频逐帧播放了,但还是需要一个独立的刷新机制。这是因为在一个循环中,重复执行一个函数的效果通常不是周期性的,因为每次加载和处理的数据所消耗的时间是不固定的,因此单纯地在一个循环中使用SDL_RenderPresent(renderer)会令视频播放产生帧率跳动的情况,因此需要引入一个定期刷新机制,令视频的播放有一个固定的帧率。通常使用多线程的方式进行画面刷新管理,主线程进入主循环中等待(SDL_WaitEvent)事件,画面刷新线程在一段时间后发送(SDL_PushEvent)画面刷新事件,主线程收到画面刷新事件后进行画面刷新操作。

画面刷新线程定期构造一个REFRESH_EVENT事件,然后调用SDL_PushEvent()函数将事件发送出来,代码如下: 



//chapter3/help-others.txt

#define REFRESH_EVENT  (SDL_USEREVENT + 1)

int g_thread_exit = 0;

int refresh_video_SDL2(void *opaque){

while (g_thread_exit == 0) {

SDL_Event event;

event.type = REFRESH_EVENT;

SDL_PushEvent(&event);

SDL_Delay(40);

}

return 0;

}



该函数只有两部分内容,第一部分是发送画面刷新事件,也就是发信号以通知主线程来干活; 另一部分是延时,使用一个定时器,保证自己是定期来通知主线程的。首先定义一个“刷新事件”,代码如下: 



#define REFRESH_EVENT   (SDL_USEREVENT + 1)//请求画面刷新事件



SDL_USEREVENT是自定义类型的SDL事件,不属于系统事件,可以由用户自定义,这里通过宏定义便于后续引用,然后调用SDL_PushEvent()函数将事件发送出来,代码如下: 



SDL_PushEvent(&event);//发送画面刷新事件



SDL_PushEvent()是SDL2.0之后引入的函数,该函数能够将事件放入SDL2的事件队列中,当它从事件队列中被取出时,被接收事件的函数识别,并采取相应操作。也就是说在刷新操作中,使用刷新线程不断地将“刷新事件”放到SDL2的事件队列,在主线程中读取SDL2的事件队列里的事件,当发现事件是“刷新事件”时就进行刷新操作。

主线程(main()函数)的主要工作是首先初始化所有的组件和变量,包括SDL2的窗口、渲染器和纹理等,然后进入一个大循环,同时读取事件队列里的事件,如果是刷新事件,则进行渲染相关工作,进行画面刷新。随后需要创建一个缓冲区,每次渲染时都是先从视频文件里读一帧,这一帧先存到缓冲区再交给渲染器去渲染。这个缓冲区的大小应该和视频文件的每帧大小是相同的,这也意味着需要提前计算该视频文件类型的每帧大小,所以需要提前计算好YUV格式的视频帧的大小,例如在本案例中视频文件的格式为YUV420p,宽和高为352×288。主循环其实就是在不断地读取事件队列里的事件,每读取到一个事件,就进行判断,根据该事件的类型采取不同的操作。当收到需要刷新画面的事件后,开始进行读数据帧并渲染的操作,代码如下: 



//chapter3/help-others.txt

while(1){






SDL_WaitEvent(&event); //等待事件

if(event.type == REFRESH_EVENT){

fread(g_buffer_YUV420p,1,g_pixel_w*g_pixel_h*g_bpp/8, fpYUV420p);

//将1帧YUV420p的数据更新到纹理中

SDL_UpdateTexture( sdlTexture,NULL,g_buffer_YUV420p,g_pixel_w);



//渲染三部曲

SDL_RenderClear( sdlRenderer );

SDL_RenderCopy( sdlRenderer, sdlTexture, NULL, &sdlRect);

SDL_RenderPresent( sdlRenderer );

if(feof(fpYUV420p) != 0 )break;//如果遇到文件尾,则自动退出循环

}else if(event.type==SDL_WINDOWEVENT){

//重定义窗口大小

SDL_GetWindowSize(screen,&g_screen_w,&g_screen_h);

}else if(event.type == SDL_QUIT){

break;

}

}



3.3.6使用FFmpeg+SDL2读取本地摄像头并渲染

使用FFmpeg可以打开本地摄像头并循环读取视频帧数据,然后可以调用SDL2对视频帧进行渲染。打开Qt Creator,创建一个基于Widget的Qt Widgets Application项目(项目名称为FFmpegSDL2QtMonitor),如图323所示。



图323新建Qt的Widgets项目



双击widget.ui界面文件,界面中使用QVerticalLayout进行布局,然后往该界面中拖曳一个QLabel和两个QPushButton(它们的文本分别为Stop Camera和Start Camera),如图324所示。



图324设计widget.ui界面


右击QPushButton按钮,在弹出的菜单中选择“转到槽”,分别为这两个QPushButton按钮添加clicked()槽函数,如图325所示。



图325为QPushButton添加槽函数


1.  封装一个类QtFFmpegCamera用于操作FFmpeg

新增一个类QtFFmpegCamera用于操作FFmpeg,为了响应Qt的信号槽机制,将该类的父类设置为QObject。为了方便使用,需要创建一个Play()和SetStopped()等公共成员函数,并且新增AVPicture、AVFormatContext、AVCodecContext、AVFrame、SwsContext和AVPacket 等类型的私有成员函数。该类的头文件为qtffmpegcamera.h,代码如下: 



//chapter3/FFmpegSDL2QtMonitor/qtffmpegcamera.h

#ifndef QTFFMPEGCAMERA_H

#define QTFFMPEGCAMERA_H







//必须加以下内容,否则编译不能通过,为了兼容C和C99标准

#ifndef INT64_C

#define INT64_C

#define UINT64_C

#endif



//引入FFmpeg和SDL2的头文件

#include <iostream>

extern "C"

{

#include <libavcodec/avcodec.h>

#include <libavformat/avformat.h>

#include <libswscale/swscale.h>

#include <libavdevice/avdevice.h>

#include <libavfilter/avfilter.h>

#include <libavutil/imgutils.h>

#include <SDL.h>

#include <SDL_main.h>

};

#undef main





using namespace std;



#include <QObject>

#include <QMutex>

#include <QImage>



class QtFFmpegCamera : public QObject

{

Q_OBJECT

public:

explicit QtFFmpegCamera(QObject *parent = nullptr);



void Play();



int GetVideoWidth() const { return this->videoWidth; }

int GetVideoHeight() const {return this->videoHeight; }

int GetVideoStreamIndex() const { return this->videoStreamIndex; }

QString GetVideoURL() const {return this->videoURL; }

void SetVideoURL(QString url){this->videoURL = url; }

void SetStopped(int st){this->stopped = st;}



private:

AVPicture  pAVPicture;

AVFormatContext *pAVFormatCtx;

AVCodecContext  *pAVCodecContext;

AVFrame *pAVFrame;






SwsContext * pSwsContext;//将YUYV422 转换为YUV420p

SwsContext * pSwsContext2;//将YUV420p 转换为 RGB888 (RGB24)

AVPacket pAVPacket;



QMutexmutex;

intvideoWidth;

intvideoHeight;

intvideoStreamIndex;

QStringvideoURL;

intstopped;



signals:

void GetImage(const QImage &image);



public slots:

};



#endif //QTFFMPEGCAMERA_H



在该类的构造函数QtFFmpegCamera::QtFFmpegCamera(QObject *parent)中对成员变量进行初始化,代码如下: 



//chapter3/FFmpegSDL2QtMonitor/qtffmpegcamera.cpp

QtFFmpegCamera::QtFFmpegCamera(QObject *parent) : QObject(parent)

{

stopped = 0;

pAVFormatCtx = NULL;

pAVCodecContext = NULL;

pSwsContext = NULL;

pSwsContext2 = NULL;

pAVFrame = NULL;

}



在该类的成员函数QtFFmpegCamera::Play()中初始化FFmpeg和SDL2,使用FFmpeg打开本地摄像头并循环读取视频帧数据,然后调用SDL2进行渲染,代码如下: 



//chapter3/FFmpegSDL2QtMonitor/qtffmpegcamera.cpp

void QtFFmpegCamera::Play(){

avformat_network_init();//初始化FFmpeg的网络库



pAVFormatCtx = avformat_alloc_context();//分配格式化上下文环境

pAVFormatCtx->probesize = 10000 * 1024;

pAVFormatCtx->duration = 10 * AV_TIME_BASE;



//1. 打开本地摄像头

OpenLocalCamera(pAVFormatCtx, true);



printf("---------File Information:输出格式信息---------\n");






av_dump_format(pAVFormatCtx, 0, NULL, 0);



//2.寻找视频流信息

if (avformat_find_stream_info(pAVFormatCtx, NULL) < 0)

{

printf("Couldn't find stream information.\n");

return ;

}



//打开视频以获取视频流,设置视频默认索引值

int videoindex = -1;

for (int i = 0; i < pAVFormatCtx->nb_streams; i++)

{

if (pAVFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)

{

videoStreamIndex = videoindex = i;

//break;

}

}

//如果没有找到视频的索引,则说明没有视频流

if (videoindex == -1) {

printf("Didn't find a video stream.\n");

return ;

}



//3. 打开解码器

//AVCodecContext 为解码上下文结构体

//avcodec_alloc_context3  为解码分配函数

//avcodec_parameters_to_context 为参数格式转换

//avcodec_find_decoder(codec_ID) 用于查找解码器

//avcodec_open2 用于打开解码器

//分配解码器上下文

pAVCodecContext = avcodec_alloc_context3(NULL);

//获取解码器上下文信息

if (avcodec_parameters_to_context(pAVCodecContext, pAVFormatCtx->streams[videoindex]->codecpar) < 0)

{

cout << "Copy stream failed!" << endl;

return ;

}

//查找解码器//codec_id=13

//AV_CODEC_ID_RAWVIDEO: 13

//AV_CODEC_ID_H264: 27

printf("codec_id=%d\n", pAVCodecContext->codec_id);

AVCodec *pCodec = avcodec_find_decoder(pAVCodecContext->codec_id);

if (pCodec == NULL) {

printf("Codec not found.\n");

return ;

}






//打开解码器

if (avcodec_open2(pAVCodecContext, pCodec, NULL) < 0)

{

printf("Could not open codec.\n");

return ;

}



//4. 格式转换

//(1)sdl: yuyv422--->yuv420p:SDL2渲染需要使用YUV420p格式

//(2)Qt : yuv420p--->rgb24:Qt渲染需要使用RGB24格式

//对图形进行裁剪以便于显示得更好

pSwsContext = sws_getContext(

pAVCodecContext->width, pAVCodecContext->height, pAVCodecContext->pix_fmt,pAVCodecContext->width, pAVCodecContext->height, AV_PIX_FMT_YUV420P,SWS_BICUBIC, NULL, NULL, NULL);



pSwsContext2 = sws_getContext(

pAVCodecContext->width, pAVCodecContext->height, AV_PIX_FMT_YUV420P,pAVCodecContext->width, pAVCodecContext->height, AV_PIX_FMT_RGB24,SWS_BICUBIC, NULL, NULL, NULL);





if (NULL == pSwsContext) {

cout << "Get swscale context failed!" << endl;

return ;

}



//获取视频流的分辨率大小

//pAVCodecContext = pAVFormatContext->streams[videoStreamIndex]->codec;

videoWidth = pAVCodecContext->width; //视频宽度

videoHeight = pAVCodecContext->height;//视频高度

avpicture_alloc(&pAVPicture, AV_PIX_FMT_RGB24, videoWidth, videoHeight);
//以视频格式及分辨率来分配内存





//5. SDL2.0:初始化SDL2的库

if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)){

printf("Could not initialize SDL - %s\n", SDL_GetError());

return;

}

//SDL2的多窗口支持

int screen_w = pAVCodecContext->width;

int screen_h = pAVCodecContext->height;

SDL_Window* screen = SDL_CreateWindow("FFmpegPlayer", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, screen_w, screen_h, SDL_WINDOW_OPENGL);

if (!screen){

printf("SDL: could not create window - exiting:%s\n", SDL_GetError());

return ;

}






//创建渲染器

SDL_Renderer* sdlRenderer = SDL_CreateRenderer(screen, -1, 0);



//创建纹理

//IYUV: Y + U + V  (3 planes):3个平面

//YV12: Y + V + U  (3 planes):3个平面

SDL_Texture* sdlTexture = SDL_CreateTexture(sdlRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING,pAVCodecContext->width, pAVCodecContext->height);



SDL_Rect sdlRect;

sdlRect.x = 0;

sdlRect.y = 0;

sdlRect.w = screen_w;

sdlRect.h = screen_h;



//创建渲染刷新线程:thread.sdl

SDL_Thread *video_tid = SDL_CreateThread(sfp_refresh_thread, NULL, NULL);





//6. 使用FFmpeg解封装并解码


AVPacket *packet = (AVPacket *)av_malloc(sizeof(AVPacket));

AVFrame *pFrame = av_frame_alloc();

AVFrame *pFrameYUV420p = av_frame_alloc();

uint8_t *out_buffer = (uint8_t *)av_malloc(av_image_get_buffer_size(

AV_PIX_FMT_YUV420p,pAVCodecContext->width,pAVCodecContext->height, 1));

av_image_fill_arrays( pFrameYUV420p->data, pFrameYUV420p->linesize, out_buffer,AV_PIX_FMT_YUV420P,pAVCodecContext->width, AVCodecContext->height, 1);





//一帧一帧地读取视频:事件循环

SDL_Event event;

for (;;) {

if(this->stopped){ //检测是否需要停止

thread_exit  = 1;

break;

}

//等待事件:Wait

SDL_WaitEvent(&event);

if (event.type == SFM_REFRESH_EVENT) {

//----------读取视频包--------------------

if (av_read_frame(pAVFormatCtx, packet) >= 0) //解封装

{

if (packet->stream_index == videoindex) {

//解码 : YUYV422--->YUV420p

decode(pAVCodecContext, packet, pFrame, pFrameYUV420p, pSwsContext);





//格式转换: YUV420p-->RGB24

mutex.lock();






sws_scale(pSwsContext2,

(const uint8_t* const *)pFrameYUV420p->data,

pFrameYUV420p->linesize,

0,videoHeight,

pAVPicture.data,

pAVPicture.linesize);

//Qt发送获取一帧图像信号:注意下面两行代码使用的是Qt的渲染机制

//QImage image(pAVPicture.data[0],videoWidth,videoHeight,QImage::Format_RGB888);

//emit GetImage(image);

mutex.unlock();



//SDL2:渲染视频帧

SDL_UpdateTexture(sdlTexture, NULL, pFrameYUV420p->data[0], pFrameYUV420p->linesize[0]);

SDL_RenderClear(sdlRenderer);

SDL_RenderCopy(sdlRenderer, sdlTexture, &sdlRect, &sdlRect);

SDL_RenderCopy(sdlRenderer, sdlTexture, NULL, NULL);

SDL_RenderPresent(sdlRenderer);

}

av_packet_unref(packet);

}

else  {

//退出线程

thread_exit = 1;

}

}

else if (event.type == SDL_KEYDOWN) {

qDebug() << "keywdown";

//暂停

if (event.key.keysym.sym == SDLK_SPACE)

thread_pause = !thread_pause;

else if (event.key.keysym.sym == SDLK_ESCAPE){

thread_exit = 1;

qDebug() << SDLK_ESCAPE;

}

}

else if (event.type == SDL_QUIT) {

thread_exit = 1;

}

else if (event.type == SFM_BREAK_EVENT) {

break;

}

}





sws_freeContext(pSwsContext);

SDL_Quit();

av_frame_free(&pFrameYUV420p);






av_frame_free(&pFrame);

avcodec_close(pAVCodecContext);

avformat_close_input(&pAVFormatCtx);

}



在qtffmpegcamera.cpp文件中有一个OpenLocalCamera()静态函数,它的功能是调用FFmpeg来打开本地摄像头,代码是通用的,可以兼容Windows和Linux平台,但传递的参数略有区别。该函数的完整代码如下: 



//chapter3/FFmpegSDL2QtMonitor/qtffmpegcamera.cpp

/***********************

* 打开本地摄像头

***********************/

static int OpenLocalCamera(AVFormatContext *pFormatCtx, bool isUseDshow = false)

{

avdevice_register_all();//注册Libavdevice库

#ifdef _WIN32

if (isUseDshow) {//使用DShow方式打开摄像头

AVInputFormat *ifmt = av_find_input_format("dshow");

//设置视频设备的名称

//if (avformat_open_input(&pFormatCtx, "video=Lenovo EasyCamera", ifmt, NULL) != 0)

if (avformat_open_input(&pFormatCtx, "video=HP TrueVision HD Camera", ifmt, NULL) != 0) {

printf("Couldn't open input stream.(无法打开输入流)\n");

return -1;

}

}

else {//使用VFW方式打开摄像头

AVInputFormat *ifmt = av_find_input_format("vfwcap");

if (avformat_open_input(&pFormatCtx, "0", ifmt, NULL) != 0) {

printf("Couldn't open input stream.(无法打开输入流)\n");

return -1;

}

}

#endif

//Linux平台

#ifdef linux

AVInputFormat *ifmt = av_find_input_format("video4linux2");

if (avformat_open_input(&pFormatCtx, "/dev/video0", ifmt, NULL) != 0) {

printf("Couldn't open input stream.(无法打开输入流)\n");

return -1;

}



#endif



return 0;



}








注意: 
在Windows平台下avformat_open_input(&pFormatCtx, "video=HP TrueVision HD Camera", ifmt, NULL)函数中的参数需要修改为读者本地的摄像头名称,例如笔者这里的摄像头名称为HP TrueVision HD Camera。



在qtffmpegcamera.cpp文件中有一个sfp_refresh_thread()静态函数,它的功能是手工创建SDL的刷新事件,以此来实时刷新视频帧,代码如下: 



//chapter3/FFmpegSDL2QtMonitor/qtffmpegcamera.cpp

//Refresh Event

#define SFM_REFRESH_EVENT  (SDL_USEREVENT + 1)

#define SFM_BREAK_EVENT  (SDL_USEREVENT + 2)

static int thread_exit = 0;

static int thread_pause = 0;



static int sfp_refresh_thread(void *opaque)

{

thread_exit = 0;

thread_pause = 0;



while (!thread_exit){

if (!thread_pause) {//判断是否暂停

//手工创建SDL刷新事件

SDL_Event event;

event.type = SFM_REFRESH_EVENT;

SDL_PushEvent(&event);

}

SDL_Delay(5);



}

thread_exit = 0;

thread_pause = 0;

//Break:退出事件

SDL_Event event;

event.type = SFM_BREAK_EVENT;

SDL_PushEvent(&event);

return 0;

}



2. 新增1个Qt的线程类来调用FFmpeg

因为读取视频帧需要使用while循环,从界面上单击Start Camera按钮之后,如果直接进入while循环,就会导致界面僵死,所以需要开启一个独立的线程来源源不断地读取视频帧并解码,然后将解码出来的YUV帧数据送给SDL2进行渲染。可以使用Qt的QThread类来封装一个线程类,头文件代码如下: 



//chapter3/FFmpegSDL2QtMonitor/qtcamerathread.h

#ifndef QTCAMERATHREAD_H

#define QTCAMERATHREAD_H



#include <QThread>

#include "qtffmpegcamera.h"



class QtCameraThread : public QThread{

Q_OBJECT

public:

explicit QtCameraThread(QObject *parent = nullptr);

void run();

void setffmpeg(QtFFmpegCamera *f){ffmpeg=f;}



private:

QtFFmpegCamera * ffmpeg;



signals:



public slots:

};



#endif //QTCAMERATHREAD_H



QtCameraThread类的基类是QThread,在该类中有一个QtFFmpegCamera类型的私有成员变量,用于操作FFmpeg,然后需要重写QThread的run()虚函数,代码如下: 



//chapter3/FFmpegSDL2QtMonitor/qtcamerathread.cpp

#include "qtcamerathread.h"

QtCameraThread::QtCameraThread(QObject *parent) :

QThread(parent) {

}

void  QtCameraThread::run() {

ffmpeg->Play();

}



由此可见,在run()虚函数中主要调用QtFFmpegCamera类的Play()函数,先打开本地摄像头,然后循环读取视频帧并通过SDL2进行渲染。

3.  通过Qt的界面按钮开启或停止摄像头

在Start Camera按钮的Widget::on_pushButton_OpenCamera_clicked()槽函数中开启线程即可,代码如下: 



//chapter3/FFmpegSDL2QtMonitor/qtcamerathread.cpp

void Widget::on_pushButton_OpenCamera_clicked(){

qDebug() << "clicked\n";

//修改按钮状态







ui->pushButton_stop->setEnabled(true);

ui->pushButton_OpenCamera->setEnabled(false);

objFmpg.SetStopped(0);

//objFmpg.Play();//注意:不要直接调用FFmpeg封装类的Play()函数,否则会导致界面僵死

//通过线程方式来开启FFmpeg封装类的Play()函数

QtCameraThread *rtsp = new QtCameraThread(this);

rtsp->setffmpeg(&objFmpg);

rtsp->start();

}



在Stop Camera按钮的Widget::on_pushButton_stop_clicked()槽函数中修改播放状态即可,代码如下: 



//chapter3/FFmpegSDL2QtMonitor/qtcamerathread.cpp

void Widget::on_pushButton_stop_clicked(){

objFmpg.SetStopped(1);//将播放状态设置为 Stopped即可

//修改按钮的状态

ui->pushButton_stop->setEnabled(false );

ui->pushButton_OpenCamera->setEnabled( true);

}



编译并运行该程序,发现使用SDL2渲染时会弹出一个单独的窗口,如图326所示。



图326SDL2弹出的窗口


4.  将SDL2弹出的窗口嵌入Qt的QLabel中

可以将SDL2弹出的窗口嵌入Qt的QLabel中,将SDL_CreateWindow()函数替换为SDL_CreateWindowFrom()函数即可,代码如下: 



//chapter3/FFmpegSDL2QtMonitor/qtcamerathread.cpp

//SDL_Window* screen = SDL_CreateWindow("FFmpegPlayer", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, screen_w, screen_h, SDL_WINDOW_OPENGL);

//将SDL2窗口嵌入Qt子窗口的方法

SDL_Window* screen  = SDL_CreateWindowFrom( m_MainWidget  );



重新编译并运行该程序,SDL2的窗口已经被嵌入Qt的QLabel标签中,如图327所示。



图327将SDL2弹出的窗口嵌入Qt窗口中


3.4FFmpeg+Qt读取并显示本地摄像头

信号与槽(Signal & Slot)是Qt编程的基础,也是Qt的一大创新。因为有了信号与槽的编程机制,在Qt中处理界面各个组件的交互操作时会变得更加直观和简单。信号槽是Qt框架引以为豪的机制之一。所谓信号槽,实际就是观察者模式。当某个事件发生之后,例如按钮检测到自己被单击了一下,它就会发出一个信号(Signal)。以这种方式发出信号类似广播。如果有对象对这个信号感兴趣,则可以使用连接(Connect)函数将想要处理的信号和自己的一个槽函数(Slot)绑定,以此来处理这个信号。也就是说,当信号发出时,被连接的槽函数会自动被回调。信号与槽机制是Qt GUI编程的基础,使用信号与槽机制可以比较容易地将信号与响应代码关联起来。

3.4.1信号

信号(Signal)就是在特定情况下被发射的事件,例如下压式按钮(PushButton)最常见的信号就是鼠标单击时发射的clicked()信号,而一个组合下拉列表(ComboBox)最常见的信号是选择的列表项变化时发射的CurrentIndexChanged()信号。GUI 程序设计的主要内容就是对界面上各组件的信号进行响应,只需知道什么情况下发射哪些信号,合理地去响应和处理这些信号就可以了。信号是一个特殊的成员函数声明,返回值的类型为void,只能声明而不能通过定义实现。信号必须用signals关键字声明,访问属性为protected,只能通过emit关键字调用(发射信号)。当某个信号对其客户或所有者发生的内部状态发生改变时,信号被一个对象发射。只有定义过这个信号的类及其派生类能够发射这个信号。当一个信号被发射时,与其相关联的槽将被立刻执行,就像一个正常的函数调用一样。信号槽机制完全独立于任何GUI事件循环。只有当所有的槽返回以后发射函数(emit)才返回。如果存在多个槽与某个信号相关联,则当这个信号被发射时,这些槽将会一个接一个地执行,但执行的顺序将会是随机的,不能人为地指定哪个先执行、哪个后执行。信号的声明是在头文件中进行的,Qt的signals关键字用于指出进入了信号声明区,随后即可声明自己的信号,代码如下: 



signals:

void mycustomsignals();



signals是QT的关键字,而非C/C++的关键字。信号可以重载,但信号却没有函数体定义,并且信号的返回类型都是void,不要指望能从信号返回什么有用信息。信号由MOC自动产生,不应该在.cpp文件中实现。

3.4.2槽

槽(Slot)就是对信号响应的函数,即槽就是一个函数,与一般的C++函数是一样的,可以定义在类的任何部分(public、private 或 protected),可以具有任何参数,也可以被直接调用。槽函数与一般函数的不同点在于: 槽函数可以与一个信号关联,当信号被发射时,关联的槽函数被自动执行。槽也能够声明为虚函数。槽的声明也是在头文件中进行的,代码如下: 



public slots:

void setValue(int value);



只有QObject的子类才能自定义槽,定义槽的类必须在类声明的最开始处使用Q_OBJECT,类中声明槽需要使用slots关键字,槽与所处理的信号在函数签名上必须一致。

3.4.3信号与槽的关联

信号与槽关联是用 QObject::connect() 函数实现的,其代码如下: 



//chapter3/qt-help-apis.txt

//QObject::connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));

bool QObject::connect ( const QObject * sender, const char * signal,

const QObject * receiver, const char *method,

Qt::ConnectionType type = Qt::AutoConnection );



connect()函数是QObject 类的一个静态函数,而 QObject 是所有 Qt 类的基类,在实际调用时可以忽略前面的限定符,所以可以直接写为如下形式。



connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));



其中,sender 是发射信号的对象的名称,signal() 是信号名称。信号可以看作特殊的函数,需要带圆括号,有参数时还需要指明参数。receiver 是接收信号的对象名称,slot() 是槽函数的名称,需要带圆括号,有参数时还需要指明参数。SIGNAL 和 SLOT 是Qt 的宏,用于指明信号和槽,并将它们的参数转换为相应的字符串。一段简单的代码如下: 



QObject::connect(btnClose, SIGNAL(clicked()), Widget, SLOT(close()));




这行代码的作用就是将 btnClose按钮的clicked()信号与窗体(Widget)的槽函数close()相关联,当单击btnClose按钮(界面上的Close按钮)时,就会执行Widget的close()槽函数。

当信号与槽没有必要继续保持关联时,可以使用disconnect函数来断开连接,代码如下: 



bool QObject::disconnect (const QObject * sender, const char * signal,

const QObject * receiver, const char *method);




disconnect()函数用于断开发射者中的信号与接收者中的槽函数之间的关联。在disconnect()函数中0可以用作一个通配符,分别表示任何信号、任何接收对象、接收对象中的任何槽函数,但是发射者sender不能为0,其他3个参数的值可以等于0。以下3种情况需要使用disconnect()函数断开信号与槽的关联。

(1) 断开与某个对象相关联的任何对象,代码如下: 



disconnect(sender, 0, 0, 0);

sender->disconnect();




(2) 断开与某个特定信号的任何关联,代码如下: 



disconnect(sender, SIGNAL(mySignal()), 0, 0);

sender->disconnect(SIGNAL(mySignal()));




(3) 断开两个对象之间的关联,代码如下: 



disconnect(sender, 0, receiver, 0);

sender->disconnect(receiver);




3.4.4信号与槽的注意事项

Qt利用信号与槽(Signal/Slot)机制取代传统的回调函数机制(callback)进行对象之间的沟通。当操作事件发生时,对象会提交一个信号(Signal),而槽(Slot)则是一个函数接收特定信号并且运行槽本身设置的动作。信号与槽之间需要通过QObject的静态方法connect()函数连接。信号在任何运行点上皆可发射,甚至可以在槽里再发射另一个信号,信号与槽的链接不限定为一对一的链接,一个信号可以链接到多个槽或者多个信号链接到同一个槽,甚至信号也可连接到信号。以往的callback缺乏类型安全,在调用处理函数时,无法确定是传递正确型态的参数,但信号和其接收的槽之间传递的数据型态必须相匹配,否则编译器会发出警告。信号和槽可接收任何数量、任何形态的参数,所以信号与槽机制是完全类型安全。信号与槽机制也确保了低耦合性,发送信号的类并不知道可被哪个槽接收,也就是说一个信号可以调用所有可用的槽。此机制会确保当“连接”信号和槽时,槽会接收信号的参数并且正确运行。关于信号与槽的使用,需要注意以下规则。

(1) 一个信号可以连接多个槽,代码如下: 



connect(spinNum, SIGNAL(valueChanged(int)), this, SLOT(addFun(int));

connect(spinNum, SIGNAL(valueChanged(int)), this, SLOT(updateStatus(int));




当一个对象spinNum的数值发生变化时,所在窗体有两个槽函数进行响应,一个addFun()函数用于计算,另一个updateStatus()函数用于更新状态。当一个信号与多个槽函数关联时,槽函数按照建立连接时的按顺序依次执行。当信号和槽函数带有参数时,在connect()函数里要写明参数的类型,但可以不写参数名称。

(2) 多个信号可以连接同一个槽,例如将3个选择颜色的RadioButton的clicked()信号关联到相同的一个自定义槽函数setTextFontColor(),代码如下: 



//chapter3/qt-help-apis.txt

connect(ui->rBtnBlue,SIGNAL(clicked()),this,SLOT(setTextFontColor()));

connect(ui->rBtnRed,SIGNAL(clicked()),this,SLOT(setTextFontColor()));

connect(ui->rBtnBlack,SIGNAL(clicked()),this,SLOT(setTextFontColor()));




当任何一个 RadioButton 被单击时,都会执行 setTextFontColor() 槽函数。

(3) 一个信号可以连接另外一个信号,代码如下: 



connect(spinNum, SIGNAL(valueChanged(int)), this, SIGNAL (refreshInfo(int));




当一个信号发射时,也会发射另外一个信号,实现某些特殊的功能。

(4) 在严格的情况下,信号与槽的参数的个数和类型需要一致,至少信号的参数不能少于槽的参数。如果不匹配,则会出现编译错误或运行错误。

(5) 在使用信号与槽的类中,必须在类的定义中加入宏 Q_OBJECT。

(6) 当一个信号被发射时,与其关联的槽函数通常会被立即执行,就像正常调用一个函数一样。只有当信号关联的所有槽函数执行完毕后,才会执行发射信号处后面的代码。

3.4.5元对象工具

元对象编译器(Meta Object Compiler,MOC)对C++文件中的类声明进行分析并生成用于初始化元对象的C++代码,元对象包含全部信号和槽的名字及指向槽函数的指针。

当MOC读C++源文件时,如果发现有Q_OBJECT宏声明的类,就会生成另外一个C++源文件,新生成的文件中包含该类的元对象代码。假设有一个头文件mysignal.h,在这个文件中包含信号或槽的声明,那么在编译之前MOC工具就会根据该文件自动生成一个名为mysignal.moc.h的C++源文件并将其提交给编译器; 对应的mysignal.cpp文件MOC工具将自动生成一个名为mysignal.moc.cpp的文件提交给编译器。

元对象代码是Signal/Slot机制所必需的。用MOC生成的C++源文件必须与类实现一起进行编译和连接,或者用#include语句将其包含到类的源文件中。MOC并不扩展#include或者#define宏定义,只是简单地跳过所遇到的任何预处理指令。

信号和槽函数的声明一般位于头文件中,同时在类声明的开始位置必须加上Q_OBJECT语句,Q_OBJECT语句将告诉编译器在编译之前必须先应用MOC工具进行扩展。关键字signals是对信号的声明,signals默认为protected等属性。关键字slots是对槽函数的声明,slots有public、private、protected等属性。signals、slots关键字是Qt 自己定义的,不是C++中的关键字。信号的声明类似于函数的声明而非变量的声明,左边要有类型,右边要有括号,如果要向槽中传递参数,则可在括号中指定每个形式参数的类型,而形式参数的个数可以多于一个。关键字slots指出随后开始槽的声明,这里slots用的也是复数形式。槽的声明与普通函数的声明一样,可以携带零或多个形式参数。既然信号的声明类似于普通C++函数的声明,那么信号也可采用C++中虚函数的形式进行声明,即同名但参数不同。例如,第1次定义的void mySignal()没有带参数,而第2次定义的却带有参数,从这里可以看出Qt的信号机制是非常灵活的。信号与槽之间的联系必须事先用connect()函数进行指定。如果要断开二者之间的联系,则可以使用disconnect()函数。

3.4.6案例: 标准信号槽

新建一个Qt Widgets Application项目(笔者的项目名称为MySignalSlotsDemo),基类选择QWidget,如图328所示,然后在构造函数中动态地创建一个按钮,实现单击按钮关闭窗口的功能。编译并运行该程序,效果如图329所示。本项目包含的代码如下: 




注意: 
该案例的完整工程代码可参考本书源码中的chapter3/MySignalSlotsDemo,建议读者先下载源码将工程运行起来,然后结合本书进行学习。





//chapter3/MySignalSlotsDemo/widget.h

//widget.h头文件////

#ifndef WIDGET_H

#define WIDGET_H



#include <QWidget>



namespace Ui {






class Widget;

}



class Widget : public QWidget

{

Q_OBJECT



public:

explicit Widget(QWidget *parent = nullptr);

~Widget();



private:

Ui::Widget *ui;

};

#endif //WIDGET_H





/////widget.cpp文件//////

#include "widget.h"

#include "ui_widget.h"

#include <QPushButton>



Widget::Widget(QWidget *parent) :

QWidget(parent),

ui(new Ui::Widget)

{

ui->setupUi(this);



//创建一个按钮

QPushButton *btn = new QPushButton;

//btn->show(); //以顶层方式弹出窗口控件

//让btn依赖在myWidget窗口中

btn->setParent(this);//this指当前窗口

btn->setText("关闭");

btn->move(100,100);



//关联信号和槽:单击按钮关闭窗口

//参数1:信号发送者;参数2:发送的信号(函数地址)

//参数3:信号接收者;参数4:处理槽函数地址

connect(btn, &QPushButton::clicked, this, &QWidget::close);



}



Widget::~Widget()

{

delete ui;

}






图328Qt Widgets项目的基类选择




图329Qt信号槽的运行效果


3.4.7案例: 自定义信号槽

当Qt提供的标准信号和槽函数无法满足需求时,就需要用到自定义信号槽,可以使用emit关键字来发射信号。例如定义老师和学生两个类(都继承自QObject),当老师发出“下课”信号时,学生响应“去吃饭”的槽功能。由于“下课”不是Qt标准的信号,所以需要用到自定义信号槽机制。这里不再创建新的Qt项目,直接使用3.4.6节的MySignalSlotsDemo项目,先添加两个自定义类Teacher和Student,它们都继承自QObject。右击项目名称MySignalSlotsDemo,在弹出的菜单中选择 Add New... 菜单选项,如图330所示,然后在弹出的“新建文件”对话框中,单击左侧的C++模板,在右侧选择 C++ Class,如图331所示,接着在弹出的C++ Class对话框中,输入Class name(Teacher),在Base class下拉列表中选择QObject,如图332所示,再以同样的步骤创建Student类,成功后,项目中多了两个类(Teacher和Student),如图333所示。



图330Qt项目中通过Add New添加新项




图331Qt项目中选择C++ Class




图332Qt项目中添加新类并选择QObject基类




图333Qt项目中添加了两个类



在Teacher类中添加一个“下课”信号(finishClass),代码如下: 



//chapter3/MySignalSlotsDemo/student.h

signals:

//自定义信号,写到signals下

//返回值为void,只用申明,不需要实现

//可以有参数,也可以重载

void finishClass();




在Student类中添加一个“去吃饭”槽(gotoEat),代码如下: 



//chapter3/MySignalSlotsDemo/student.h

public slots:

//早期Qt版本需要写到public slots下,高级版本可以写到public或全局下

//返回值为void,需要声明,也需要实现

//可以有参数,也可以重载

void gotoEat();



//sutdent.cpp

void Student::gotoEat()






{

qDebug() << "准备去吃饭......";

}




在Widget类中声明老师类(Student)和学生类(Student)的成员变量,并在构造函数中通过new创建实例,然后通过connect()函数来关联老师类的“下课”信号和学生类的“去吃饭”槽,代码如下: 



//chapter3/MySignalSlotsDemo/widget.h

private:

Teacher *m_teacher;

Student *m_student;



//widget.cpp

Widget::Widget(QWidget *parent):

QWidget(parent),

ui(new Ui::Widget)

{

...

//创建老师对象

this->m_teacher = new Teacher(this);

//创建学生对象

this->m_student = new Student(this);

//连接老师的"下课"信号和学生的"去吃饭"槽函数

connect(m_teacher,&Teacher::finishClass,

m_student,&Student::gotoEat);

}




在界面上拖曳一个按钮,将文本内容修改为“下课”,用来模拟老师的下课信号,然后双击这个按钮,在Qt自动生成的Widget::on_pushButton_clicked()函数中添加的代码如下: 



//chapter3/MySignalSlotsDemo/widget.cpp

void Widget::on_pushButton_clicked()

{

//通过emit发射信号

emit this->m_teacher->finishClass();

}




编译并运行该程序,单击“下课”按钮后会在控制台输出“准备去吃饭……”,证明学生类的槽函数被成功地触发了,如图334所示。在本案例中老师类和学生类的相关代码如下(其余代码读者可参考源码工程): 



//chapter3/MySignalSlotsDemo/teacher.h

////teacher.h////

#ifndef TEACHER_H

#define TEACHER_H






#include <QObject>



class Teacher : public QObject

{

Q_OBJECT

public:

explicit Teacher(QObject *parent = nullptr);



signals:

//自定义信号,写到signals下

//返回值为void,只用声明,不需要实现

//可以有参数,也可以重载

void finishClass();



public slots:



};

#endif //TEACHER_H





////teacher.cpp////

#include "teacher.h"

Teacher::Teacher(QObject *parent) : QObject(parent)

{



}





////student.h////

#ifndef STUDENT_H

#define STUDENT_H

#include <QObject>



class Student : public QObject

{

Q_OBJECT

public:

explicit Student(QObject *parent = nullptr);



signals:



public slots:

//早期Qt版本需要写到public slots下,高级版本可以写到public或全局下

//返回值为void,需要声明,也需要实现

//可以有参数,也可以重载

void gotoEat();

};

#endif //STUDENT_H







////student.cpp////

#include "student.h"

#include <QDebug>

Student::Student(QObject *parent) : QObject(parent)

{

}



void Student::gotoEat()

{

qDebug() << "准备去吃饭......";

}







图334Qt项目中自定义信号槽的应用


3.4.8Qt显示图像

Qt可显示基本的图像类型,利用QImage、QPxmap类可以实现图像的显示,并且利用类中的方法可以实现图像的基本操作(缩放、旋转等)。Qt可以直接读取并显示的格式有BMP、GIF、JPG、JPEG、PNG、TIFF、PBM、PGM、PPM、XBM和XPM等。可以使用QLabel显示图像,QLabel类有setPixmap()函数,可以用来显示图像。也可以直接用QPainter画出图像。如果图像过大,则可直接用QLabel显示,此时将会出现有部分图像显示不出来,可以用Scroll Area部件解决此问题。

首先使用QFileDialog类的静态函数getOpenFileName()打开一张图像,将图像文件加载进QImage对象中,再用QPixmap对象获得图像,最后用QLabel选择一个QPixmap图像对象进行显示。该过程的关键代码如下(完整代码可参考chapter3/QtImageDemo工程): 



//chapter3/QtImageDemo/widget.cpp

//Qt显示图片

void Widget::on_btnShowImage_clicked()

{

QString filename;

filename = QFileDialog::getOpenFileName(this,

tr("选择图像"),"",tr("Images (*.png *.bmp *.jpg *.tif *.gif )"));

if(filename.isEmpty()){

return;

}

else{

m_img = new QImage;

if(! ( m_img->load(filename) ) ) //加载图像

{

QMessageBox::information(this,

tr("打开图像失败"),tr("打开图像失败!"));

delete m_img;

return;

}

ui->lblImage->setPixmap(QPixmap::fromImage(*m_img));

}

}




QImage对图像的像素级访问进行了优化,QPixmap使用底层平台的绘制系统进行绘制,无法提供像素级别的操作,而QImage则使用了独立于硬件的绘制系统。编译并运行该工程,单击“显示图像”按钮,选择一张本地的图片,如图335所示。



图335Qt使用QImage和QPixmap显示图像


3.4.9Qt缩放图像

Qt缩放图像可以用scaled函数实现,代码如下: 



//chapter3/qt-help-apis.txt

QImage QImage::scaled (const QSize & size,Qt::AspectRatioMode aspectRatioMode = Qt::IgnoreAspectRatio, Qt::TransformationModetransformMode = Qt::FastTransformation) const;




利用上面已经加载成功的图像(m_img),在scaled函数中width和height表示缩放后图像的宽和高,即将原图像缩放到(width×height)大小。例如在本案例中显示的图像原始宽和高为(200×200),缩放后修改为(100×100),编译并运行,如图336所示,代码如下: 



//chapter3/QtImageDemo/widget.cpp

void Widget::on_btnScale_clicked(){

QImage* imgScaled = new QImage;

*imgScaled = m_img->scaled(100,100, Qt::KeepAspectRatio);

ui->lblScale->setPixmap(QPixmap::fromImage(*imgScaled));

}






图336Qt缩放图像


3.4.10Qt旋转图像

Qt旋转图像可以用QMatrix类的rotate函数实现,代码如下: 



//chapter3/QtImageDemo/widget.cpp

void Widget::on_btnRotate_clicked(){

QImage* imgRotate = new QImage;

QMatrix matrix;

matrix.rotate(270);

*imgRotate = m_img->transformed(matrix);

ui->lblRotate->setPixmap(QPixmap::fromImage(*imgRotate));

}




编译并运行该项目,依次单击“显示图像”“缩放”和“旋转”按钮,效果如图337所示。



图337Qt显示、缩放和旋转图像