全景视频播放器代码分析

  • 一、前期准备
    • (1)FFmpeg新旧接口对照使用一览
    • (2)libswscale图片像素数据处理类库
    • (3)OpenGL相关记录
    • (4)列队与线程
  • 二、代码分析

来总结一下最近研究的全景视频播放器代码

平台: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库里函数都是分散的百度出来的,很杂很散的样子。

除了这些,零零碎碎搜到的一些东西也大致记录了一下。

(1)FFmpeg新旧接口对照使用一览

FFmpeg新旧接口对照使用一览.

(2)libswscale图片像素数据处理类库

sws_getContext():初始化一个SwsContext。
sws_scale():处理图像数据。
sws_freeContext():释放一个SwsContext。

关于avpicture_fill与sws_scale:
关于avpicture_fill与sws_scale.对应于FFmpeg解码之后像素格式变换的代码
FFmpeg解码H264及swscale缩放详解.函数解释很细

(3)OpenGL相关记录

总结到了另一篇:全景视频播放器中OpenGL的相关记录.

(4)列队与线程

总结到了:列队与线程(全景视频播放器).

二、代码分析

主要的思路是将全景视频利用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.1415926GLfloat  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);//设置当前使用的清除颜色值,这里为黑色,参数为RGBaglClearDepth(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 arraysglFlush();//保证绘图命令将实际进行,而不是存储在缓冲区等待其他命令}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);//原最后一个参数为25HANDLE hThrd = NULL;DWORD threadId;hThrd = CreateThread(NULL, 0, ThreadFunc, 0, 0, &threadId);//创建一个新线程//该函数才真正进入GLUT事件循环,语句阻塞在此。//当对应的事件发生时,被注册的回调函数如glutDisplayFunc中注册的就会被调用。glutMainLoop();//无限执行的循环,判断窗口是否需要重绘//WaitForSingleObject(hThrd, INFINITE);if (hThrd){CloseHandle(hThrd);//线程句柄生命周期结束}return 0;
}

全景视频播放器代码分析相关推荐

  1. android vr播放器 开发,Android应用开发之Android VR Player(全景视频播放器)- ExoPlayer播放器MPEG-DASH视频播放...

    本文将带你了解Android应用开发之Android VR Player(全景视频播放器)- ExoPlayer播放器MPEG-DASH视频播放,希望本文对大家学Android有所帮助. Androi ...

  2. 全景视频播放器中OpenGL的相关记录

    全景视频播放器中OpenGL的相关记录 一.OpenGL顶点数组 二.坐标系与投影 三.坐标系相关函数 四.纹理坐标 五.纹理过滤 六.深度缓冲区 七.OpenGL的glut库 OpenGL函数功能g ...

  3. QT + OpenGL + FFmpeg写的一个全景视频播放器

    临时被分配了一个任务 写一个C++版本的全景视频播放器 网上搜了搜  基于前辈的基础上 写的差不多了 测试视频源是用ffmpeg拉RTSP的流 最终是要嵌入到别的一个视频播放器模块 所以解码这块我不用 ...

  4. Android VR Player(全景视频播放器) [7]:视频列表的实现-网络视频

    Android VR Player(全景视频播放器) [7]:视频列表的实现-网络视频 前期准备 在之前的博文,Android VR Player(全景视频播放器) [6]:视频列表的实现-本地视频 ...

  5. Android VR Player(全景视频播放器) [6]:视频列表的实现-本地视频

    Android VR Player(全景视频播放器) [6]:视频列表的实现-本地视频 (本篇博客参考<Android第一行代码(第二版)>中关于RecyclerView的部分) 列表的实 ...

  6. flv f4v mp4 视频播放器代码

    flv f4v mp4 视频播放器代码 ckplayer是一款在网页上播放视频的免费的播放器,功能强大,体积小巧,跨平台,使用起来随心所欲. 播放器主要以adobe的flash(所使用的版本是CS5) ...

  7. 简单的SWF视频播放器代码

    简单的SWF视频播放器代码 SWF视频播放器代码危险演出: 代码如下: <TABLE style="BORDER-RIGHT: #28435b 10px solid; BORDER-T ...

  8. Android 全景视频播放器(VR视频播放器探索二)

        上次随便写着玩的  http://blog.csdn.net/ai_yong_jie/article/details/51159367   Android 全景视频播放器(VR视频播放器探索一 ...

  9. 网页视频播放器代码Vcastr2

    TIP:播放的视频地址必须是线上的 第一种:js <p style="text-align: center;"><script type="text/j ...

最新文章

  1. kosaraju算法
  2. 5G 信令流程 — 5GS 的 gNB 切换(Xn/N2 Handover)管理
  3. 介绍下重绘和回流(Repaint Reflow),以及如何进行优化
  4. UNITY Destroy()和DestroyImadiate()的区别
  5. 部署xhprof监控php效率(linux版本)
  6. Ubuntu上的samba共享文件安装配置
  7. C#多线程与并行编程方面的电子书,中英文版本
  8. [Swift]LeetCode853. 车队 | Car Fleet
  9. 互逆的压缩与解压(洛谷P1319、P1320题题解,Java语言描述)
  10. 5.2 各种类型的Attention: 原理、计算流程
  11. 怎么把ppt弄成链接的形式_如何在PPT中插入视频是嵌入而不是将视频文件设为链接...
  12. FFT算法讲解——麻麻我终于会FFT了!
  13. C printf输出格式控制
  14. 《CSS权威指南》读书笔记3
  15. 华为交换机路由器登陆密码详细操作
  16. Ribbon停止维护
  17. 微信小程序实现vtt视频字幕
  18. 阿里系App抓包详细分析
  19. 塑造成功性格的15种方法
  20. matlab编程入门实例,matlab编程实例100例

热门文章

  1. java中实现多线程的三种方式
  2. 主梁弹性模量计算_弹性模量计算
  3. 2023新华为OD机试题 - 相同数字的积木游戏 1(JavaScript)
  4. 欧格教育:拼多多搜索排名的算法是怎么样
  5. IGMP-Snooping协议原理
  6. 你会用浩辰3D软件来进行零件建模吗?3D零件建模全解析!
  7. html 设置不可用,NVIDIA 显示设置不可用。原因与解决方法
  8. 4款需要低调使用的黑科技电脑软件,请悄悄收藏,不然太可惜
  9. 递归神经网络结构形式,RNN神经网络基本原理
  10. 移动魔百盒M301H,CW代工,通用强刷-卡刷固件