前言
最近入手了一款非常便宜的usb摄像头(买回来感觉画质很低,没有描述的4k,不过也够用于学习了),想着利用它来开启流媒体相关技术的学习。第一步便是打开摄像头,从而才能够对它进行一系列后续操作,诸如实时编解码,推流摄像头采集的数据等等。
本篇文章记录了如何通过QT、libvlc、ffmpeg三种方式来调用usb摄像头,顺带讲述了如何播放http网络视频流。以及如何搭建一个srs流媒体服务器。如代码有逻辑错误或者可优化,欢迎大家指正!希望和大家共同进步。
VS2022 + QT 调用usb摄像头
vs2022如果还没配置QT可参考博客: https://blog.csdn.net/H0893_888/article/details/129772600
第一步: vs2022新建一个Qt Widgets Application
第二步: 填写项目名称和项目位置,大家随意
第三步: qt向导中进行如下勾选(这里没选全,后续也可再更改)
主要代码
void Qt_Camera::openCamera(void)
{
//获取可用摄像头列表信息
QList<QCameraInfo> cameras = QCameraInfo::availableCameras();
for(auto camera: cameras)
{
qDebug() << camera;
}
/* 创建摄像头对象
* QCamera *camera = new QCamera;
* 这里使用QCameraInfo::availableCameras().at(1)是因为0为电脑自带的摄像头,1为usb摄像头
*/
QCamera *camera = new QCamera(QCameraInfo::availableCameras().at(1));
// 创建摄像头取景器对象
QCameraViewfinder *finder = new QCameraViewfinder;
// 设置取景器为摄像头的视图
camera->setViewfinder(finder);
camera->setCaptureMode(QCamera::CaptureVideo);
//创建图像捕获对象
QCameraImageCapture *imageCapture = new QCameraImageCapture(camera);
//设置图片保存路径
imageCapture->setCaptureDestination(QCameraImageCapture::CaptureToFile);
// 创建布局管理器
QVBoxLayout *layout = new QVBoxLayout;
layout->addWidget(finder);
// 创建拍照按钮
QPushButton *captureButton = new QPushButton("Capture");
layout->addWidget(captureButton);
//信号与槽连接
connect(imageCapture, &QCameraImageCapture::imageCaptured, this, &MainWindow::takePhoto);
// 连接拍照按钮的点击事件到图像捕获对象的拍照槽函数
connect(captureButton, &QPushButton::clicked, [=](){
imageCapture->capture();
});
// 创建主窗口
QWidget *window = new QWidget;
window->setLayout(layout);
window->show();
// 启动摄像头
camera->start();
}
/* 这里的 id 和 image 参数 由 QCameraImageCapture::imageCaptured 这个信号传递过来 */
void Qt_Camera::takePhoto(int id, const QImage &image)
{
//使用系统时间来命名图片的名称,时间是唯一的,图片名也是唯一的
QDateTime dateTime(QDateTime::currentDateTime());
QString time = dateTime.toString("yyyy-MM-dd-hh-mm-ss");
//创建图片保存路径名
QString filename = QString("D:/%1.jpg").arg(time);
//保存一帧数据
image.save(filename);
//提示获取到了一帧数据
QMessageBox::information(this, "提示:", "获取到一帧图片");
}
效果图
VS2022 + libvlc 调用usb摄像头/网络视频/本地视频
libvlc官网下载: https://download.videolan.org/pub/videolan/vlc/ (32/64位自选)
第一步: 创建空项目
第二步: 填写项目名称/路径,大家随意
第三步: 新建一个main.cpp
第四步: 拷贝libvlc至项目路径下
第五步: 拷贝libvlc下的所有内容至项目路径下(plugins、libvlc.dll、libvlccore.dll)
注意: 如果忘记拷贝 plugins 文件夹,无法通过 libvlc_new 得到 libvlc_instance_t* 类型的一个实例。
第六步: 获取usb摄像头名称,libvlc中需要先获得usb摄像头的名称
- 1 打开电脑中计算机管理
- 2 点击设备管理器
- 3 点击照相机,下方即为电脑摄像头设备名称
main.cpp代码(这里使用C++代码,相对清爽一些)
#include <iostream>
#include "vlc/vlc.h"
using namespace std;
void setMedia(const char* url)
{
//判断url是否为空
if (!url)
{
cout << "url is nullptr!" << endl;
return;
}
libvlc_instance_t* vlcInstance = libvlc_new(0, nullptr);
//判断vlc实例是否为空
if (!vlcInstance)
{
cout << "vlcInstance is nullptr!" << endl;
return;
}
//创建媒体
//libvlc_media_t *vlcMedia= libvlc_media_new_path(vlcInstance,url); //播放本地视频文件
//libvlc_media_t* vlcMedia = libvlc_media_new_location(vlcInstance, "http://sovv.qianpailive.com/a455359ee41b684baaf0e4a094ee34c4/1714302042/b740/20240427/e5eba6250b87047662500bd5a3356465?a=ylu7kvm4yc47&t=1714302042023730569&u=el2KNaf9FEzbwLuS6FKdUaqObdUwp2i5Q7qqeWI3&vid=Vjkz13DanVMG"); //播放网络视频文件
libvlc_media_t* vlcMedia = libvlc_media_new_location(vlcInstance, "dshow://DV20 USB CAMERA"); //打开usb摄像头
if (!vlcMedia)
{
cout << "vlcMedia is nullptr!" << endl;
return;
}
//创建媒体播放器
libvlc_media_player_t* vlcMediaPlayer = libvlc_media_player_new_from_media(vlcMedia);
if (!vlcMediaPlayer)
{
cout << "vlcMediaPlayer is nullptr!" << endl;
return;
}
libvlc_media_player_play(vlcMediaPlayer);
getchar();
//释放媒体播放器
if (vlcMediaPlayer)
{
libvlc_media_player_release(vlcMediaPlayer);
}
//释放媒体
if (vlcMedia)
{
libvlc_media_release(vlcMedia);
}
//释放vlc实例
if (vlcInstance)
{
libvlc_release(vlcInstance);
}
}
int main(void)
{
setMedia("D:\\3.flv"); //传递播放媒体url
cout << "hello vlc" << endl;
getchar();
return 0;
}
效果图:
-
本地视频
-
网络视频
-
摄像头采集一帧画面(感觉不是很清晰)
VS2022 + ffmpeg + sdl 摄像头采集数据
不了解 ffmpeg 和 sdl 可参考我的上一篇博客: https://blog.csdn.net/m0_73431159/article/details/138195924
由于上一篇已经讲过 ffmpeg + sdl 在 vs2022 中的环境配置,这里就不再赘述了。
ffmpeg可以直接用命令行查看摄像头设备名
可先使用chcp 65001 临时改变 cmd 窗口编码格式为 utf-8
ffmpeg -list_devices true -f dshow -i dummy
代码
#pragma warning(disable:4996)
#include <stdio.h>
extern "C" // ffmpeg有C语言编写,这里为cpp文件
{
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
#include "libswscale/swscale.h"
#include "libavdevice/avdevice.h"
#include "SDL/SDL.h"
#include "SDL/SDL_thread.h"
}
#undef main // sdl中已有main的相关定义
#define REFRESH_EVENT (SDL_USEREVENT + 1) // 自定义事件
int thread_exit = 0; //线程循环状态变量
int refresh_thread(void* opaque)
{
while (thread_exit == 0) {
SDL_Event event;
event.type = REFRESH_EVENT; // 将事件类型定为REFRESH_EVENT
SDL_PushEvent(&event); // 送入事件队列中
SDL_Delay(40); // 如果是打开摄像头,可注释该行。如果是播放视频,该行表示每秒25帧
}
return 0;
}**加粗样式**
int main(void)
{
//-----------ffmpeg------------
AVFormatContext* pFormatContext = NULL; //封装格式上下文
AVDictionary* opts = NULL; // 字典
const char* url = "http://sovv.qianpailive.com/6ddf66db8dc4449123e9f93259db899b/1714201703/cf05/20231002/c36e0216c67cdfb009dcd1467ac4075f?a=ylu7kvm4yc47&t=1714201703527245094&u=lEqwQq9pxNSGX4uxOLgaVBmDh9FrvJwoKTdAX6t2&vid=8jLl7AGQLxjA"; //网络视频地址
int videoIndex = -1; //视频流索引号
int i = 0; // 循环变量
AVCodecContext* pCodecContext = NULL; // 编解码上下文
AVCodec* pCodec = NULL; // 编解码器
AVPacket* packet = NULL; // 解码前一帧视频数据
AVFrame* pFrame = NULL; // 解码后一帧数据
AVFrame* pFrameYUV = NULL; // 裁剪后一帧数据
struct SwsContext* img_convert_ctx = NULL; //像素格式转换等
//-------------SDL-------------
SDL_Surface* screen; //画布
int screen_w = 0, screen_h = 0;
SDL_Overlay* bmp; //涂层叠层
SDL_Rect rect; //绘图区域
int ret, got_picture;
SDL_Thread* video_tid; //线程
SDL_Event event; //事件
av_register_all(); // 注册所有组件
avformat_network_init(); //网络初始化
avdevice_register_all(); //注册所有设备
#if 0 //如果想要播放网络流视频,将其置 1j
/* 加上一些限制条件 */
av_dict_set(&opts, "http_transport", "tcp", 0);
av_dict_set(&opts, "max_delay", "500", 0);
pFormatContext = avformat_alloc_context();
/* 打开网络视频 */
if (avformat_open_input(&pFormatContext, url, NULL, &opts) != 0)
{
printf("Can't open input !\n");
return -1;
}
#else
pFormatContext = avformat_alloc_context();
/* DShow是DirectShow的简称,它是一种用于Windows平台的多媒体框架 */
pFormatContext->iformat = av_find_input_format("dshow");
// pFormatContext->iformat = av_find_input_format("vfwcap");
/* 打开摄像头作为输入 */
if (avformat_open_input(&pFormatContext, "video=DV20 USB CAMERA", pFormatContext->iformat, NULL) != 0)
{
printf("Can't open input !\n");
return -1;
}
#endif
/* 获取流信息 */
if (avformat_find_stream_info(pFormatContext, NULL) < 0)
{
printf("Couldn't find stream information.\n");
return -1;
}
/* 寻找视频流 */
for (int i = 0; i < pFormatContext->nb_streams; i++)
{
if (pFormatContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
{
videoIndex = i;
break;
}
}
/* 判断是否找到视频流 */
if (videoIndex == -1)
{
printf("Can't find video stream !\n");
return -1;
}
pCodecContext = pFormatContext->streams[videoIndex]->codec; //获取解码器上下文
pCodec = avcodec_find_decoder(pCodecContext->codec_id); //获取解码器
/* 判断是否成功获取解码器 */
if (pCodec == NULL)
{
printf("Can't find decoder !\n");
return -1;
}
/* 打开解码器是否成功 */
if (avcodec_open2(pCodecContext, pCodec, NULL) < 0)
{
printf("Can't open decoder !\n");
return -1;
}
/* 分配空间 */
packet = (AVPacket*)av_malloc(sizeof(AVPacket));
pFrame = avcodec_alloc_frame();
pFrameYUV = avcodec_alloc_frame();
/* 初始化SDL子系统 */
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
printf("Could not initialize SDL - %s\n", SDL_GetError());
return -1;
}
/* 获取宽、高 */
screen_w = pCodecContext->width;
screen_h = pCodecContext->height;
/*
* SDL_SetVideoMode是一个用于设置SDL窗口的宽、高度、位深度和标志的函数。
* @return 返回一个指向SDL_Surface结构的针,该结构表示窗口的表面。
*/
screen = SDL_SetVideoMode(screen_w, screen_h, 0, 0);
if (!screen) {
printf("SDL: could not set video mode - exiting:%s\n", SDL_GetError());
return -1;
}
/*
* SDL_CreateYUVOverlay函数: SDL库中用于创建YUV覆盖的函数。YUV覆盖是一种用于在屏幕上显示视频的数据结构。
* SDL_YV12_OVERLAY是SDL库中的一个宏定义,用于创建一个YV12格式的图像叠加层(overlay)。
* YV12是一种颜色编码格式,常用于视频图像的处理和显示。
* YV12格式将图像的亮度(Y)和色度(UV)分开存储,可以有效地压缩图像数据并提高图像质量。
*/
bmp = SDL_CreateYUVOverlay(pCodecContext->width, pCodecContext->height, SDL_YV12_OVERLAY, screen);
rect.x = 0;
rect.y = 0;
rect.w = screen_w;
rect.h = screen_h;
/* 图片格式转换 */
img_convert_ctx = sws_getContext(pCodecContext->width, pCodecContext->height, pCodecContext->pix_fmt, pCodecContext->width, pCodecContext->height, PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
/* 创建一个线程 */
video_tid = SDL_CreateThread(refresh_thread, NULL);
/* SDL_WM_SetCaption函数是SDL库中的一个函数,用于设置显示窗口的标题和图标 */
SDL_WM_SetCaption("MyCameraTest", NULL);
while(1)
{
//等待事件到来
SDL_WaitEvent(&event);
/* 判断事件类型是否为REFRESH_EVENT,是则读帧 */
if (event.type == REFRESH_EVENT)
{
//------------------------------
if (av_read_frame(pFormatContext, packet) >= 0)
{
//判断是否为视频帧
if (packet->stream_index == videoIndex)
{
//解码packet
ret = avcodec_decode_video2(pCodecContext, pFrame, &got_picture, packet);
if (ret < 0)
{
printf("Decode Error.\n");
return -1;
}
//判断是否获得一帧画面
if (got_picture)
{
/* SDL_LockYUVOverlay函数用于锁定YUV覆盖层,以便在其上进行像素操作。 */
SDL_LockYUVOverlay(bmp);
pFrameYUV->data[0] = bmp->pixels[0];
pFrameYUV->data[1] = bmp->pixels[2];
pFrameYUV->data[2] = bmp->pixels[1];
pFrameYUV->linesize[0] = bmp->pitches[0];
pFrameYUV->linesize[1] = bmp->pitches[2];
pFrameYUV->linesize[2] = bmp->pitches[1];
/*
* sws_scale是一个图像缩放函数,用于将输入图像缩放到指定的输出尺寸。
* c:SwsContext结构体指针,包含了图像缩放的相关参数。
* srcSlice:输入图像数据的指针数组,每个指针指向一个输入图像平面。
* srcStride:输入图像数据的步长数组,每个步长对应一个输入图像平面。
* srcSliceY:输入图像的起始行数。
* srcSliceH:输入图像的高度。
* dst:输出图像数据的指针数组,每个指针指向一个输出图像平面。
* dstStride:输出图像数据的步长数组,每个步长对应一个输出图像平面。
*/
sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data, pFrame->linesize, 0, pCodecContext->height, pFrameYUV->data, pFrameYUV->linesize);
/*
* SDL_UnlockYUVOverlay函数: SDL库中用于解锁YUV叠加层的函数。
* 它的作用是解锁一个已经锁定的YUV叠加层,以便可以对其进行修改或者渲染操作。
* @param overlay 是一个指向SDL_Overlay结构体的指针,它表示要解锁的YUV叠加层。
*/
SDL_UnlockYUVOverlay(bmp);
/*
* SDL_DisplayYUVOverlay函数: SDL库中用于显示YUV覆盖层的函数。
* @param overlay:指向SDL_Overlay结构的指针,表示要显示的YUV覆盖层。
* @param dstrect:指向SDL_Rect结构的指针,表示覆盖层在屏幕上的位置和大小。
*/
SDL_DisplayYUVOverlay(bmp, &rect);
}
}
//free a packet
av_free_packet(packet);
}
else
{
//Exit Thread
thread_exit = 1;
break;
}
}
// 事件类型为退出事件则退出循环
else if (event.type == SDL_QUIT)
{
thread_exit = 1;
break;
}
}
/* 关闭释放相关资源 */
sws_freeContext(img_convert_ctx);
SDL_Quit();
av_free(pFrameYUV);
avcodec_close(pCodecContext);
avformat_close_input(&pFormatContext);
return 0;
}
效果图:
搭建srs流媒体服务器
srs官⽹:https://github.com/ossrs/srs
码云的源速度快:https://gitee.com/winlinvip/srs.oschina.git
github的源速度慢:https://github.com/ossrs/srs.git
第⼀步,获取SRS
git clone https://gitee.com/winlinvip/srs.oschina.git
cd srs.oschina
git checkout -b 5.0 remotes/origin/5.0release
cd trunk
第二步,编译SRS
./configure && make
第三步,编写SRS配置⽂件
将以下内容保存为⽂件,譬如 conf/rtmp.conf ,服务器启动时指定该配置⽂件(srs的conf⽂件夹有该⽂件)。
# conf/rtmp.conf
listen 1935;
max_connections 1000;
vhost __defaultVhost__ {
}
第四步,启动srs
./objs/srs -c conf/rtmp.conf
第五步,启动推流编码器
视频推流
ffmpeg -re -i 3.flv -vcodec copy -acodec copy -f flv -y rtmp://192.168.72.128/live/livestream
摄像头推流
ffmpeg -f dshow -i video="Integrated Camera" -c:v libx264 -preset ultrafast -tune zerolatency -f flv rtmp://192.168.72.128/live/livestream
这里需要根据每个人的实际情况进行填写,如: 3.flv 需替换成您电脑的视频地址
rtmp后面的地址也得改成实际的服务器地址
第六步,观看RTMP流。
- 可以使用ffplay rtmp://192.168.72.128/live/livestream
- 可以使用vlc播放器进行拉流播放
注意:如果是在虚拟机上进行测试,记得关闭虚拟机防火墙
第五步,启动推流编码器
视频推流
ffmpeg -re -i 3.flv -vcodec copy -acodec copy -f flv -y rtmp://192.168.72.128/live/livestream
摄像头推流
ffmpeg -f dshow -i video="Integrated Camera" -c:v libx264 -preset ultrafast -tune zerolatency -f flv rtmp://192.168.72.128/live/livestream
这里需要根据每个人的实际情况进行填写,如: 3.flv 需替换成您电脑的视频地址
rtmp后面的地址也得改成实际的服务器地址
第六步,观看RTMP流。
- 可以使用ffplay rtmp://192.168.72.128/live/livestream
- 可以使用vlc播放器进行拉流播放
注意:如果是在虚拟机上进行测试,记得关闭虚拟机防火墙
systemctl stop firewalld