视频播放器是如何播放音视频的?

当我们用手机或者电脑打开一个电影视频或者一首音频歌曲的时候,不论是在线流量还是离线本地播放,通常设备上的音视频播放器都可以将音视频文件中的画面和声音给到我们的视觉和听觉器官,这是我们习以为常的东西。但不知你是否有考虑过,播放器底层究竟是如何处理音视频文件的呢?

如果你对音视频有一些基础和了解,应该知道通常播放一段音视频的基本流程是:解协议 → 解封装 → 解码 → 视音频同步这几大步骤。这里的解协议通常对应的是网络流媒体传输音视频,比如RTMP协议,RTSP协议,HTTP协议等。解完协议之后我们得到的是采用一个音视频(字幕)组织在一起的封装音视频文件,例如MP4,MKV,RMVB,TS,FLV,AVI等等。此时我们通过解封装将音频和视频数据分离出来,音频的归音频,视频的归视频。

然后我们分别将视频,音频的压缩编码数据,解码成为非压缩的视频/音频原始数据,如H264视频和PCM音频。音频的压缩编码标准有AAC,MP3,AC-3等等,视频的压缩编码标准有H.264,MPEG2,VC-1等等。

在拿到解码后的音视频数据之后,我们将其各自送到可以识别和处理的硬件设备上进行播放,比如视频播放的显卡,音频播放的声卡。但是在将解码后的音频和视频数据送去播放的时候,有一个同步的关键问题。比如电影播放一定要保证画面中声音和画面动作的匹配和一致,这样才能有比较理想的观看体验。

以上内容可以说是目前市面上所有播放器的基本流程和必有流程。而且,目前多数播放器底层实现均是基于开源软件ffmpeg自带的ffplay播放器模型。下面就谈谈ffplay是如何工作的。

一、视频播放

需要说明的是,从FFmpeg3.x版本开始,视频解码接口avcodec_decode_video2就被废弃了,解码取而代之的是avcodec_send_packet和avcodec_receive_frame。
av_read_frame得到压缩的数据包AVPacket,一般有三种压缩的数据包(视频、音频和字幕),都用AVPacket表示。然后调用avcodec_send_packet 和 avcodec_receive_frame对AVPacket进行解码得到AVFrame。

在 read_thread 函数中,通过 av_read_frame函数读取数据包AVPacket,然后调用packet_queue_put将AVPacket添加到PacketQueue中。
在 video_thread 函数中,通过get_video_frame函数读取数据帧AVFrame, 然后调用queue_picture将AVFrame添加到FrameQueue中。

那么两个队列是怎么联系起来的呢?通过分析read_thread函数可以知晓:
首先,创建解复用和解码所需要的数据结构;
然后分别通过 stream_component_open 函数打开三种数据流;
最后,通过av_read_frame将解复用后的数据包分别添加到对应的PacketQueue中。

stream_component_open函数主要负责解码工作,ffplay中为解码工作专门设置了一个数据结构Decoder,Decoder结构中有一个成员queue,这个queue就是输入的PacketQueue。

通过decoder_init函数来指定PacketQueue,这个工作就是在stream_component_open中执行的。
指定PacketQueue之后通过get_video_frame函数从PacketQueue中解码出AVFrame结构。
最后通过queue_picture函数将解码得到的帧添加到FrameQueue。

video_thread 和 audio_thread的作用把packet缓冲区里的packet解码成frame,装入frame缓冲区FrameQueue。

video_refresh内部工作过程:
1.先获取最早的frame,计算和下一帧的时间(vp_duration)
2.处理音视频同步的。根据同步的方式,获取修正过后的下一帧时间。
3.如果时间没到直接跳过退出video_refresh,进入av_usleep暂停下。代码里有点坑的是,这里写的是goto display;,而display代码块需要is->force_refresh这个为true才真的起作用,所以实际上是结束了,看起来有些误导。
4.确定要显示下一帧之后,才调用frame_queue_next把下一帧推到队列关键位,即索引rindex指定的位置。
5.video_display2最终到了SDL_Vout的display_overlay函数。和前面一样,到了显示层,在这做了解耦处理,SDL_Vout对象是ffp->vout,也是在IJKFFMoviePlayerControllerinit里构建的。

