目录
1.设计思路
2.Pro文件配置
3.头文件引入
4.界面设计
5.初始化设备函数
6.发起视频链接函数
7.初始化定时器模块函数
8.TCP链接模块函数
9.处理接收的数据线程函数
10.实现功能展示
设计思路
基于TCP协议的视频传输Demo,设计要实现的功能主要是TCP传输还有视频,可参考以下开发思路:
1.视频本质都是通过照片组成,由于连续的照片数量多了,画面就由单纯的静态图片变成动态的视频,所以要实现视频传输,实现得把摄像头录制到的画面分成帧使用TCP协议发送,具体开发细节如下:
1.设置QTimer类对象,实现定时器功能,定期捕获摄像头录制的画面
2.为了实现捕获画面功能,需要定义QImageCapture对象,并使用QMediaCaptureSession类对象统一管理摄像头和QImageCapture对象
2.TCP协议的链接,主要通过三次握手形成可靠的链接。所以我们需要一个QTcpSocket对象来进行链接,并对该对象发出的readyRead信号(读取到数据)进行相对应的槽函数绑定
3.由于要同时录制视频,还有捕获视频画面发送至其他用户,所以要使用线程,单独使用线程往往管理不是很方便,所以Demo中使用了QThradPool类(线程池)来管理线程,并使用QThreadPool::globalInstance()->start()函数将线程放置全局线程池统一管理启动
4.有时候不仅仅满足于视频传输的需求(现实生活中或许只有倒车视频,监控等功能不需要实现音频录制),所以该Demo使用了QAudioDevice访问音频设备,并录制
Pro文件配置
为了实现基于TCP协议的视频传输Demo,我们需要Qt中自带的两个模块,分别为multimedia 模块和multimediawidgets模块:
1.multimedia模块:该模块提供了音频和视频播放、录制以及处理的基本功能。支持多种音频和视频格式,能实现流媒体播放等功能
2.multimediawidgets模块:该模块提供了一组预构建的多媒体小部件,使得开发者可以快速地在应用程序中添加多媒体播放和控制功能
所以开发前Pro文件中应该添加以下字段
QT += multimedia multimediawidgets
头文件引入
以下为项目main文件中需要引入的头文件,我会把为什么要引入该头文件列在备注中,以供参考:
头文件 | 备注 |
QDir | 用于获取操作文件系统目录 |
QFile | 用于文件操作的类 |
QTimer | 用于实现定时器功能 |
QCamera | 用于访问和控制摄像头设备 |
QHostInfo | 用于获取主机信息 |
QTcpSocket | 用于实现TCP套接字的类 |
QTcpServer | 用于实现TCP服务器端的类 |
QMessageBox | 用于显示消息框,向用户显示信息、警告或错误 |
QAudioInput | 用于音频输入设备的类 |
QThreadPool | 用于管理线程池 |
QAudioDevice | 用于访问和控制音频设备 |
QMediaDevices | 提供对媒体设备信息的访问 |
QCameraDevice | 用于访问和控制相机设备 |
QImageCapture | 用于捕获摄像头录制的图片 |
QMediaRecorder | 用于录制媒体内容 |
QMediaCaptureSession | 管理媒体捕获会话的类 |
表1.头文件列表
界面设计
设计思想是为了实现多个用户同时视频的功能,但是没实现服务端,于是把服务端使用的QTcpServer转到客户端实现,写Demo时本人将更多考虑的是提供更强的可扩展性
初始化设备函数
以下函数为初始化设备的函数,针对Ui文件中控件名称的不同可以修改对应的指针,完成效果
/* 初始化MainWindow界面信息
* device:当前摄像头
* 返回值:void
*/
void MainWindow::init_MainWindowInfo()
{
session = new QMediaCaptureSession(this);
session->setVideoOutput(ui->MyvideoPreview); //设置视频输出控件
QAudioInput* audioInput = new QAudioInput(this);
audioInput->setDevice(QMediaDevices::defaultAudioInput()); //获取默认的音频输入设备
session->setAudioInput(audioInput); //设置音频输入设备
QCameraDevice defaultCameraDevice = QMediaDevices::defaultVideoInput();//获取当前摄像头
if(defaultCameraDevice.isNull()){
QMessageBox::critical(this,"提示","没有发现摄像头");
return;
}
init_CameraDevice(defaultCameraDevice);
}
/* 初始化摄像头
* device:当前摄像头
* 返回值:void
*/
void MainWindow::init_CameraDevice(const QCameraDevice& device)
{
QByteArray defaultCameraID = device.id();
int index=0;
for(int i=0; i<QMediaDevices::videoInputs().size(); i++){
QCameraDevice device =QMediaDevices::videoInputs().at(i);
if (device.id() == defaultCameraID){
ui->comboCam_List->addItem(device.description()+"(默认)",QVariant::fromValue(device));
index=i;
}else{
ui->comboCam_List->addItem(device.description(),QVariant::fromValue(device));
}
}
if (ui->comboCam_List->currentIndex() != index){
ui->comboCam_List->setCurrentIndex(index);
}
camera_Obj = new QCamera(this); //初始化摄像头对象
camera_Obj->setCameraDevice(device);
session->setCamera(camera_Obj); //为session设置摄像头
capture_Obj = new QImageCapture(this);//创建图像捕获对象
session->setImageCapture(capture_Obj);//设置画面捕获对象
//捕获到画面时发送画面
connect(capture_Obj, &QImageCapture::imageCaptured, this, &MainWindow::handle_SendingImg);
//设置摄像头信息
connect(ui->comboCam_List,&QComboBox::currentIndexChanged,this, &MainWindow::handle_ShowCameraDevice);
handle_ShowCameraDevice(ui->comboCam_List->currentIndex());
}
/* 初始化摄像头设备信息
* device:当前摄像头
* 返回值:void
*/
void MainWindow::init_CameraDeviceInfo(const QCameraDevice& device)
{
ui->comboCam_VideoRes->clear(); //支持的视频分辨率
ui->comboCam_FrameRate->clear(); //支持的帧率范围
foreach(QCameraFormat format, device.videoFormats()){
QSize size=format.resolution();
QString str=QString::asprintf("%d X %d",size.width(),size.height());
ui->comboCam_VideoRes->addItem(str);
str=QString::asprintf("%.1f ~ %.1f",format.minFrameRate(),format.maxFrameRate());
ui->comboCam_FrameRate->addItem(str);
}
}
/* 显示摄像头信息
* index:项下标
* 返回值:void
*/
void MainWindow::handle_ShowCameraDevice(int index)
{
QVariant var=ui->comboCam_List->itemData(index);
QCameraDevice device = var.value<QCameraDevice>();
init_CameraDeviceInfo(device); //显示摄像头设备信息
camera_Obj->setCameraDevice(device); //重新设置摄像头设备
}
发起视频链接函数
//发起视频链接
void MainWindow::on_Camclose_Btn_2_clicked()
{
QString ip = ui->lineEdit->text();
QString port = ui->lineEdit_2->text();
if(port.isEmpty() || ip.isEmpty()){
QMessageBox::information(nullptr,"提示","未输入目标端口或IPv4地址",QMessageBox::Ok);
}
if(tcpSocket != nullptr){
tcpSocket->deleteLater();
}
if(video_ThreadObj != nullptr){
video_ThreadObj->handle_CloseThread();
}
tcpSocket = new QTcpSocket(this);
qDebug() << "发起连接,目标地址:" << ip << "目标端口" << port.toUShort();
//连接成功
connect(tcpSocket,&QAbstractSocket::connected,this,&MainWindow::handle_Connect);
//显示接收的图片
connect(tcpSocket,&QTcpSocket::readyRead,this,&MainWindow::hanlde_RelayinThread);
tcpSocket->connectToHost(ip,port.toUShort()); //链接服务器
video_ThreadObj = new VideoThread(ui->show_Img);
}
void MainWindow::handle_Connect()
{
qDebug() << "连接成功";
init_TimerModel();
}
初始化定时器模块函数
//初始化定时器
void MainWindow::init_TimerModel()
{
timer_Obj = new QTimer;
timer_Obj->start(100);
//定时截取摄像头拍摄的图片
connect(timer_Obj,&QTimer::timeout,this,&MainWindow::handle_InterceptImg);
}
//定时截取摄像头图片
void MainWindow::handle_InterceptImg()
{
QDir().mkdir("images");
QString path = QDir::currentPath() + "/images/image.jpg"; //捕获图像的保存路径
qDebug() << "定时截取摄像头图片" << path;
capture_Obj->captureToFile(path);
}
//发送截取的图片
void MainWindow::handle_SendingImg(int id, const QImage &image)
{
QDir().mkdir("images");
QString path = QDir::currentPath() + "/images/image.jpg"; //捕获图像的保存路径
if (!image.isNull()) { //截取到图片
image.save(path,"jpg",30); //保存图像并压缩,质量为30
QFile file(path);
file.open(QIODevice::ReadOnly);
tcpSocket->write(file.readAll()); //发送图片
tcpSocket->flush(); //确保消息被发送
file.close();
qDebug() << "发送图片";
}
}
TCP链接模块函数
//初始化Tcp模块
void MainWindow::init_TcpModule()
{
tcpSocket = new QTcpSocket;
tcpServer = new QTcpServer(this);
connect(tcpServer,&QTcpServer::newConnection,this,&MainWindow::init_TcpConnection);
get_LocalIPv4(); //获取IPv4地址
QString port = ui->lineEdit_3->text();//监听端口
qDebug() << "监听端口" << port.toUShort();
tcpServer->listen(QHostAddress::Any,port.toUShort()); //开始监听
}
//初始化Tcp链接
void MainWindow::init_TcpConnection()
{
qDebug() << "获取到连接";
//获取第一个链接的客户端
tcpSocket = tcpServer->nextPendingConnection();
video_ThreadObj = new VideoThread(ui->show_Img);
//显示接收的图片
connect(tcpSocket,&QAbstractSocket::readyRead,this,&MainWindow::hanlde_RelayinThread);
//连接成功
connect(tcpSocket,&QAbstractSocket::connected,this,&MainWindow::handle_Connect);
QThreadPool::globalInstance()->start(video_ThreadObj);//启动线程
}
//转发至线程处理读取到的数据
void MainWindow::hanlde_RelayinThread()
{
qDebug() << "处理接收的函数";
video_ThreadObj->handle_ShowReceiveImg(tcpSocket->readAll());
}
// 获取本机IPV4地址
int MainWindow::get_LocalIPv4()
{
QString hostName = QHostInfo::localHostName(); //获取主机设备名
QHostInfo hostInfo = QHostInfo::fromName(hostName);//获取主机信息
QList<QHostAddress> addList = hostInfo.addresses();//获取主机地址列表
if(addList.isEmpty()){ //未获取到地址
return 0;
}
else{
foreach(const QHostAddress& item,addList){
if(item.protocol() == QAbstractSocket::IPv4Protocol){
ui->udpIpv4_Box->addItem(item.toString());
}
}
return 1;
}
return -1;
}
处理接收的数据线程函数
VideoThread::VideoThread(QObject *parent)
: QRunnable{}
, QObject{parent}
{
setAutoDelete(true); //设置线程自动析构
}
VideoThread::VideoThread(QLabel *widget, QObject *parent)
: QRunnable{}
, QObject{parent}
, video_Label{widget}
{
ok = true; //初始化线程状态
setAutoDelete(true); //设置线程自动析构
}
void VideoThread::run()
{
while(ok);
}
//显示接收的图像
void VideoThread::handle_ShowReceiveImg(QByteArray data)
{
QPixmap pic = QPixmap::fromImage(QImage::fromData(data));
video_Label->setPixmap(pic.scaled(video_Label->size(),Qt::KeepAspectRatio,Qt::SmoothTransformation));
}
//关闭线程
void VideoThread::handle_CloseThread()
{
if(ok){
ok = false;
}
}
实现功能展示
PS:由于本机只有一个摄像头,就不能开另一边应用的摄像头了,后续会把Demo的github地址发布在评论区。该Demo还需要更多的完善才能真正的实现一个视频通话功能,后续会更新优化后的Demo