超详细解析FFplay之视频输出和尺寸变换模块
lipiwang 2024-11-15 22:01 13 浏览 0 评论
1.视频输出模块
ffplay为了适应不同的平台,选择了SDL(跨平台)作为显示的SDK,以便在windows、linux、macos等不同平台上实现视频画?的显示。
视频(图像)输出初始化。
开始分析视频(图像)的显示。因为使?了SDL,?video的显示也依赖SDL的窗?显示系统,所以先从main函数的SDL初始化看起(节选):
int main(int argc, char **argv)
{
//……
//3. SDL的初始化
flags = SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER;
if (SDL_Init (flags)) {
av_log(NULL, AV_LOG_FATAL, "Could not initialize SDL - %s\n" , SDL_GetError());
av_log(NULL, AV_LOG_FATAL, "(Did you set the DISPLAY variabl e?)\n");
exit(1);
}
//4. 创建窗?
window = SDL_CreateWindow(program_name, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, default_width, default_height, flags);
if (window) {
//创建renderer
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCEL ERATED | SDL_RENDERER_PRESENTVSYNC);
if (!renderer) {
av_log(NULL, AV_LOG_WARNING, "Failed to initialize a har dware accelerated renderer: %s\n", SDL_GetError());
renderer = SDL_CreateRenderer(window, -1, 0);
if (renderer) {
if (!SDL_GetRendererInfo(renderer, &renderer_info))
av_log(NULL, AV_LOG_VERBOSE, "Initialized %s rendere r.\n", renderer_info.name);
}
}
//5. 通过stream_open函数,开启read_thread读取线程
is = stream_open(input_filename, file_iformat);//这?创建了read_th read
if (!is) {
av_log(NULL, AV_LOG_FATAL, "Failed to initialize VideoState! \n");
do_exit(NULL);
}
//6. 事件响应
event_loop(is);
}
}
main函数主要步骤如下:
(1)SDL_Init,主要是SDL_INIT_VIDEO的?持。
(2)SDL_CreateWindow,创建主窗?。
(3)SDL_CreateRender,基于主窗?创建renderer,?于渲染输出。
(4)stream_open。
(5)event_loop,播放控制事件响应循环,但也负责了video显示输出。
重点分析set_default_window_size的原理,该函数主要获取窗?的宽?,以及视频渲染的区域:
//7 从待处理流中获取相关参数,设置显示窗?的宽度、?度及宽??
if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
AVStream *st = ic->streams[st_index[AVMEDIA_TYPE_VIDEO]];
AVCodecParameters *codecpar = st->codecpar;
/*根据流和帧宽??猜测帧的样本宽??。该值只是?个参考
*/
AVRational sar = av_guess_sample_aspect_ratio(ic, st, NULL);
if (codecpar->width) {
// 设置显示窗?的??和宽??
set_default_window_size(codecpar->width, codecpar->height, s ar);
}
}
static void set_default_window_size(int width, int height, AVRationa l sar)
{
SDL_Rect rect;
int max_width = screen_width ? screen_width : INT_MAX; // 确定 是否指定窗?最?宽度
int max_height = screen_height ? screen_height : INT_MAX; // 确定 是否指定窗?最??度
if (max_width == INT_MAX && max_height == INT_MAX)
max_height = height; // 没有指定最??度时则使?视频的?度
calculate_display_rect(&rect, 0, 0, max_width, max_height, width , height, sar);
default_width = rect.w;
default_height = rect.h;
}
screen_width和screen_height可以在ffplay启动时设置 -x screen_width -y screen_height获取指定的宽?,如果没有指定,则max_height = height,即是视频帧的?度,宽度是窗口宽度。
初始化窗?显示??
主要分析calculate_display_rect,根据传?的参数(int scr_xleft, int scr_ytop, int scr_width, int scr_height,int pic_width, int pic_height, AVRational pic_sar)获取显示区域的起始坐标和??(rect)。
static void calculate_display_rect(SDL_Rect *rect,int scr_xleft,
int scr_ytop, int scr_width, int scr_height,
int pic_width, int pic_height, AV Rational pic_sar)
{
AVRational aspect_ratio = pic_sar; // ?率
int64_t width, height, x, y;
if (av_cmp_q(aspect_ratio, av_make_q(0, 1)) <= 0)
aspect_ratio = av_make_q(1, 1);// 如果aspect_ratio是负数或者为 0,设置为1:1
// 转成真正的播放?例
aspect_ratio = av_mul_q(aspect_ratio, av_make_q(pic_width, pic_h eight));
/* XXX: we suppose the screen has a 1.0 pixel ratio */
// 计算显示视频帧区域的宽?
// 先以?度为基准 ,就是把高度按照原始视频高度来,铺满
height = scr_height;
// &~1, 取偶数宽度
width = av_rescale(height, aspect_ratio.num, aspect_ratio.den) & ~1;
if (width > scr_width) {
// 当以?度为基准,发现计算出来的需要的窗?宽度不?时调整为以窗?宽度为基准
width = scr_width;
height = av_rescale(width, aspect_ratio.den, aspect_ratio.nu m) & ~1;
}
// 计算显示视频帧区域的起始坐标(在显示窗?内部的区域)
x = (scr_width - width) / 2;
y = (scr_height - height) / 2;
rect->x = scr_xleft + x;
rect->y = scr_ytop + y;
rect->w = FFMAX((int)width, 1);
rect->h = FFMAX((int)height, 1);
}
typedef struct AVRational{
int num; ///< Numerator 分?
int den; ///< Denominator 分?
} AVRational;
注意视频显示尺?比例的计算,计算出真实的比例。
aspect_ratio = av_mul_q(aspect_ratio, av_make_q(pic_width, pic_height));
视频(图像)输出逻辑。
基本步骤如下:
1main() -- >
2 event_loop -->
3 refresh_loop_wait_event() -->
4 video_refresh() -->
5 video_display() -->
6 video_image_display() -->
7 upload_texture()
event_loop 开始处理SDL事件:
static void event_loop(VideoState *cur_stream)
{
SDL_Event event;
double incr, pos, frac;
for (;;) {
double x;
refresh_loop_wait_event(cur_stream, &event);//video是在这?显示 的
switch (event.type) {
//……
case SDLK_SPACE://按空格键触发暂停/恢复
toggle_pause(cur_stream);
break;
case SDL_QUIT:
case FF_QUIT_EVENT://?定义事件,?于出错时的主动退出
do_exit(cur_stream);
break;
}
}
event_loop 的主要代码是?个主循环,主循环内执?:
(1)refresh_loop_wait_event。
(2)处理SDL事件队列中的事件。?如按空格键可以触发暂停/恢复,关闭窗?可以触发do_exit销毁播放现场。
video的显示主要在 refresh_loop_wait_event :
static void refresh_loop_wait_event(VideoState *is, SDL_Event *event ) {
double remaining_time = 0.0; /* 休眠等待,remaining_time的计算在vide o_refresh中 */
/* 调?SDL_PeepEvents前先调?SDL_PumpEvents,将输?设备的事件抽到事件队 列中 */
SDL_PumpEvents();
/*
* SDL_PeepEvents check是否事件,?如?标移?显示区等
* 从事件队列中拿?个事件,放到event中,如果没有事件,则进?循环中
* SDL_PeekEvents?于读取事件,在调?该函数之前,必须调?SDL_PumpEvents 搜集键盘等事件
*/
while (!SDL_PeepEvents(event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, S DL_LASTEVENT)) {
if (!cursor_hidden && av_gettime_relative() - cursor_last_sh own > CURSOR_HIDE_DELAY) {
SDL_ShowCursor(0);
cursor_hidden = 1;
}
/*
* remaining_time就是?来进??视频同步的。
* 在video_refresh函数中,根据当前帧显示时刻(display time)和实际时刻 (actual time)
* 计算需要sleep的时间,保证帧按时显示
*/
if (remaining_time > 0.0)
//sleep控制画?输出的时机
av_usleep((int64_t)(remaining_time * 1000000.0));
remaining_time = REFRESH_RATE;
if (is->show_mode != SHOW_MODE_NONE && // 显示模式不等于SHOW_MO DE_NONE
(!is->paused // ?暂停状态
|| is->force_refresh) // ?强制刷新状态
) {
video_refresh(is, &remaining_time);
}
/* 从输?设备中搜集事件,推动这些事件进?事件队列,更新事件队列的状态,
* 不过它还有?个作?是进?视频?系统的设备状态更新,如果不调?这个函数,
* 所显示的视频会在?约10秒后丢失?彩。没有调?SDL_PumpEvents,将不会
* 有任何的输?设备事件进?队列,这种情况下,SDL就?法响应任何的键盘等硬件 输?。
*/
//响应更多事件
SDL_PumpEvents();
}
}
响应事件时,画面就有可能要暂停。
SDL_PeepEvents通过参数SDL_GETEVENT?阻塞查询队列中是否有事件。如果返回值不为0,表示有事件发?(或-1表示发?错误),那么函数就会返回,接着让event_loop处理事件;否则,就调?video_refresh显示画?,并通过输出参数remaining_time获取下?轮应当sleep的时间,以保持稳定的画?输出。
这?还有?个判断是否要调?video_refresh的前置条件。满?以下条件即可显示:
(1)显示模式不为SHOW_MODE_NONE(如果?件中只有audio,也会显示其波形或者频谱图等)。
(2)或者,当前没有被暂停。
(3)或者,当前设置了force_refresh,我们分析force_refresh置为1的场景:
a. video_refresh??帧该显示,这个是常规情况。
b. SDL_WINDOWEVENT_EXPOSED,窗?需要重新绘制。
c.SDL_MOUSEBUTTONDOWN && SDL_BUTTON_LEFT 连续?标左键点击2次显示窗?间隔?
于0.5秒,进?全屏或者恢复原始窗?播放。
d. SDLK_f,按f键进?全屏或者恢复原始窗?播放。
video_refresh:视频刷新显示
接下来,分析video显示的关键函数 video_refresh (经简化):
static void video_refresh(void *opaque, double *remaining_time)
{
VideoState *is = opaque;
double time;
Frame *sp, *sp2;
if (is->video_st) {
retry:
if (frame_queue_nb_remaining(&is->pictq) == 0) {// 帧队列是否为 空
// nothing to do, no picture to display in the queue
// 什么都不做,队列中没有图像可显示
} else {
double last_duration, duration, delay;
Frame *vp, *lastvp;
/* dequeue the picture */
lastvp = frame_queue_peek_last(&is->pictq); //读取上?帧
vp = frame_queue_peek(&is->pictq); // 读取待显示帧
if (vp->serial != is->videoq.serial) {
// 如果不是最新的播放序列,则将其出队列,以尽快读取最新序列的帧
frame_queue_next(&is->pictq);
goto retry;
}
if (lastvp->serial != vp->serial) // 新的播放序列重置当前时间
is->frame_timer = av_gettime_relative() / 1000000.0;
if (is->paused)
goto display;
/* compute nominal last_duration */
last_duration = vp_duration(is, lastvp, vp); //计算上?帧l astvp应显示的时?
delay = compute_target_delay(last_duration, is); //计算上 ?帧lastvp还要播放的时间
time= av_gettime_relative()/1000000.0;
//相当于做了一个流控
if (time < is->frame_timer + delay) { // 还没有到播放时间
*remaining_time = FFMIN(is->frame_timer + delay - ti me, *remaining_time);
goto display;
}
// ?少该到vp播放的时间了
is->frame_timer += delay;
//事件差值在某个范围,也认定是有效。
if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESH OLD_MAX)
is->frame_timer = time; //实时更新时间
SDL_LockMutex(is->pictq.mutex);
if (!isnan(vp->pts))
update_video_pts(is, vp->pts, vp->pos, vp->serial);
// 更新video的时钟
SDL_UnlockMutex(is->pictq.mutex);
//表示队列至少有2帧
if (frame_queue_nb_remaining(&is->pictq) > 1) {
//取出下一帧
Frame *nextvp = frame_queue_peek_next(&is->pictq);
//计算与上一帧时长
duration = vp_duration(is, vp, nextvp);
//如果不是video_master,并且可以drop
if(!is->step && (framedrop>0
|| (framedrop && get_master_sync_ty pe(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) {
//显示字幕
//……
} frame_queue_next(&is->pictq);
is->force_refresh = 1;
if (is->step && !is->paused)
stream_toggle_pause(is);
}
display:
/* 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;
}
video_refresh ?较?,即使已经经过了简化,去掉了次要分?。
函数中涉及到FrameQueue中的3个节点是lastvp, vp, nextvp,其中:
(1)vp这次将要显示的?标帧(待显示帧)。
(2)lastvp是已经显示了的帧(也是当前屏幕上看到的帧)。
(3)nextvp是下?次要显示的帧(排在vp后?)。
顺序就是nextvp(下下次准备显示)->vp(待显示)->lastvp(已经显示)
取出其前??帧与后??帧,是为了通过pts准确计算duration(这个值有可能会发生变化)。duration的计算通过函数 vp_duration完成:
// 计算上?帧需要持续的duration,这?有校正算法
static double vp_duration(VideoState *is, Frame *vp, Frame *nextvp) {
if (vp->serial == nextvp->serial) { // 同?播放序列,序列连续的情况下
double duration = nextvp->pts - vp->pts;
if (isnan(duration) // duration 数值异常
|| duration <= 0 // pts值没有递增时
|| duration > is->max_frame_duration // 超过了最?帧 范围
) { // 异常情况,
// 1. 异常情况??之前?队列的时候计算的帧间隔(主要根据帧率去计算)
return vp->duration; /* 异常时以帧时间为基准(1秒/帧率) */
}
else { // 2. 相邻pts'
return duration; //使?两帧pts差值计算duration,?般情况下也是 ?的这个分?
}
} else { // 不同播放序列, 序列不连续则返回0
return 0.0;
}
}
video_refresh 的主要流程如下:
先来看下上?流程图中的主流程——即中间?列框图。从框图进?步抽象, video_refresh 的主体流程分为3个步骤:
(1)取出上?帧lastvp和待显示的帧vip。
(2)计算上?帧lastvp应显示的时?,判断是否继续显示上?帧。
(3)估算当前帧应显示的时?,判断是否要丢帧,已过时就要丢帧。未过时,就要显示。
(4)调?video_display进?显示。
整体一个渲染逻辑就是,video_display 会调? frame_queue_peek_last 获取上次显示的frame(lastvp),并显示。所以在 video_refresh 中如果流程直接?到 video_display 就会显示 lastvp (需要注意的是在此时如果不是触发了force_refresh,则不会去重新取lastvp进?重新渲染),如果先调? frame_queue_next再调? video_display ,那么就会显示 vp。
下?我们具体分析这3个步骤,并和流程图与代码进?对应阅读。
计算上?帧应显示的时?,判断是否继续显示上?帧
?先检查pictq是否为空(调? frame_queue_nb_remaining 判断队列中是否有未显示的帧),如果为空,则调? video_display (显示上?帧)。
在进?步准确计算上?帧应显示时间前,需要先判断 frame_queue_peek 获取的 vp 是否是最新序列——即 if (vp->serial != is->videoq.serial) ,如果条件成?,说明发?过seek等操作,流不连续,应该抛弃lastvp。故调? frame_queue_next 抛弃lastvp后,返回流程开头重试下?轮。
接下来可以计算准确的 lastvp 应显示时?了。计算应显示时间的代码是:
last_duration = vp_duration(is, lastvp, vp);
delay = compute_target_delay(last_duration, is);
直观理解,主要基于 vp_duration 计算两帧pts差,即帧持续时间,即可。但是,如果考虑到同步,?如视频同步到?频,则还需要考虑当前与主时钟的差距,进?决定是重复上?帧还是丢帧,还是正常显示下?帧(待显示帧vp)。这?只需要理解通过以上两步就可以计算出准确的上?帧应显示时?了。最后,根据上?帧应显示时?(delay变量),确定是否继续显示上?帧:
time= av_gettime_relative()/1000000.0;//获取当前系统时间(单位秒)
if (time < is->frame_timer + delay)
{
*remaining_time = FFMIN(is->frame_timer + delay - time, *remainin g_time);
goto display;
}
frame_timer 可以理解为帧显示时刻,对于更新前,可以理解为上?帧的显示时刻。对于更新后,可以理解为当前帧显示时刻。time < is->frame_timer + delay (上一帧显示还没结束),如果当前系统时刻还未到达上?帧的结束时刻,那么还应该继续显示上?帧。这里的delay就是如图下的last_duration。
估算当前帧应显示的时?,判断是否要丢帧
这个步骤执?前,还需要?点准备?作:更新frame_timer和更新vidclk。
is->frame_timer += delay;//更新frame_timer,现在表示vp的显示时刻
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);//更新vidclk
SDL_UnlockMutex(is->pictq.mutex);
接下来就可以判断是否要丢帧了:
if (frame_queue_nb_remaining(&is->pictq) > 1) {//有nextvp才会检测是否该 丢帧
Frame *nextvp = frame_queue_peek_next(&is->pictq);
duration = vp_duration(is, vp, nextvp);
if(!is->step // ?逐帧模式才检测是否需要丢帧 is->step==1 为逐帧播放 ,逐帧播放最好别丢帧。
&& (framedrop>0 || // cpu解帧过慢
(framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_M ASTER)) // ?视频同步?式
&& time > is->frame_timer + duration // 确实落后了?帧数据
) {
printf("%s(%d) dif:%lfs, drop frame\n", __FUNCTION__, __LINE __,
(is->frame_timer + duration) - time); 11 is->frame_drops_late++; // 统计丢帧情况
// 这?实现真正的丢帧
frame_queue_next(&is->pictq);
goto retry;
}
}
丢帧的代码也?较简单,只需要 frame_queue_next ,然后retry。丢帧的前提条件是frame_queue_nb_remaining(&is->pictq) > 1,即是要有nextvp,且需要同时满?以下条件:
(1)不处于step状态。换?之,如果当前是step状态,不会触发丢帧逻辑。(step?于pause状态下进?seek操作时,于seek操作结束后显示seek后的?帧画?,?于直观体现seek?效了)。
(2)启?framedrop机制,或?AV_SYNC_VIDEO_MASTER(不是以video为同步)。
(3)时间已?于frame_timer + duration,已经超过该帧该显示的时?(超过一定的阈值)。
调?video_display进?显示
如果既不需要重复上?帧,也不需要抛弃当前帧,那么就可以安?显示当前帧了。之前有顺带提过video_display 中显示的是 frame_queue_peek_last ,所以需要先调? frame_queue_next ,移动pictq内的指针,将vp变成shown,确保 frame_queue_peek_last 取到的是vp。
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);
}
如果初步了解解SDL接?的使?,我们直接看 video_image_display :
static void video_image_display(VideoState *is)
{
Frame *vp;
Frame *sp = NULL;
SDL_Rect rect;
vp = frame_queue_peek_last(&is->pictq);//取要显示的视频帧
if (is->subtitle_st) {
//字幕显示逻辑
}
//将帧宽?按照sar最?适配到窗?
calculate_display_rect(&rect, is->xleft, is->ytop, is->width, is ->height, vp->width, vp->height, vp->sar);
if (!vp->uploaded) {//如果是重复显示上?帧,那么uploaded就是1
if (upload_texture(&is->vid_texture, vp->frame, &is->img_con vert_ctx) < 0)
return;
vp->uploaded = 1; // 已经拷?过?次到vid_texture则不需要再拷?
vp->flip_v = vp->frame->linesize[0] < 0; // = 1垂直翻转, = 0正 常播放
}
SDL_RenderCopyEx(renderer, is->vid_texture, NULL, &rect, 0, NULL , vp->flip_v ? SDL_FLIP_VERTICAL : 0);
if (sp) {
//字幕显示逻辑
}
}
如果了解了SDL的显示, video_image_display 的逻辑不算复杂,即先 frame_queue_peek_last取要显示帧,然后 upload_texture 更新到SDL_Texture,最后通过 SDL_RenderCopyEx 拷?纹理给render显示。
最后,了解下 upload_texture 具体是如何将AVFormat的图像数据传给sdl的纹理:
static int upload_texture(SDL_Texture **tex, AVFrame *frame, struct SwsContext **img_convert_ctx) {
int ret = 0;
Uint32 sdl_pix_fmt;
SDL_BlendMode sdl_blendmode;
// 根据frame中的图像格式(FFmpeg像素格式),获取对应的SDL像素格式
get_sdl_pix_fmt_and_blendmode(frame->format, &sdl_pix_fmt, &sdl_ blendmode);
// 参数tex实际是&is->vid_texture,此处根据得到的SDL像素格式,为&is->vid_ texture
if (realloc_texture(tex, sdl_pix_fmt == SDL_PIXELFORMAT_UNKNOWN
? SDL_PIXELFORMAT_ARGB8888 : sdl_pix_fmt, frame->width, frame->heigh t, sdl_blendmode, 0) < 0)
return -1;
switch (sdl_pix_fmt) {
/*
frame格式是SDL不?持的格式,则需要进?图像格式转换,
转换为?标格式AV_ PIX_FMT_BGRA(因为这个转换导致效率可能不如libyuv和shader)
,对应SDL_PIXELFORMAT_BGRA32
*/
case SDL_PIXELFORMAT_UNKNOWN:
/* This should only happen if we are not using avfilte r... */
*img_convert_ctx = sws_getCachedContext(*img_convert_ctx ,
frame->width, frame->height, frame->format, frame->w idth, frame->height, AV_PIX_FMT_BGRA, sws_flags, NULL, NULL, NULL);
(*img_convert_ctx != NULL) {
uint8_t *pixels[4];
int pitch[4];
if (!SDL_LockTexture(*tex, NULL, (void **)pixels, pi tch)) {
sws_scale(*img_convert_ctx, (const uint8_t * con st *)frame->data, frame->linesize,
0, frame->height, pixels, pitch);
SDL_UnlockTexture(*tex);
}
} else {
av_log(NULL, AV_LOG_FATAL, "Cannot initialize the co nversion context\n");
ret = -1;
}
break;
// frame格式对应SDL_PIXELFORMAT_IYUV,不?进?图像格式转换,调?SDL _UpdateYUVTexture()更新SDL texture
case SDL_PIXELFORMAT_IYUV:
if (frame->linesize[0] > 0 && frame->linesize[1] > 0 && frame->linesize[2] > 0) {
ret = SDL_UpdateYUVTexture(*tex, NULL, frame->data[0 ], frame->linesize[0],
frame->data[1 ], frame->linesize[1],frame->data[2 ], frame->linesize[2]);
} else if (frame->linesize[0] < 0 && frame->linesize[1]< 0 && frame->linesize[2] < 0) {
ret = SDL_UpdateYUVTexture(*tex, NULL, frame->data[0 ] + frame->linesize[0] * (frame->height - 1), -fr ame->linesize[0],
frame->data[1 ] + frame->linesize[1] * (AV_CEIL_RSHIFT(frame->height, 1) - 1), -fr ame->linesize[1],
frame->data[2 ] + frame->linesize[2] * (AV_CEIL_RSHIFT(frame->height, 1) - 1), -fr ame->linesize[2]);
} else {
av_log(NULL, AV_LOG_ERROR, "Mixed negative and posit ive linesizes are not supported.\n");
return -1;
}
break;
// frame格式对应其他SDL像素格式,不?进?图像格式转换,调?SDL_UpdateT exture()更新SDL texture
default:
if (frame->linesize[0] < 0) {
ret = SDL_UpdateTexture(*tex, NULL, frame->data[0] + frame->linesize[0] * (frame->height - 1), -frame->linesize[0]);
} else {
ret = SDL_UpdateTexture(*tex, NULL, frame->data[0], frame->linesize[0]);
}
break;
}
return ret;
}
frame中的像素格式是FFmpeg中定义的像素格式,FFmpeg中定义的很多像素格式和SDL中定义的很多像素格式其实是同?种格式,只名称不同?已。根据frame中的像素格式与SDL?持的像素格式的匹配情况,upload_texture()处理三种类型,对应switch语句的三个分?:
(1)如果frame图像格式对应SDL_PIXELFORMAT_IYUV格式,不进?图像格式转换,使?SDL_UpdateYUVTexture() 将图像数据更新到 &is->vid_texture。
(2)如果frame图像格式对应其他被SDL?持的格式(诸如AV_PIX_FMT_RGB32),也不进?图像格式转换,使? SDL_UpdateTexture() 将图像数据更新到 &is->vid_texture。
(3)如果frame图像格式不被SDL?持(即对应SDL_PIXELFORMAT_UNKNOWN),则需要进?图像格式转换。
根据映射表获取frame对应SDL中的像素格式
get_sdl_pix_fmt_and_blendmode()
这个函数的作?,获取输?参数 format (FFmpeg像素格式)在SDL中的像素格式,取到的SDL像素格式存在输出参数 sdl_pix_fmt 中。
static void get_sdl_pix_fmt_and_blendmode(int format, Uint32 *sdl_pi x_fmt, SDL_BlendMode *sdl_blendmode)
{
int i;
*sdl_blendmode = SDL_BLENDMODE_NONE;
*sdl_pix_fmt = SDL_PIXELFORMAT_UNKNOWN;
if (format == AV_PIX_FMT_RGB32 ||
format == AV_PIX_FMT_RGB32_1 ||
format == AV_PIX_FMT_BGR32 ||
format == AV_PIX_FMT_BGR32_1)
*sdl_blendmode = SDL_BLENDMODE_BLEND;
for (i = 0; i < FF_ARRAY_ELEMS(sdl_texture_format_map) - 1; i++) {
if (format == sdl_texture_format_map[i].format) {
*sdl_pix_fmt = sdl_texture_format_map[i].texture_fmt;
return;
}
}
}
在ffplay.c中定义了?个表 sdl_texture_format_map[] ,其中定义了FFmpeg中?些像素格式与SDL像素格式的映射关系,如下:
static const struct TextureFormatEntry {
enum AVPixelFormat format;
int texture_fmt;
} sdl_texture_format_map[] = {
{ AV_PIX_FMT_RGB8, SDL_PIXELFORMAT_RGB332 },
{ AV_PIX_FMT_RGB444, SDL_PIXELFORMAT_RGB444 },
{ AV_PIX_FMT_RGB555, SDL_PIXELFORMAT_RGB555 },
{ AV_PIX_FMT_BGR555, SDL_PIXELFORMAT_BGR555 },
{ AV_PIX_FMT_RGB565, SDL_PIXELFORMAT_RGB565 },
{ AV_PIX_FMT_BGR565, SDL_PIXELFORMAT_BGR565 },
{ AV_PIX_FMT_RGB24, SDL_PIXELFORMAT_RGB24 },
{ AV_PIX_FMT_BGR24, SDL_PIXELFORMAT_BGR24 },
{ AV_PIX_FMT_0RGB32, SDL_PIXELFORMAT_RGB888 },
{ AV_PIX_FMT_0BGR32, SDL_PIXELFORMAT_BGR888 },
{ AV_PIX_FMT_NE(RGB0, 0BGR), SDL_PIXELFORMAT_RGBX8888 },
{ AV_PIX_FMT_NE(BGR0, 0RGB), SDL_PIXELFORMAT_BGRX8888 },
{ AV_PIX_FMT_RGB32, SDL_PIXELFORMAT_ARGB8888 },
{ AV_PIX_FMT_RGB32_1, SDL_PIXELFORMAT_RGBA8888 },
{ AV_PIX_FMT_BGR32, SDL_PIXELFORMAT_ABGR8888 },
{ AV_PIX_FMT_BGR32_1, SDL_PIXELFORMAT_BGRA8888 },
{ AV_PIX_FMT_YUV420P, SDL_PIXELFORMAT_IYUV },
{ AV_PIX_FMT_YUYV422, SDL_PIXELFORMAT_YUY2 },
{ AV_PIX_FMT_UYVY422, SDL_PIXELFORMAT_UYVY },
{ AV_PIX_FMT_NONE, SDL_PIXELFORMAT_UNKNOWN },
};
可以看到,除了最后?项,其他格式的图像送给SDL是可以直接显示的,不必进?图像转换。关于这些像素格式的含义,可参考附录:?彩空间与像素格式。
重新分配vid_texture
realloc_texture()
根据新得到的SDL像素格式,为 &is->vid_texture 重新分配空间,如下所示,先SDL_DestroyTexture() 销毁,再 SDL_CreateTexture() 创建。
static int realloc_texture(SDL_Texture **texture, Uint32 new_format, int new_width, int new_height, SDL_BlendMode blendmode, int init_tex ture)
{
Uint32 format;
int access, w, h;
if (!*texture || SDL_QueryTexture(*texture, &format, &access, &w , &h) < 0 ||
new_width != w || new_height != h || new_format != form at) {
void *pixels;
int pitch;
if (*texture)
SDL_DestroyTexture(*texture);
if (!(*texture = SDL_CreateTexture(renderer, new_format, SDL _TEXTUREACCESS_STREAMING, new_width, new_height)))
return -1;
if (SDL_SetTextureBlendMode(*texture, blendmode) < 0)
return -1;
if (init_texture) {
if (SDL_LockTexture(*texture, NULL, &pixels, &pitch) < 0 )
return -1;
memset(pixels, 0, pitch * new_height);
SDL_UnlockTexture(*texture);
}
av_log(NULL, AV_LOG_VERBOSE, "Created %dx%d texture with % s.\n", new_width, new_height, SDL_GetPixelFormatName(new_format));
}
return 0;
}
什么情况下realloc_texture?
(1)?于显示的texture 还没有分配。
(2)SDL_QueryTexture?效。
(3)?前texture的width,height、format和新要显示的Frame不?致。
从上分析可以看出,窗???的变化不?,让realloc_texture重新SDL_CreateTexture。
格式转换-复?或新分配?个SwsContext
sws_getCachedContext()
*img_convert_ctx = sws_getCachedContext(*img_convert_ctx,
frame->width, frame->height,
frame->format, frame->width, frame-> height,
AV_PIX_FMT_BGRA, sws_flags, NULL, NULL, NULL);
检查输?参数,第?个输?参数 *img_convert_ctx 对应形参 struct SwsContext *context 。如果context是NULL,调? sws_getContext() 重新获取?个context。
如果context不是NULL,检查其他项输?参数是否和context中存储的各参数?样,若不?样,则先释放context再按照新的输?参数重新分配?个context。若?样,直接使?现有的context。
图像显示
texture对应?帧待显示的图像数据,得到texture后,执?如下步骤即可显示:
SDL_RenderClear(); // 使?特定颜?清空当前渲染?标
SDL_RenderCopy(); // 使?部分图像数据(texture)更新当前渲 染?标
SDL_RenderCopyEx(); // 和SDL_RenderCopy类似,但?持旋转
SDL_RenderPresent(sdl_renderer); // 执?渲染,更新屏幕显示
2.图像格式转换
FFmpeg中的 sws_scale() 函数主要是?来做视频像素格式和分辨率的转换,其优势在于:可以在同?个函数?实现:1.图像?彩空间转换, 2:分辨率缩放,3:前后图像滤波处理。不?之处在于:效率相对较低,不如libyuv或shader,其关联的函数主要有:
(1)sws_getContext:分配和返回?个SwsContext,需要传?输?参数和输出参数;
(2)sws_getCachedContext:检查传?的上下?是否可以?,如果不可?则重新分配?个,如果可?则返回传?的,这个是自动分配。
(3)sws_freeContext:释放SwsContext结构体。
(4)sws_scale:转换?帧图像。
函数说明
/** 2 * Allocate and return an SwsContext. You need it to perform
3 * scaling/conversion operations using sws_scale().
4 *
5 * @param srcW the width of the source image
6 * @param srcH the height of the source image
7 * @param srcFormat the source image format
8 * @param dstW the width of the destination image
9 * @param dstH the height of the destination image
10 * @param dstFormat the destination image format
* @param flags specify which algorithm and options to use for resca ling
12 * @param param extra parameters to tune the used scaler
13 * For SWS_BICUBIC param[0] and [1] tune the shape of t he basis
14 * function, param[0] tunes f(1) and param[1] f′(1)
15 * For SWS_GAUSS param[0] tunes the exponent and thus c utoff
16 * frequency
17 * For SWS_LANCZOS param[0] tunes the width of the wind ow function
18 * @return a pointer to an allocated context, or NULL in case of err or
19 * @note this function is to be removed after a saner alternative is
20 * written
21 */
struct SwsContext *sws_getContext(int srcW, int srcH, enum AVPixelFo rmat srcFormat,
int dstW, int dstH, enum AVPixelFo rmat dstFormat,
int flags, SwsFilter *srcFilter,
SwsFilter *dstFilter, const double *param);
(1)srcW,srcH, srcFormat, 原始数据的宽?和原始像素格式(YUV420)。
(2)dstW,dstH,dstFormat; ?标宽,?标?,?标的像素格式(这?的宽?可能是?机屏幕分辨率,RGBA8888),这?不仅仅包含了尺?的转换,还有像素格式的转换。
(3)flag 提供了?系列的算法,快速线性,差值,矩阵,不同的算法性能也不同,快速线性算法性能相对较?。只针对尺?的变换。对像素格式转换?此问题。不同算法的效率?《10.3 ffmpeg中的sws_scale算法性能测试》?节。
#define SWS_FAST_BILINEAR 1
#define SWS_BILINEAR 2
#define SWS_BICUBIC 4
#define SWS_X 8
#define SWS_POINT 0x10
#define SWS_AREA 0x20
#define SWS_BICUBLIN 0x40
(4)后?还有两个参数是做过滤器?的,?般?不到,传NULL,最后?个参数是跟flag算法相关,也可以传NULL。
sws_getCachedContext
/** * Check if context can be reused, otherwise reallocate a new one.
* * If context is NULL, just calls sws_getContext() to get a new
* context. Otherwise, checks if the parameters are the ones already
* saved in context. If that is the case, returns the current
* context. Otherwise, frees context and gets a new context with
* the new parameters.
* * Be warned that srcFilter and dstFilter are not checked, they
* are assumed to remain the same.
*/
struct SwsContext *sws_getCachedContext(struct SwsContext *context,
14 int srcW, int srcH, enum AVP ixelFormat srcFormat,
15 int dstW, int dstH, enum AVP ixelFormat dstFormat,
16 int flags, SwsFilter *srcFil ter,
17 SwsFilter *dstFilter, const double *param);
int srcW, /* 输?图像的宽度 */
int srcH, /* 输?图像的宽度 */
enum AVPixelFormat srcFormat, /* 输?图像的像素格式 */
int dstW, /* 输出图像的宽度 */
int dstH, /* 输出图像的?度 */
enum AVPixelFormat dstFormat, /* 输出图像的像素格式 */
int flags,/* 选择缩放算法(只有当输?输出图像??不同时有效),?般选择SWS_FAST_BILINEAR */
SwsFilter *srcFilter, /* 输?图像的滤波器信息, 若不需要传NULL */
SwsFilter *dstFilter, /* 输出图像的滤波器信息, 若不需要传NULL */
const double *param /* 特定缩放算法需要的参数(?),默认为NULL */
getCachedContext和sws_getContext的区别是就是多了struct SwsContext *context的传?。
struct SwsContext *img_convert_ctx = NULL;
img_convert_ctx = sws_getCachedContext(img_convert_ctx,
frame->width, frame->height, frame->forma t,
frame->width, frame->height, AV_PIX_FMT_B GRA,
sws_flags, NULL, NULL, NULL);
sws_scale
/** * Scale the image slice in srcSlice and put the resulting scaled
* slice in the image in dst. A slice is a sequence of consecutive
* rows in an image.
*
* Slices have to be provided in sequential order, either in
* top-bottom or bottom-top order. If slices are provided in
* non-sequential order the behavior of the function is undefined.
*
* @param c the scaling context previously created with
* sws_getContext()
* @param srcSlice the array containing the pointers to the planes of
* the source slice
* @param srcStride the array containing the strides for each plane of
* the source image
* @param srcSliceY the position in the source image of the slice to
* process, that is the number (counted starting fr om
* zero) in the image of the first row of the slice
* @param srcSliceH the height of the source slice, that is the numb er
* of rows in the slice
* @param dst the array containing the pointers to the planes of
* the destination image
* @param dstStride the array containing the strides for each plane of
* the destination image
* @return the height of the output slice
*/
int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[],
const int srcStride[], int srcSliceY, int srcSliceH,
uint8_t *const dst[], const int dstStride[]);
(1)参数 SwsContext *c, 转换格式的上下?。也就是 sws_getContext 函数返回的结果。
(2)参数 const uint8_t *const srcSlice[], 输?图像的每个颜?通道的数据指针。其实就是解码后的AVFrame中的data[]数组。因为不同像素的存储格式不同,所以srcSlice[]维数也有可能不同。
以YUV420P为例,它是planar格式,它的内存中的排布如下:
YYYYYYYY UUUU VVVV
使?FFmpeg解码后存储在AVFrame的data[]数组中时:
data[0]——-Y分量, Y1, Y2, Y3, Y4, Y5, Y6, Y7, Y8……
data[1]——-U分量, U1, U2, U3, U4……
data[2]——-V分量, V1, V2, V3, V4……
linesize[]数组中保存的是对应通道的数据宽度,这些都是通过计算:
linesize[0]——-Y分量的宽度。
linesize[1]——-U分量的宽度。
inesize[2]——-V分量的宽度。
RGB24,它是packed格式,它在data[]数组中则只有?维,它在存储?式如下:
data[0]: R1, G1, B1, R2, G2, B2, R3, G3, B3, R4, G4, B4……
特别注意,linesize[0]的值并不?定等于图?的宽度,有时候为了对?各解码器的内存,提高CPU访问速度,实际尺?会?于图?的宽度,这点在我们编程时(?如OpengGL硬件转换/渲染)要特别注意,否则解码出来的图像会异常。
(3)参数const int srcStride[],输?图像的每个颜?通道的跨度。.也就是每个通道的?字节数,对应的是解码后的AVFrame中的linesize[]数组。根据它可以确?下??的起始位置,不过stride和width不?定相同,这是因为有以下2点原因:
第一,由于数据帧存储的对?,有可能会向每?后?增加?些填充字节这样 stride = width + N。
第二,packet?彩空间下,每个像素?个通道数据混合在?起,例如RGB24,每个像素3字节连续存放,因此下??的位置需要跳过3*width字节。
(4)参数int srcSliceY, int srcSliceH,定义在输?图像上处理区域,srcSliceY是起始位置,srcSliceH是处理多少?。如果srcSliceY=0,srcSliceH=height,表示?次性处理完整个图像。这种设置是为了多线程并?,例如可以创建两个线程,第?个线程处理 [0, h/2-1]?,第?个线程处理 [h/2, h-1]?。并?处理加快速度。
(5)参数uint8_t *const dst[], const int dstStride[]定义输出图像信息,输出的每个颜?通道数据指针,每个颜?通道?字节数(也就是宽度),对应上面的输入和输出。
sws_freeContext:释放SwsContext。
/**
* Free the swscaler context swsContext.
* If swsContext is NULL, then does nothing.
*/
void sws_freeContext(struct SwsContext *swsContext);
具体转换
if (*img_convert_ctx != NULL) {
uint8_t *pixels[4];
int pitch[4];
if (!SDL_LockTexture(*tex, NULL, (void **)pixels, pitch)) {
sws_scale(*img_convert_ctx, (const uint8_t * const *)frame->d ata, frame->linesize,
0, frame->height, pixels, pitch);
SDL_UnlockTexture(*tex);
}
}
上述代码有三个步骤:
(1)SDL_LockTexture() 锁定texture中的?个rect(此处是锁定整个texture),锁定区具有只写属性,?于更新图像数据。 pixels 指向锁定区。
(2)sws_scale() 进?图像格式转换,转换后的数据写? pixels 指定的区域。 pixels 包含4个指针,指向?组图像plane。
(3)SDL_UnlockTexture() 将锁定的区域解锁,将改变的数据更新到视频缓冲区中。上述三步完成后,texture中已包含经过格式转换后新的图像数据。
由上分析可以得出texture的缓存区,我们可以直接使?,避免?次拷?。
3.ffmpeg中的sws_scale算法性能测试
这里的性能测试仅供参考,根据不同的配置环境,效果可能些许不一样。
?先,将?幅1920*1080的?景图像,缩放为400*300的24位RGB,下?的帧率,是指每秒钟缩放并渲染的次数。(经过我的测试,渲染的时间可以忽略不计,主要时间还是耗费在缩放算法上。)
总评,以上各种算法,图?缩?之后的效果似乎都不错。如果不是对?着看,?乎看不出缩放效果的好坏。上?所说的清晰(锐利)与平滑(模糊),是?种客观感受,并?清晰就?平滑好,也?平滑?清晰好。其中的Point算法,效率之?,让我震撼,但效果却不差。此外,我对?过使?CImage的绘制时缩放,其帧率可到190,但效果惨不忍睹,颜?严重失真。
第?个试验,将?幅1024*768的?景图像,放?到1920*1080,并进?渲染(此时的渲染时间,虽然不是忽略不计,但不超过5ms的渲染时间,不影响下?结论的相对准确性)。
总评,Point算法有明显锯?,Area算法锯?要不明显?点,其余各种算法,?眼看来?明显差异。此外,使?CImage进?渲染时缩放,帧率可达105,效果与Point相似。
方案选型
个?建议,如果对图像的缩放,要追求?效,?如说是视频图像的处理,在不明确是放?还是缩?时,直接使?SWS_FAST_BILINEAR算法即可。如果明确是要缩?并显示,建议使?Point算法,如果是明确要放?并显示,其实使?CImage的Strech更?效。当然,如果不计速度追求画?质量。在上?的算法中,选择帧率最低的那个即可,画?效果?般是最好的。
不过总的来说,ffmpeg的scale算法,速度还是?常快的,毕竟我选择的素材可是?清的图?。(本想顺便上传?下图?,但各组图?差异其实?常?,恐怕上传的时候格式转换所造成的图像细节丢失,已经超过了各图?本身的细节差异,因此此处不上传图?了。)
注意:试验了?下OpenCV的Resize效率,和上?相同的情况下,OpenCV在上?的放?试验中,每秒可以进?52次,缩?试验中,每秒可以进?458次。放大和缩小是不一样的结果,放大处理,更消耗资源。
FFmpeg使?不同sws_scale()缩放算法的命令示例(bilinear,bicubic,neighbor):
ffmpeg -s 480x272 -pix_fmt yuv420p -i src01_480x272.yuv -s 1280x720 - sws_flags bilinear -pix_fmt yuv420p src01_bilinear_1280x720.yuv
ffmpeg -s 480x272 -pix_fmt yuv420p -i src01_480x272.yuv -s 1280x720 - sws_flags bicubic -pix_fmt yuv420p src01_bicubic_1280x720.yuv
ffmpeg -s 480x272 -pix_fmt yuv420p -i src01_480x272.yuv -s 1280x720 - sws_flags neighbor -pix_fmt yuv420p src01_neighbor_1280x720.yuv
这篇文章就分析到这里,欢迎关注,点赞,转发,收藏。
相关推荐
- 前端入门——css 网格轨道详细介绍
-
上篇前端入门——cssGrid网格基础知识整体大概介绍了cssgrid的基本概念及使用方法,本文将介绍创建网格容器时会发生什么?以及在网格容器上使用行、列属性如何定位元素。在本文中,将介绍:...
- Islands Architecture(孤岛架构)在携程新版首页的实践
-
一、项目背景2022,携程PC版首页终于迎来了首次改版,完成了用户体验与技术栈的全面升级。作为与用户连接的重要入口,旧版PC首页已经陪伴携程走过了22年,承担着重要使命的同时,也遇到了很多问题:维护/...
- HTML中script标签中的那些属性
-
HTML中的<script>标签详解在HTML中,<script>标签用于包含或引用JavaScript代码,是前端开发中不可或缺的一部分。通过合理使用<scrip...
- CSS 中各种居中你真的玩明白了么
-
页面布局中最常见的需求就是元素或者文字居中了,但是根据场景的不同,居中也有简单到复杂各种不同的实现方式,本篇就带大家一起了解下,各种场景下,该如何使用CSS实现居中前言页面布局中最常见的需求就是元...
- CSS样式更改——列表、表格和轮廓
-
上篇文章主要介绍了CSS样式更改篇中的字体设置Font&边框Border设置,这篇文章分享列表、表格和轮廓,一起来看看吧。1.列表List1).列表的类型<ulstyle='list-...
- 一文吃透 CSS Flex 布局
-
原文链接:一文吃透CSSFlex布局教学游戏这里有两个小游戏,可用来练习flex布局。塔防游戏送小青蛙回家Flexbox概述Flexbox布局也叫Flex布局,弹性盒子布局。它决定了...
- css实现多行文本的展开收起
-
背景在我们写需求时可能会遇到类似于这样的多行文本展开与收起的场景:那么,如何通过纯css实现这样的效果呢?实现的难点(1)位于多行文本右下角的展开收起按钮。(2)展开和收起两种状态的切换。(3)文本...
- css 垂直居中的几种实现方式
-
前言设计是带有主观色彩的,同样网页设计中的css一样让人摸不头脑。网上列举的实现方式一大把,或许在这里你都看到过,但既然来到这里我希望这篇能让你看有所收获,毕竟这也是前端面试的基础。实现方式备注:...
- WordPress固定链接设置
-
WordPress设置里的最后一项就是固定链接设置,固定链接设置是决定WordPress文章及静态页面URL的重要步骤,从站点的SEO角度来讲也是。固定链接设置决定网站URL,当页面数少的时候,可以一...
- 面试发愁!吃透 20 道 CSS 核心题,大厂 Offer 轻松拿
-
前端小伙伴们,是不是一想到面试里的CSS布局题就发愁?写代码时布局总是对不齐,面试官追问兼容性就卡壳,想跳槽却总被“多列等高”“响应式布局”这些问题难住——别担心!从今天起,咱们每天拆解一...
- 3种CSS清除浮动的方法
-
今天这篇文章给大家介绍3种CSS清除浮动的方法。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。首先,这里就不讲为什么我们要清楚浮动,反正不清除浮动事多多。下面我就讲3种常用清除浮动的...
- 2025 年 CSS 终于要支持强大的自定义函数了?
-
大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!1.什么是CSS自定义属性CSS自...
- css3属性(transform)的一个css3动画小应用
-
闲言碎语不多讲,咱们说说css3的transform属性:先上效果:效果说明:当鼠标移到a标签的时候,从右上角滑出二维码。实现方法:HTML代码如下:需要说明的一点是,a链接的跳转需要用javasc...
- CSS基础知识(七)CSS背景
-
一、CSS背景属性1.背景颜色(background-color)属性值:transparent(透明的)或color(颜色)2.背景图片(background-image)属性值:none(没有)...
- CSS 水平居中方式二
-
<divid="parent"><!--定义子级元素--><divid="child">居中布局</div>...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- maven镜像 (69)
- undefined reference to (60)
- zip格式 (63)
- oracle over (62)
- date_format函数用法 (67)
- 在线代理服务器 (60)
- shell 字符串比较 (74)
- x509证书 (61)
- localhost (65)
- java.awt.headless (66)
- syn_sent (64)
- settings.xml (59)
- 弹出窗口 (56)
- applicationcontextaware (72)
- my.cnf (73)
- httpsession (62)
- pkcs7 (62)
- session cookie (63)
- java 生成uuid (58)
- could not initialize class (58)
- beanpropertyrowmapper (58)
- word空格下划线不显示 (73)
- jar文件 (60)
- jsp内置对象 (58)
- makefile编写规则 (58)