二、音频播放

音频和视频的逻辑不一样,视频是搭建好播放层(OpenGLES的view等)后,把一帧帧的数据 主动 推过去,而音频是开启了音频队列后,音频队列会过来跟你要数据,解码层这边是 被动 的把数据装载进去。
1.启动定时器Timer,计时器40ms刷新一次,利用SDL事件机制,触发从图像帧队列中读取数据,进行渲染显示;
2.stream_componet_open函数中,av_read_frame()读取到AVPacket,随后放入到音频、视频或字幕Packet队列中;
3.video_thread,从视频packet队列中获取AVPacket并进行解码,得到AVFrame图像帧,放到VideoPicture队列中。
4.audio_thread线程,同video_thread,对音频Packet进行解码;
5.subtitle_thread线程,同video_thread,对字幕Packet进行解码。

三、代码解析

3.1 main函数

ffplay的main函数主要步骤有以下几个函数:

SDL_Init
SDL_CreateWindow
SDL_CreateRender

stream_open
event_loop

这里前面3个步骤几乎是SDL的标配,初始化SDL后创建了一个窗口,然后创建Render用于渲染窗口和其他内容。
第4步接着调用stream_open,创建read_thread线程,read_thread会打开文件,解析封装,获取AVStream信息,启动解码器(创建解码线程),并开始读取文件。
第5步,event_loop主要是一个主循环,执行:refresh_loop_wait_event处理SDL事件队列中的事件。比如关闭窗口可以触发do_exit销毁播放现场。

avdevice_register_all();

avformat_network_init();

parse_options
	prepare_app_arguments
	parse_option

1.SDL_Init

SDL_EventState(SDL_SYSWMEVENT, SDL_IGNORE);
SDL_EventState(SDL_USEREVENT, SDL_IGNORE);

av_init_packet


2.SDL_CreateWindow

SDL_SetHint
3.SDL_CreateRenderer


4.stream_open()//主要函数内部会创建read_thread线程

	frame_queue_init()
	packet_queue_init()
	init_clock()
	
	SDL_CreateThread(read_thread, "read_thread", is)//this thread gets the stream from the disk or the network
		avformat_open_input//根据输入的文件,创建AVFormatContext
		avformat_find_stream_info
		
		stream_component_open()//启动线程,执行video_thread函数,从视频队列获取视频数据,然后解码和显示。
			
			avcodec_find_decoder()
			avcodec_open2()
			
			decoder_init()//decoder_init函数来指定PacketQueue
			decoder_start(&is->viddec, video_thread, is)====SDL_CreateThread
				get_video_frame()//从PacketQueue中解码出AVFrame结构
					decoder_decode_frame()
						for(;;)
							avcodec_receive_frame():流连续的情况下,不断获取解码后的frame
						packet_queue_get():阻塞调用
						将packet送入解码器。
				queue_picture()//将解码得到的帧添加到FrameQueue
				
							
		stream_component_open()//启动SDL的线程,执行sdl_audio_callback,从音频队列获取音频数据,编解码和输出。
			audio_open()
			sdl_audio_callback()
			audio_decode_frame();//处理sampq到audio_buf的过程,最多只是执行了重采样
				swr_convert()
		
			avcodec_find_decoder()
			avcodec_open2()
			
			decoder_init()//decoder_init函数来指定PacketQueue
			decoder_start(&is->auddec, audio_thread, is)//音频解码线程
				decoder_decode_frame()
		
		
5.event_loop()
	refresh_loop_wait_event()//视频显示控制函数
		video_refresh()//音视频同步
			video_display()			
				video_image_display2()
					SDL_VoutDisplayYUVOverlay()
			

video_refresh的主体流程分为3个步骤:
1.计算上一帧应显示的时长,判断是否继续显示上一帧
2.估算当前帧应显示的时长,判断是否要丢帧
3.调用video_display进行显示

video_display会调用frame_queue_peek_last获取上次显示的frame,并显示。
所以在video_refresh中如果流程直接走到video_display就会显示lastvp,如果先调用frame_queue_next再调用video_display,那么就会显示vp.

