修改ffplay,添加字幕解析功能
结合上上周做出来的assrender
https://blog.csdn.net/qq_40212938/article/details/107754243
这里我用的是vs2015编译的,因为msvc有带辅助编译功能。能够快速c跟c++混编(所谓混编是在双方语法有差异的情况下互相兼容,而不是把c编译成c++).成品已经push到了gitee上。地址是https://gitee.com/heweisheng/AssRender。这只是一个简单的demo。没有进行近一步的封装调整,目前我已经把思路运用到项目中,提供一个可以参考的版本。
首先添加了一个类,用于管理ass字幕解析跟生产字幕图片数据。AssPaintRender。然后开放一个c语言跟c++的接口,当然,c++就不需要通过这个接口访问了。主要是为了给c使用。我才开了这个接口。api.h跟api.cpp。这里由于我是用msvc开发的。所以我也没用使用传统的定义手段,比如#ifndef H #define H啥的。将一切都准备好了,去找ffplay的dll组装版本(由于时间久远,我也找不到是哪个了,如果需要更新,请自己去找)。
把其中的ffplay.c、cmdutils.h、cmdutils.c、config.h拉到我们准备好的项目中。我已经将内容整理好。只需要编译即可
首先添加#include “api.h”,
接入我们写好的模块
然后修改ffplay的subtitle_thread
static int subtitle_thread(void *arg)
{
VideoState *is = arg;
assrender *render=NULL;
Frame *sp;
int got_subtitle;
double pts;
for (;;) {
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||(sp->sub.rects&&sp->sub.rects[0]->ass))) {
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;
if (sp->sub.format != 0)
{
if (!render)
{
alloc_assrender(&render);
if (!sp->width || !sp->height)
{
sp->width = is->viddec.avctx->width;
sp->height = is->viddec.avctx->height;
}
initasspaintrender(render, is->subdec.avctx, sp->width, sp->height);
}
if (render)
{
addsubtitlestotrack(render, &sp->sub);
}
}
/* now we can update the picture count */
frame_queue_push(&is->subpq);
} else if (got_subtitle) {
avsubtitle_free(&sp->sub);
}
}
if(render)
free_assrender(&render);
return 0;
}
然后修改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) {
if (frame_queue_nb_remaining(&is->subpq) > 0) {
sp = frame_queue_peek(&is->subpq);
if (vp->pts >= sp->pts + ((float) sp->sub.start_display_time / 1000)) {
if (!sp->uploaded) {
uint8_t* pixels[4];
int pitch[4];
int i;
if (!sp->width || !sp->height) {
sp->width = vp->width;
sp->height = vp->height;
}
if (realloc_texture(&is->sub_texture, SDL_PIXELFORMAT_ARGB8888, sp->width, sp->height, SDL_BLENDMODE_BLEND, 1) < 0)
return;
for (i = 0; i < sp->sub.num_rects; i++) {
AVSubtitleRect *sub_rect = sp->sub.rects[i];
sub_rect->x = av_clip(sub_rect->x, 0, sp->width );
sub_rect->y = av_clip(sub_rect->y, 0, sp->height);
sub_rect->w = av_clip(sub_rect->w, 0, sp->width - sub_rect->x);
sub_rect->h = av_clip(sub_rect->h, 0, sp->height - sub_rect->y);
if (!sub_rect->ass)
{
is->sub_convert_ctx = sws_getCachedContext(is->sub_convert_ctx,
sub_rect->w, sub_rect->h, AV_PIX_FMT_PAL8,
sub_rect->w, sub_rect->h, AV_PIX_FMT_BGRA,
0, NULL, NULL, NULL);
if (!is->sub_convert_ctx) {
av_log(NULL, AV_LOG_FATAL, "Cannot initialize the conversion context\n");
return;
}
}
if (!sub_rect->ass)
{
if (!SDL_LockTexture(is->sub_texture, (SDL_Rect *)sub_rect, (void **)pixels, pitch)) {
sws_scale(is->sub_convert_ctx, (const uint8_t * const *)sub_rect->data, sub_rect->linesize,
0, sub_rect->h, pixels, pitch);
SDL_UnlockTexture(is->sub_texture);
}
}
else
SDL_UpdateTexture(is->sub_texture, (SDL_Rect *)sub_rect, sub_rect->data[0], sub_rect->linesize[0]);
}
sp->uploaded = 1;
}
} else
sp = NULL;
}
}
calculate_display_rect(&rect, is->xleft, is->ytop, is->width, is->height, vp->width, vp->height, vp->sar);
if (!vp->uploaded) {
if (upload_texture(&is->vid_texture, vp->frame, &is->img_convert_ctx) < 0)
return;
vp->uploaded = 1;
vp->flip_v = vp->frame->linesize[0] < 0;
}
set_sdl_yuv_conversion_mode(vp->frame);
SDL_RenderCopyEx(renderer, is->vid_texture, NULL, &rect, 0, NULL, vp->flip_v ? SDL_FLIP_VERTICAL : 0);
set_sdl_yuv_conversion_mode(NULL);
if (sp) {
#if USE_ONEPASS_SUBTITLE_RENDER
SDL_RenderCopy(renderer, is->sub_texture, NULL, &rect);
#else
int i;
double xratio = (double)rect.w / (double)sp->width;
double yratio = (double)rect.h / (double)sp->height;
for (i = 0; i < sp->sub.num_rects; i++) {
SDL_Rect *sub_rect = (SDL_Rect*)sp->sub.rects[i];
SDL_Rect target = {.x = rect.x + sub_rect->x * xratio,
.y = rect.y + sub_rect->y * yratio,
.w = sub_rect->w * xratio,
.h = sub_rect->h * yratio};
SDL_RenderCopy(renderer, is->sub_texture, sub_rect, &target);
}
#endif
}
}
最后一个比较重要的是修改字幕解码,让文本都解码成准格式的ass格式字幕
static int stream_component_open(VideoState *is, int stream_index)
{
....
if (avctx->codec_type == AVMEDIA_TYPE_VIDEO || avctx->codec_type == AVMEDIA_TYPE_AUDIO)
av_dict_set(&opts, "refcounted_frames", "1", 0);
if(avctx->codec_type==AVMEDIA_TYPE_SUBTITLE)
if (LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 26, 100))
av_dict_set(&opts, "sub_text_format", "ass", 0);
}
这三处工作做完。我们的程序就可以运行了
结果搞定了。跟传统ffplay一样,使用同样的命令。我们只是添加了个不用过滤器的ass字幕。效果看起来还行。
成品展示出来了。该说libass了。
项目中我用到了三个比较重要的结构体,ASS_Library、ASS_Renderer、ASS_Track。
ASS_Library是ass的库管理器,用于将解析功能的函数进行对接。c语言中对象操作是要自己实现的。所以ASS_Library充当桥梁进行接入跟销毁。
ASS_Renderer就比较重要了,它用于生产图片,这里提一嘴,ASS_Renderer生成的ASS_image是每位存储了一个像素透明度信息的8位位图。跟传统位图不一样,它只存储了一个透明度,然后有一个color存储了当前颜色。也就是一个ASS_image只能储存固定颜色,然后通过alpha通道进行颜色调整。这个理解起来特别抽象。只能自己去看源码理解了。因为这里涉及到透明度叠加的算法,那个算法又比较抽象。
ASS_Track用于存储未解析的数据。这个东西内置了map的机制。会对内容进行过滤插入。之前没有加入
if (LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 26, 100))
av_dict_set(&opts, "sub_text_format", "ass", 0);
这段代码的时候,一直添加不进去,一看哈希索引冲突了。如果不加会多一个Dialog:0,这个东西会导致解析失败。
libass的库使用也比较简单,主要分为初始化,添加到Track,解析出字幕。当然,我这样做字幕是不能出特效的,ass字幕还有个特效功能。不过现代字幕比较少用那种特效了。另外会在跳转的时候失去当前的字幕。我也推荐利用track的map功能,添加多线程解析渲染。不过这个需要涉及很多内容。我也比较长时间没有接触c了。就不进行实现了。
关于接口内的函数就不详细介绍了,因为c接口很多内容都是透明的,不需要我分析了,遇到问题的时候多看内容。实在不行对比vf_subtitle找找区别。
此项目仅供参考,没有进行错误考虑,存在若干不完善的地方。各位如果发现有问题很正常,比较详细的参考ffmpeg的avfilter中的vf_subtitle.c。