来总结一下最近研究的全景视频播放器代码
平台:Windows
软件:vs2019
代码来源:OpenGL全景视频.
刚开始的时候想先从代码入手,和想象的不太一样,本来以为C语言的代码撑死每句指令都百度,打断点看变量应该也能看懂。于是先在b站找到了C++编写视频播放器的视频,看的我一头雾水,里面用了vlc的库,代码可以说是一句不懂。之后在csdn,博客园搜了好多,发现全景视频播放器好多都是基于安卓开发的,里面许多名词也都不懂。
大作业+期末的日子结束,回家后美赛练习过程中才又重新开始找资料看,看过一些音视频入门的文章后才知道音视频开发,进行的是采集、渲染、处理、传输等一系列的开发和应用,这时候才有大致框架,有了解封装,解码,音频流,视频流等相关概念。和做大作业的时候一样,习惯了直接上手,有什么问题再说,但其实首先做的应该是弄清楚大的框架和要求,再着眼细节。
再之后就是知道了FFmpeg和OpenGL,抛下代码先学了相关的理论。
FFmpeg参考了雷神的博客和在b站的视频讲解
把库和函数列出来了:FFmpeg原理介绍与代码实现.
后来发现了一个写的比较系统的教程:ffmpeg和SDL教程.
OpenGL有一个很系统的讲解:LearnOpenGL CN.
万字长文详解如何用 Python 玩转 OpenGL | CSDN 博文精选.坐标系、投影、变换的概念都有
另外有一些找到的写的不错的文章:
最简单的视音频播放示例5:OpenGL播放RGB/YUV.有OpenGL渲染管线的步骤,窗口相关函数都有提到。
OpenGL正面剔除,深度测试,混合.有实际图片示例
还有《OpenGL编程指南》好像也多人推荐
但我的感觉是代码里好多东西这书和网站都没有讲到,像glut库里函数都是分散的百度出来的,很杂很散的样子。
除了这些,零零碎碎搜到的一些东西也大致记录了一下。
FFmpeg新旧接口对照使用一览.
sws_getContext():初始化一个SwsContext。
sws_scale():处理图像数据。
sws_freeContext():释放一个SwsContext。
关于avpicture_fill与sws_scale:
关于avpicture_fill与sws_scale.对应于FFmpeg解码之后像素格式变换的代码
FFmpeg解码H264及swscale缩放详解.函数解释很细
总结到了另一篇:全景视频播放器中OpenGL的相关记录.
总结到了:列队与线程(全景视频播放器).
主要的思路是将全景视频利用FFmpeg解封装解码后,将视频帧利用OpenGL渲染显示在一个球上。
值得注意的点:
(1)FFmpeg解码后还进行了像素帧的格式转换,好像OpenGL只能渲染RGB,yuv要转成rgb?
(2)线程及列队的使用
程序中使用两个线程分别实现解码和渲染,它们之间相互独立。但解码和渲染的速度我们无法控制,就通过列队实现均衡。我们自己设定列队的size,手动将解码出的数据存到列队中。解码出的视频帧保存在列队中,即向列队中输入使size增加,而OpenGL的渲染消耗列队中的元素,使size减小。当解码的数据将列队容量占满时,解码线程会稍作等待,等渲染的线程继续执行使列队中有剩余位置时,解码线程才会继续运行。
另外,临界区就是一段不会被中断的代码,可以避免数据冲突而使程序崩溃的情况。在本程序中,列队中元素增加和减少是都会在临界区进行操作。OpenGL用解码出的图像生成纹理后就已经消耗掉了列队中的元素,就可以离开临界区了,之后计算各点坐标将纹理对应成像素点并进行绘制。
(3)OpenGL绘制球体
通过设置经线和纬线的数量,我们可将一个球分成数个长方形,OpenGL基本的绘制单元是三角形,一个矩形分两个三角形,即6个顶点。每个顶点有xyz三维的空间坐标和(s, t)二维纹理坐标。空间坐标就是数学上的坐标表示,而纹理坐标是根据
[0,1]分份数算出来的。
(4)关于glut中的回调函数,以对该事件或条件进行响应
(5)两个线程打断点调试不能反映真实的程序运行情况,断点打下这个线程不动了另一个跑,但实际是两个线程都在跑。(或者确实可以真实反映程序的调试俺不知道)
具体代码及注释如下:
// glPanorama.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#define PI 3.1415926
GLfloat xangle = 0.0; //X 旋转量,之后可通过鼠标或键盘的控制改变
GLfloat yangle = 0.0; //Y 旋转量
GLfloat zangle = 0.0; //Z 旋转量
//交叉点的坐标
int cx = 0;
int cy = 0;
GLfloat distance =0;//0或1100.0;
GLuint texturesArr;
int cap_H = 1;//必须大于0,且cap_H应等于cap_W
int cap_W = 1;//绘制球体时,每次增加的角度
float* verticals;
float* UV_TEX_VERTEX;
void init(void);
void reshape(int w, int h);
void display(void);
void getPointMatrix(GLfloat radius);
#define MAXSIZE 10//列队的最大容量
//定义了一个结构体Frame用于保存一帧视频画面、音频
typedef struct Vid_Frame {
AVFrame *frame;//视频或音频的解码数据
int serial;
double pts; /* presentation timestamp for the frame */
double duration; /* estimated duration of the frame */
int64_t pos; /* byte position of the frame in the input file */
uint8_t *buffer;
int width;
int height;
AVRational sar;
} Vid_Frame;
//FrameQueue不是用链表实现队列,而是用数组实现队列(环形缓冲区)。
typedef struct FrameQueue{
Vid_Frame queue[MAXSIZE];队列元素,用数组模拟队列,其中就有AVFrame的解码数据
int front;
int rear;//后
int size;//当前存储的节点个数(或者说,当前已写入的节点个数)
CRITICAL_SECTION cs;//critica_section定义一个临界区对象cs,它是全局变量
}FrameQueue;
FrameQueue frame_queue;
//frame_queue是一个循环队列,解码的时候入队,渲染的时候出队
void initQueue(FrameQueue *q)//初始化列队
{
int i;
for (i = 0; i<MAXSIZE; i++){
if (!(q->queue[i].frame = av_frame_alloc()))//为数组queue中的每个元素的frame(AVFrame*)的字段分配内存
return ;
q->queue[i].buffer = NULL;
}
q->front = 0;
q->rear = 0;
q->size = 0;
InitializeCriticalSection(&q->cs);//初始化临界区,创立了一个叫cs的临界区对象
}
void deQueue(FrameQueue *q)
{
free(q);
}
void init(void)
{
initQueue(&frame_queue);//初始化列队
//创建纹理,输入生成纹理的数量1,然后把它们储存在第二个参数的unsigned int数组中
glGenTextures(1, &texturesArr);
glBindTexture(GL_TEXTURE_2D, texturesArr);//绑定它,让之后任何的纹理指令都可以配置当前绑定的纹理
//IplImage *image = cvLoadImage("5.png", 1);
//生成一个纹理
//glTexImage2D(GL_TEXTURE_2D, 0, 3, image->width, image->height, 0, GL_BGR_EXT, GL_UNSIGNED_BYTE, image->imageData);
//我们需要自己告诉OpenGL在纹理中采取哪种采样方式
//纹理被放大和缩小时都使用了线性过滤
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); //线形滤波
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); //线形滤波
glClearColor(0.0, 0.0, 0.0, 0.0);//设置当前使用的清除颜色值,这里为黑色,参数为RGBa
glClearDepth(1);// 清除深度缓存 1.0是最大深度([0.0,1.0])
glShadeModel(GL_SMOOTH);//设定opengl中绘制指定两点间其他点颜色的过渡模式,启用栅格化
glEnable(GL_TEXTURE_2D);//允许采用 2D 纹理技术
glEnable(GL_DEPTH_TEST);//启用深度测试,决定何时覆盖一个像素
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);//表示颜色和纹理坐标插补的质量。 如果角度更正参数插值不有效地支持由 OpenGL 实现,提示 GL_DONT_CARE 或 GL_FASTEST 可以导致简单线性插值的颜色和/或纹理坐标。
getPointMatrix(500);//用函数得到顶点坐标和纹理坐标
}
void getPointMatrix(GLfloat radius)
{
//开辟空间用来储存顶点坐标和纹理坐标,顶点坐标每个顶点有3个维度,纹理坐标2个(一个矩形中按6个顶点)
verticals = new float[(180 / cap_H) * (360 / cap_W) * 6 * 3];
UV_TEX_VERTEX = new float[(180 / cap_H) * (360 / cap_W) * 6 * 2];
float x = 0;
float y = 0;
float z = 0;
int index = 0;
int index1 = 0;
float r = radius;//球体半径
double d = cap_H * PI / 180;//每次递增的弧度
for (int i = 0; i < 180; i += cap_H) {
double d1 = i * PI / 180;
for (int j = 0; j < 360; j += cap_W) {
double d2 = j * PI / 180;
//获得球体上切分的超小片矩形的顶点坐标(两个三角形组成,所以有六点顶点)
verticals[index++] = (float)(x + r * sin(d1 + d) * cos(d2 + d));
verticals[index++] = (float)(y + r * cos(d1 + d));
verticals[index++] = (float)(z + r * sin(d1 + d) * sin(d2 + d));
//获得球体上切分的超小片三角形的纹理坐标,纹理坐标范围是(0,1)
UV_TEX_VERTEX[index1++] = (j + cap_W) * 1.0f / 360;
UV_TEX_VERTEX[index1++] = (i + cap_H) * 1.0f / 180;
verticals[index++] = (float)(x + r * sin(d1) * cos(d2));
verticals[index++] = (float)(y + r * cos(d1));
verticals[index++] = (float)(z + r * sin(d1) * sin(d2));
UV_TEX_VERTEX[index1++] = j * 1.0f / 360;
UV_TEX_VERTEX[index1++] = i * 1.0f / 180;
verticals[index++] = (float)(x + r * sin(d1) * cos(d2 + d));
verticals[index++] = (float)(y + r * cos(d1));
verticals[index++] = (float)(z + r * sin(d1) * sin(d2 + d));
UV_TEX_VERTEX[index1++] = (j + cap_W) * 1.0f / 360;
UV_TEX_VERTEX[index1++] = i * 1.0f / 180;
verticals[index++] = (float)(x + r * sin(d1 + d) * cos(d2 + d));
verticals[index++] = (float)(y + r * cos(d1 + d));
verticals[index++] = (float)(z + r * sin(d1 + d) * sin(d2 + d));
UV_TEX_VERTEX[index1++] = (j + cap_W) * 1.0f / 360;
UV_TEX_VERTEX[index1++] = (i + cap_H) * 1.0f / 180;
verticals[index++] = (float)(x + r * sin(d1 + d) * cos(d2));
verticals[index++] = (float)(y + r * cos(d1 + d));
verticals[index++] = (float)(z + r * sin(d1 + d) * sin(d2));
UV_TEX_VERTEX[index1++] = j * 1.0f / 360;
UV_TEX_VERTEX[index1++] = (i + cap_H) * 1.0f / 180;
verticals[index++] = (float)(x + r * sin(d1) * cos(d2));
verticals[index++] = (float)(y + r * cos(d1));
verticals[index++] = (float)(z + r * sin(d1) * sin(d2));
UV_TEX_VERTEX[index1++] = j * 1.0f / 360;
UV_TEX_VERTEX[index1++] = i * 1.0f / 180;
}
}
}
void reshape(int w, int h)
{
glViewport(0, 0, (GLsizei)w, (GLsizei)h);
glMatrixMode(GL_PROJECTION);//接下来要做投影相关的操作
glLoadIdentity();//在进行变换前把当前矩阵设置为单位矩阵
//glOrtho(-250.0, 250, -250.0, 250, -500, 500);
//glFrustum(-250.0, 250, -250.0, 250, -5, -500);
gluPerspective(60, (GLfloat)w / h, 1.0f, 1000.0f); //设置投影矩阵
glMatrixMode(GL_MODELVIEW);//对模型视景的操作
glLoadIdentity();
}
//渲染时把解出来的数据从队列中取出生成新的纹理。
//渲染采用glDrawArrays函数,使用的GL_TRIANGLES参数,使用这个参数
//对于计算球的顶点坐标和纹理坐标来说不需要考虑很多,比较方便,就是点数过多的时候可能会影响渲染的效率。
void display(void)
{
glLoadIdentity();//恢复初始坐标系 注释掉后会一直闪
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除两个缓冲区,clear all pixels像素
gluLookAt(0, 0, distance, 0, 0, 500, 0, 1, 0);//在球里面的时候,500处的值越小,离得越近
//前三个是脑袋的位置,中间三个是眼睛看向的位置,后三个是头顶朝向的位置
printf("distance: %f \n", distance);
//glRotatef(Angle, Xvector, Yvector, Zvector) 用于绕轴旋转物体。
//Angle 是一个用于指定旋转角度的数字(通常存储于变量中)。
//Xvector, Yvector 和 Zvector 这三个参数用于描述一条向量, 以规定物体的旋转轴。
//在鼠标或键盘操控时,xangle会随之变化所以才会旋转
glRotatef(xangle, 1.0f, 0.0f, 0.0f); //绕X轴旋转
glRotatef(yangle, 0.0f, 1.0f, 0.0f); //绕Y轴旋转
glRotatef(zangle, 0.0f, 0.0f, 1.0f); //绕Z轴旋转
EnterCriticalSection(&frame_queue.cs);
printf("display size = %d \n", frame_queue.size);
if (frame_queue.size > 0)
{
Vid_Frame *vp = &frame_queue.queue[frame_queue.front];//vp指向队首
//glGenTextures(1, &texturesArr);
glBindTexture(GL_TEXTURE_2D, texturesArr);
//glTexImage2D第七第八个参数定义了源图的格式和数据类型,最后一个参数是真正的图像数据
//当前绑定的纹理对象就会被附加上纹理图像,生成纹理
glTexImage2D(GL_TEXTURE_2D, 0, 3, vp->width, vp->height, 0, GL_BGR_EXT, GL_UNSIGNED_BYTE, vp->buffer);
frame_queue.size--;
frame_queue.front = (frame_queue.front + 1) % MAXSIZE;
}
LeaveCriticalSection(&frame_queue.cs);
//glColor3f(1.0, 0.0, 0.0); //绘制物体所使用的颜色
// 启用顶点数组
glEnableClientState(GL_VERTEX_ARRAY);
//启用纹理数组 顶点坐标+纹理坐标
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
/*glVertexPointer指定顶点数组的位置,3表示每个顶点由三个量构成(x, y,z),
GL_FLOAT表示每个量都是一个GLfloat类型的值。第三个参数0表示紧密排列。
最后一个指明了数组实际的位置。*/
glVertexPointer(3, GL_FLOAT, 0, verticals);
glTexCoordPointer(2, GL_FLOAT, 0, UV_TEX_VERTEX);
//绘制,第二个参数是从数组缓存中的哪一位开始绘制,一般为0。第三个参数为数组中顶点的数量。
glDrawArrays(GL_TRIANGLES, 0, (180 / cap_H) * (360 / cap_W) * 6);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
glDisableClientState(GL_VERTEX_ARRAY); // disable vertex arrays
glFlush();//保证绘图命令将实际进行,而不是存储在缓冲区等待其他命令
}
DWORD WINAPI ThreadFunc(LPVOID n)
{
AVFormatContext* pFormatCtx;
int i, videoindex;
AVCodec* pCodec;//解码器
AVCodecContext* pCodecCtx = NULL;
char filepath[] = "cuc_ieschool.mp4";
av_register_all();//注册组件
avformat_network_init();//支持网络流
pFormatCtx = avformat_alloc_context();//创建AVFormatContext结构体
//该函数读取文件头并将有关文件格式的信息存储在我们提供的AVFormatContext结构中。
//最后两个参数用于指定文件格式,缓冲区大小和格式选项,但是通过将其设置为NULL或0,libavformat将自动检测它们。
if (avformat_open_input(&pFormatCtx, filepath, NULL, NULL) != 0){//打开一个输入流
printf("Couldn't open input stream.(无法打开输入流)\n");
return -1;
}
if (avformat_find_stream_info(pFormatCtx, NULL) < 0)//获取流信息
{
printf("Couldn't find stream information.(无法获取流信息)\n");
return -1;
}
videoindex = -1;
for (i = 0; i < pFormatCtx->nb_streams; i++){//找到流队列中,视频流所在位置
if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
{
videoindex = i;
break;
}
}
if (videoindex == -1)
{
printf("Didn't find a video stream.(没有找到视频流)\n");
return -1;
}
//查找解码器
pCodecCtx = pFormatCtx->streams[videoindex]->codec;
pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
if (pCodec == NULL)
{
printf("Codec not found.(没有找到解码器)\n");
return -1;
}
if (avcodec_open2(pCodecCtx, pCodec, NULL)<0)
{
printf("Could not open codec.(无法打开解码器)\n");
return -1;
}
AVFrame *pFrame;
pFrame = av_frame_alloc();//分配视频帧,存储从packet中解码出来的原始视频帧
int ret, got_picture;
AVPacket *packet = (AVPacket *)av_malloc(sizeof(AVPacket));
AVFrame *pFrameBGR = NULL;
pFrameBGR = av_frame_alloc();
struct SwsContext *img_convert_ctx;
int index = 0;
while (av_read_frame(pFormatCtx, packet) >= 0)//return 0 if OK, < 0 on error or end of file
{
if (packet->stream_index == videoindex)//判断是不是来自视频流的数据包,不是会直接跳出
{
//解码,将数据包转换为帧。输入为packet,输出为original_video_frame
//其中的pFrame存储解码视频的AVFrame。
ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
if (ret < 0)
{
printf("Decode Error.(解码错误)\n");
continue;
}
if (got_picture)
{
index++;
flag_wait:
if (frame_queue.size >= MAXSIZE)//如果解码过快列队存储不下,则此线程暂缓,让主线程渲染后再继续运行
{
printf("size = %d I'm WAITING ... \n", frame_queue.size);
Sleep(100);
goto flag_wait;
}
EnterCriticalSection(&frame_queue.cs);//防止数据错乱
Vid_Frame *vp;
vp = &frame_queue.queue[frame_queue.rear];//vp指向列队的尾部,自动就知道rear?
//vp->frame->pts = pFrame->pts;
/* alloc or resize hardware picture buffer */
//令vp的buffer的size width height都等于pFrame,就是给存储数据的buffer赋值
if (vp->buffer == NULL || vp->width != pFrame->width || vp->height != pFrame->height)
{
if (vp->buffer != NULL)
{
av_free(vp->buffer);
vp->buffer = NULL;
}
//int iSize = avpicture_get_size(AV_PIX_FMT_BGR24, pFrame->width, pFrame->height);
int iSize = av_image_get_buffer_size(AV_PIX_FMT_BGR24, pFrame->width, pFrame->height, 1);
av_free(vp->buffer);
vp->buffer = (uint8_t *)av_mallocz(iSize);
vp->width = pFrame->width;
vp->height = pFrame->height;
}
av_image_fill_arrays(vp->frame->data, vp->frame->linesize, vp->buffer, AV_PIX_FMT_BGR24, pCodecCtx->width, pCodecCtx->height, 1);
//frame和buffer都是已经申请到的一段内存, 会将frame的数据按BGR24的格式自动"关联"到buffer。
//avpicture_fill((AVPicture *)vp->frame, vp->buffer, AV_PIX_FMT_BGR24, pCodecCtx->width, pCodecCtx->height);
if (vp->buffer)
{
//用sws_getContext初始化SwsContex
/*srcW:源图像的宽
srcH:源图像的高
srcFormat:源图像的像素格式
dstW:目标图像的宽
dstH:目标图像的高
dstFormat:目标图像的像素格式
flags:设定图像拉伸使用的算法*/
img_convert_ctx = sws_getContext(vp->width, vp->height, (AVPixelFormat)pFrame->format, vp->width, vp->height,
AV_PIX_FMT_BGR24, SWS_BICUBIC, NULL, NULL, NULL); //AV_PIX_FMT_YUV420P, AV_PIX_FMT_BGR24
//转换一帧图像,转换完成的数据保存到了vp,也自动到了buffer里面。
sws_scale(img_convert_ctx, pFrame->data, pFrame->linesize, 0, vp->height, vp->frame->data, vp->frame->linesize);
//释放SwsContext结构体
sws_freeContext(img_convert_ctx);
//vp->pts = pFrame->pts;
}
frame_queue.size++;
frame_queue.rear = (frame_queue.rear + 1) % MAXSIZE;
LeaveCriticalSection(&frame_queue.cs);
}
}
av_free_packet(packet);
}
avcodec_close(pCodecCtx);
avformat_close_input(&pFormatCtx);
return 0;
}
void reDraw(int millisec)
{
glutTimerFunc(millisec, reDraw, millisec);
glutPostRedisplay();
}
void keyboard(unsigned char key, int x, int y)
{
switch (key)
{
case 'x': //当按下键盘上x时,以沿X轴旋转为主
xangle += 1.0f; //设置旋转增量
break;
case 'X':
xangle -= 1.0f; //设置旋转增量
break;
case 'y':
yangle += 1.0f;
break;
case 'Y':
yangle -= 1.0f;
break;
case 'z':
zangle += 1.0f;
break;
case 'Z':
zangle -= 1.0f;
break;
case 'd':
distance += 10.0f;
break;
case 'D':
distance -= 10.0f;
break;
default:
return;
}
glutPostRedisplay(); //重绘函数
}
//处理鼠标点击
void Mouse(int button, int state, int x, int y)
{
if (state == GLUT_DOWN) //第一次鼠标按下时,记录鼠标在窗口中的初始坐标
{
//记住鼠标点击后光标坐标
cx = x;
cy = y;
}
}
//处理鼠标拖动
void onMouseMove(int x, int y)
{
float offset =0.3;// 0.18;值越大,到相同位置转的角度就越大,即需要拖得越长才能到相应的位置
//计算拖动后的偏移量,然后进行xy叠加减
yangle -= ((x - cx) * offset);
if ( y > cy) {//往下拉
xangle += ((y - cy) * offset);
}
else if ( y < cy) {//往上拉
xangle += ((y - cy) * offset);
}
glutPostRedisplay();
//保存好当前拖放后光标坐标点
cx = x;
cy = y;
}
int main(int argc, char* argv[])
{
printf("可通过按键或者鼠标控制视频旋转\n");
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB | GLUT_DEPTH);//创建窗口的模式:单缓冲区和RGB模式、使用深度缓存
glutInitWindowSize(1280, 720);//窗口大小
glutInitWindowPosition(50, 50);//窗口左上角屏幕位置
glutCreateWindow("OpenGL全景");
init();
glutReshapeFunc(reshape); //窗口大小改变或窗口位置改变时候要调用的函数
glutDisplayFunc(display); //指定当窗口内容需要重绘时要调用的函数,在窗口刚打开,弹出,改变位置,点击等,都会触发事件。
glutKeyboardFunc(keyboard);//当一个能生成ASCII字符的键按下时,keyboard函数会被调用
glutMouseFunc(Mouse);//当一个鼠标按钮按下或释放,调用Mouse函数
glutMotionFunc(onMouseMove);//当鼠标按下并在窗口移动鼠标时,调用onMouseMove函数
glutTimerFunc(25, reDraw,1);//原最后一个参数为25
HANDLE hThrd = NULL;
DWORD threadId;
hThrd = CreateThread(NULL, 0, ThreadFunc, 0, 0, &threadId);//创建一个新线程
//该函数才真正进入GLUT事件循环,语句阻塞在此。
//当对应的事件发生时,被注册的回调函数如glutDisplayFunc中注册的就会被调用。
glutMainLoop();//无限执行的循环,判断窗口是否需要重绘
//WaitForSingleObject(hThrd, INFINITE);
if (hThrd)
{
CloseHandle(hThrd);//线程句柄生命周期结束
}
return 0;
}