3.2视频解码

video_thread
1.调用get_video_frame解码一帧图像
2.计算时长和pts
3.调用queue_picture放入FrameQueue

static int video_thread(void *arg)
{
    AVRational tb = is->video_st->time_base;
    AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);
 
    for (;;) 
	{
        ret = get_video_frame(is, frame); //解码获取一帧视频画面
        
		if (ret < 0)//解码结束
            goto the_end;
        if (!ret)//没有解码得到画面
            continue;
 
        duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);//用帧率估计帧时长
        pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);//将pts转化为秒为单位
        ret = queue_picture(is, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);//将解码后的帧存入FrameQueue
        
		av_frame_unref(frame);
 
        if (ret < 0)
		{
            goto the_end;
		}
    }
 the_end:
    av_frame_free(&frame);
    return 0;
}

其中get_video_frame简化如下:
static int get_video_frame(VideoState *is, AVFrame *frame)
{
    int got_picture;
 
    if ((got_picture = decoder_decode_frame(&is->viddec, frame, NULL)) < 0)
	{
        return -1;
	}
 
    if (got_picture) 
	{
        frame->sample_aspect_ratio = av_guess_sample_aspect_ratio(is->ic, is->video_st, frame);
        //……
    }
    return got_picture;
}

其中

static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) 
{
    for (;;) 
	{
        //1. 流连续的情况下,不断调用avcodec_receive_frame获取解码后的frame
        if (d->queue->serial == d->pkt_serial) 
		{
            do 
			{
                ret = avcodec_receive_frame(d->avctx, frame);
                if (ret == AVERROR_EOF) 
				{
                    return 0;
                }
                if (ret >= 0)
                    return 1;
            } while (ret != AVERROR(EAGAIN));
        }
 
        //2. 取一个packet,顺带过滤“过时”的packet
        do 
		{
            if (d->queue->nb_packets == 0)
                SDL_CondSignal(d->empty_queue_cond);
            if (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial) < 0)
                return -1;
        } while (d->queue->serial != d->pkt_serial);
 
        //3. 将packet送入解码器
        avcodec_send_packet(d->avctx, &pkt);
    }
}

往PacketQueue送入一个flush_pkt后,PacketQueue的serial值会加1,而送入的flush_pkt和PacketQueue的serial值保持一致。
所以如果有“过时”Packet,过滤后,取到的第一个pkt将是flush_pkt,此时需要调用avcodec_flush_buffers。

video_thread中关键的get_video_frame函数,可以取到正确解码后的一帧数据了。接下来就要把这一帧放入FrameQueue:

duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
ret = queue_picture(is, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);
av_frame_unref(frame);

主要是调用queue_picture的代码,先frame_queue_peek_writable取FrameQueue的当前写节点,然后把该拷贝的拷贝给节点(struct Frame)保存,
然后frame_queue_push,“push”节点到队列中。
唯一需要关注的是,AVFrame的拷贝是通过av_frame_move_ref实现的,所以拷贝后src_frame就是无效的了。

audio的解码过程,不考虑filter部分,与video的解码几乎一样。就不重复分析了。

3.3字幕解码

static int subtitle_thread(void *arg)
{
    VideoState *is = arg;
    Frame *sp;
    int got_subtitle;
    double pts;
 
    for (;;) {
        //注意这里是先frame_queue_peek_writable再decoder_decode_frame
        if (!(sp = frame_queue_peek_writable(&is->subpq)))
            return 0;
 
        if ((got_subtitle = decoder_decode_frame(&is->subdec, NULL, &sp->sub)) < 0)
            break;
 
        pts = 0;
 
        if (got_subtitle && sp->sub.format == 0) {
            if (sp->sub.pts != AV_NOPTS_VALUE)
                pts = sp->sub.pts / (double)AV_TIME_BASE;
            sp->pts = pts;
            sp->serial = is->subdec.pkt_serial;
            sp->width = is->subdec.avctx->width;
            sp->height = is->subdec.avctx->height;
            sp->uploaded = 0;
 
            /* now we can update the picture count */
            frame_queue_push(&is->subpq);
        } else if (got_subtitle) {
            avsubtitle_free(&sp->sub);
        }
    }
    return 0;
}

主要流程是:
frame_queue_peek_writable先取一个可写的帧节点,
decoder_decode_frame解码一帧字幕,
我们会发现subtitle_thread中的主流程顺序和video_thread是相反的,video_thread是先解码后取可写的帧节点,然后写入。

猜测是:由于字幕的FrameQueue消耗比较慢(比如常见的一句话对应一帧),这就导致FrameQueue经常是处于堆满的状态。
如果先解码,再调用frame_queue_peek_writable,大概率会被阻塞,期间如果发生seek,则解码的这一帧就浪费了。
与其先解码再等锁,不如先等锁再解码。
解码字幕的时候还走avcodec_decode_subtitle2,对应的decoder_decode_frame简化为:

static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
    int ret = AVERROR(EAGAIN);
    for (;;) 
	{
        //1. 判断返回值
        if (d->queue->serial == d->pkt_serial) {
            do {
                if (ret == AVERROR_EOF) {
                    return 0;
                }
                if (ret >= 0)
                    return 1;
            } while (ret != AVERROR(EAGAIN));
        }
 
        //2. 取pkt
        packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial);
 
        //3.解码
        ret = avcodec_decode_subtitle2(d->avctx, sub, &got_frame, &pkt);
        if (ret < 0) {
            ret = AVERROR(EAGAIN);
        } else {
            if (got_frame && !pkt.data) {//如果是null_packet,且置为pending,下次继续
               d->packet_pending = 1;
               av_packet_move_ref(&d->pkt, &pkt);
            }
            ret = got_frame ? 0 : (pkt.data ? AVERROR(EAGAIN) : AVERROR_EOF);
        }
    }
}

因为和audio/video共用一个函数,而又使用的avcodec_decode_subtitle2(相比send/receive只一个函数完成解码)解字幕,所以代码顺序看起来会有些奇怪:
1.判断返回值
2.取pkt
3.解码
步骤1,在步骤3后更合适,但因为是循环体内,所以,也是可以正确运行的。

在解码字幕时,null_packet用于最后将解码器内剩余的解码数据取出。如果还能取到数据(got_frame == 1),则将null_packet暂存d->pkt,置packet_pending,下次继续取。直到avcodec_decode_subtitle2返回got_frame == 0.

3.4显示视频

/**
 * 如果没有事件,则显示视频video_refresh
 * 循环检测并优先处理用户输入事件
 * 内置刷新率控制,约10ms刷新一次
 */
static void refresh_loop_wait_event(VideoState *is, SDL_Event *event) {
    double remaining_time = 0.0;
	
    /*从输入设备收集事件并放到事件队列中 */
    SDL_PumpEvents();
	
	
    /**
     * SDL_PeepEvents
     * 从事件队列中提取事件,由于这里使用的是SDL_GETEVENT, 所以获取事件时会从队列中移除
     * 如果有事件发生,返回事件数量,则while循环不执行。
     * 如果出错,返回负数的错误码,则while循环不执行。
     * 如果当前没有事件发生,且没有出错,返回0,进入while循环。
     */
	 
    while (!SDL_PeepEvents(event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT)) 
	{
		//此时没有事件
        /*隐藏鼠标指针,CURSOR_HIDE_DELAY = 1s */
        if (!cursor_hidden && av_gettime_relative() - cursor_last_shown > CURSOR_HIDE_DELAY) 
		{
            SDL_ShowCursor(0);
            cursor_hidden = 1;
        }
		
        /* 默认屏幕刷新率控制,REFRESH_RATE = 10ms */
        if (remaining_time > 0.0)
		{
            av_usleep((int64_t)(remaining_time * 1000000.0));
		}
        remaining_time = REFRESH_RATE;
        
		
		/* 显示视频 */
        if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh))
		{
            video_refresh(is, &remaining_time);
		}
        
		
		/*再次从输入设备收集事件并放到事件队列中 */
        SDL_PumpEvents();
    }
}

显示视频的流程是:
video_refresh()–>video_display()–>video_image_display()–>calculate_display_rect()

1…计算上一帧应显示的时长,判断是否继续显示上一帧
2.估算当前帧应显示的时长,判断是否要丢帧
3.调用video_display进行显示


```c
video_refresh
{
    VideoState *is = opaque;
    double time;

    Frame *sp, *sp2;

    if (!is->paused && get_master_sync_type(is) == AV_SYNC_EXTERNAL_CLOCK && is->realtime)
    {
		check_external_clock_speed(is);
	}

	//音频波形图显示
    if (!display_disable && is->show_mode != SHOW_MODE_VIDEO && is->audio_st) 
	{
        time = av_gettime_relative() / 1000000.0;
        if (is->force_refresh || is->last_vis_time + rdftspeed < time) 
		{
            video_display(is);
            is->last_vis_time = time;
        }
        *remaining_time = FFMIN(*remaining_time, is->last_vis_time + rdftspeed - time);
    }


	// 视频播放
    if(is->video_st) 
	{
retry:
		//首先检查pictq是否为空
        if (frame_queue_nb_remaining(&is->pictq) == 0) 
		{
            //nothing to do, no picture to display in the queue
        } 
		else 
		{
            double last_duration, duration, delay;

			//lastvp上一帧,vp当前帧,nextvp下一帧
            Frame *vp, *lastvp;

            /* dequeue the picture */
            lastvp = frame_queue_peek_last(&is->pictq);

			//获取待显示的第一个帧
            vp = frame_queue_peek(&is->pictq);

			//获取待显示的第二个帧,跳帧处理
			//先判断frame_queue_peek获取的vp是否是最新序列
            if (vp->serial != is->videoq.serial) 
			{
				//条件成立,说明发生过seek等操作,流不连续,应该抛弃lastvp
                frame_queue_next(&is->pictq);//抛弃lastvp后,返回流程开头重试下一轮
                goto retry;
            }

            if (lastvp->serial != vp->serial)
			{
                is->frame_timer = av_gettime_relative() / 1000000.0;//上一帧播放时刻,基准时间
            }

			//暂停处理不停播放上一帧图像
            if (is->paused)
            {
				goto display;
			}

			//下面计算准确的lastvp应显示时长
            last_duration = vp_duration(is, lastvp, vp);
			//根据播放时长,和主音频的时钟信号,重新计算延时delay
            delay = compute_target_delay(last_duration, is);
			//以上两步计算出准确的上一帧应显示时长了


			//最后根据上一帧应显示时长(delay变量),确定是否继续显示上一帧
            time= av_gettime_relative()/1000000.0;

			//如果当前系统时间小于显示时间 则直接进行显示上一帧,如果上一帧显示时长未满,重复显示上一帧
			//frame_timer可以理解为帧显示时刻,更新前,为上一帧的显示时刻;更新后,为当前帧显示时刻
            if (time < is->frame_timer + delay) 
			{
                *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
                goto display;
            }


			//第二步准备工作:更新frame_timer和更新vidclk
            is->frame_timer += delay; //根据音频时钟,只要需要延时,即delay大于0,就需要更新累加到frame_timer
            if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX)
            {
				is->frame_timer = time;//如果与系统时间的偏离太大,则修正为系统时间
			}   
			/** 更新视频时间轴 **/
            SDL_LockMutex(is->pictq.mutex);
            if (!isnan(vp->pts))
                update_video_pts(is, vp->pts, vp->pos, vp->serial);
            SDL_UnlockMutex(is->pictq.mutex);



			//丢帧逻辑
			/**如果队列中有未显示的帧,如果开启了丢帧处理或者 不是以视频为主时间轴,则进行丢帧处理**/
            if (frame_queue_nb_remaining(&is->pictq) > 1)
			{
                Frame *nextvp = frame_queue_peek_next(&is->pictq);
                duration = vp_duration(is, vp, nextvp);

				//如果允许丢帧.则当下一帧数据播放时间晚于当前时间.则丢弃
				//1.不处于step状态。step用于pause状态下进行seek操作时,seek操作结束后显示seek后的一帧画面
				//2.启用framedrop,或当前video不是主时钟
				//3.系统时刻已大于frame_timer+duration,即当前这一帧永无出头日
                if(!is->step && (framedrop > 0 
					|| (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) 
					&& time > is->frame_timer + duration)
				{
                    is->frame_drops_late++;

					//用于在读完一个节点后调用,用于标记一个节点已经被读过
                    frame_queue_next(&is->pictq);
					
                    goto retry;
                }
            }

			//字幕
            if (is->subtitle_st) 
			{
				//;
            }

			//删除当前显示的帧(lastvp),将指针移动到下一个显示的帧(vp)或者(nextvp)用于显示
            frame_queue_next(&is->pictq);
            is->force_refresh = 1;

            if (is->step && !is->paused)
			{
                stream_toggle_pause(is);
			}
        }

display:

		//第三步:调用video_display进行显示
		//video_display中显示的是frame_queue_peek_last,所以需要先调用frame_queue_next,
		//移动pictq内的指针,将vp变成shown,确保frame_queue_peek_last取到的是vp。
		
        /* display picture */
        if (!display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown)
		{
            video_display(is);
		}
    }
    is->force_refresh = 0;

    if (show_status) 
	{
       //;
    }
}





/*display the current picture, if any */
static void video_display(VideoState *is)
{
    if (!is->width)
	{
        video_open(is);
	}

	//设置画笔的颜色
    SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
	
	//用指定的颜色清空缓冲区
    SDL_RenderClear(renderer);
	
    if (is->audio_st && is->show_mode != SHOW_MODE_VIDEO)
    {
		video_audio_display(is);
	}
    else if (is->video_st)
    {
		video_image_display(is);
	}
	
	

    SDL_RenderPresent(renderer);


}
video_image_display():
	calculate_display_rect()
	upload_texture():内部完成  SDL_UpdateTexture  调用
    set_sdl_yuv_conversion_mode(vp->frame);
    SDL_RenderCopyEx();//将源纹理的一部分复制到当前渲染目标,围绕给定中心旋转角度
    set_sdl_yuv_conversion_mode();

3.6 SDL渲染显示视频

使用SDL播放一个视频代码流程大体如下:
初始化:?在ffplay的main函数里面完成:

SDL_Init(): 初始化SDL
SDL_EventState()
SDL_CreateWindow(): 创建窗口(Window)
SDL_CreateRenderer(): 基于窗口创建渲染器(Render)
SDL_CreateTexture():  创建纹理(Texture)

循环渲染数据:?在ffplay的函数video_image_display里面完成。

SDL_UpdateTexture(): 设置纹理的数据。
SDL_RenderCopyEx():  纹理复制给渲染器。
SDL_RenderPresent(): 显示。

3.7渲染器

SDL_RenderPresent()调用了SDL_Render的RenderPresent()方法显示图像
Direct3D 渲染器中对应RenderPresent()的函数是D3D_RenderPresent():调用Direct3D的API IDirect3DDevice9_Present 完成显示
OpenGL 渲染器中对应RenderPresent()的函数是GL_RenderPresent():显示函数位于 SDL_GL_SwapWindow() :调用了SDL_VideoDevice的 GL_SwapWindow()函数
Software 渲染器中对应RenderPresent()的函数是SW_RenderPresent():SDL_UpdateWindowSurface()–>SDL_UpdateWindowSurfaceRects():BitBlt()函数
软件渲染器 SDL_UpdateTexture() 调用了memcpy()填充像素数据。

SDL渲染涉及的结构体:
SDL_Window: 代表了窗口
SDL_Renderer:代表了渲染器
SDL_Texture: 代表了纹理
SDL_Rect: 一个矩形框,用于确定纹理显示的位置。
YUV DATA:

渲染器将3D物体绘制到屏幕上的任务。渲染器分为硬件渲染器和软件渲染器组成。
OpenGL API通过OpenGL图形库来使用3D硬件
DirectX API使用微软的DirectX库——归并到 Windows操作系统中
软件渲染器则纯粹利用CPU的能力进行计算,通常采用光线追踪的方法进行渲染

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值