在Android采集视频过程中增加水印功能实现

2023-05-02 15:24:43 浏览数 (2)

CSDN旧文搬迁!

在Android采集视频过程中增加水印,并且水印包含一个变化的时间戳,这里考虑方案实现的时候,就想到了ffmpeg,ffmpeg包含很多filter能实现水印添加的功能。

基本实现方案是,Camera预览 -> 得到预览帧的bitmap -> 给bitmap通过ffmpeg 添加水印  -> ffmpeg把bitmap使用h264编码 -> 写文件。

其实ffmpeg添加水印功能在网上例子很多,也都大同小异,但在Android端,比较难搞的地方其实是ffmpeg编译出能带水印添加功能的so库文件,其中:ffmpeg的drawtext filter依赖freetype的so,需要先编一个android平台下freetype的so,而在android平台的编译ffmpeg时,难搞的pkg-cfg总是检查依赖的freetype失败,迫不得已修改了ffmpeg的configure,不在对freetype库做检查,但编译配置的时候需要手动指定freetype so的搜索路径。

我使用的配置如下:

代码语言:javascript复制
/usr/local/lib/pkgconfig $FFMPEG_ROOT/configure --target-os=linux 
--prefix=$PREFIX 
--disable-encoders 
--disable-decoders 
--disable-muxers 
--disable-demuxers 
--disable-parsers 
--disable-bsfs 
--disable-protocols 
--disable-devices 
--disable-avdevice 
--disable-zlib 
--disable-bzlib  
--enable-cross-compile 
--enable-runtime-cpudetect 
--pkg-config-flags="--static" 
--disable-asm 
--arch=arm 
--enable-armv5te 
--cc=$PREBUILT/bin/arm-linux-androideabi-gcc 
--cross-prefix=$PREBUILT/bin/arm-linux-androideabi- 
--disable-stripping 
--nm=$PREBUILT/bin/arm-linux-androideabi-nm 
--sysroot=$PLATFORM 
--enable-nonfree 
--enable-version3 
--enable-gpl 
--disable-doc 
--disable-ffplay 
--disable-ffserver 
--disable-ffprobe 
--enable-avcodec 
--enable-avformat 
--enable-avutil 
--enable-avfilter 
--enable-avresample 
--enable-swresample 
--enable-swscale 
--enable-postproc 
--enable-libx264 
--enable-encoder=libx264 
--enable-decoder=h264 
--enable-hwaccels 
--enable-memalign-hack 
--disable-debug 
--enable-pthreads 
--disable-filters 
--enable-libfreetype 
--enable-filter=drawbox 
--enable-filter=drawtext 
--enable-avisynth 
--enable-iconv 
--extra-cflags="-Os -s -I$X264_ROOT -I$NDK/sysroot/include -I$PREFIX/include/freetype -I$PREFIX/include/ -fPIC -DANDROID -D__thumb__ -mthumb -Wfatal-errors -Wno-deprecated -mfloat-abi=softfp -mfpu=neon -marm -march=armv7-a  -mvectorize-with-neon-quad" 
--extra-ldflags="-L$ELIB -L$NDK/sysroot/lib -L$NDK/sources/cxx-stl/gnu-libstdc  /4.6/libs/armeabi-v7a -L$PREFIX/lib" 
--extra-libs="-lfreetype2-static -lstdc   -lgnustl_static -fexceptions -lsupc   -llog "

然后添加水印过程中出现水波纹的现象,主要原因是给定的width和height和实际的bitmap不匹配产生。

如下为doubango下编码前添加水印的部分代码:

代码语言:javascript复制
#   include <libavfilter/avfiltergraph.h>
#   include <libavfilter/avfilter.h>
#   include <libavfilter/avcodec.h>
#   include <libavfilter/buffersink.h>
#   include <libavfilter/buffersrc.h>
#   include <libavutil/avutil.h>
#   include <libavutil/imgutils.h>
static AVFilterContext* buffersink_ctx = NULL;
static AVFilterContext* buffersrc_ctx = NULL;
static AVFilterGraph* filter_graph = NULL;
static AVFrame* frame_in = NULL;
static AVFrame* frame_out = NULL;
static int isInited;
static int origin_in_width = 480;
static int origin_in_height = 320;
static char last_wartmark_str[125] = "";
static char filters_descr[256] = "";
static int init_filters(tmedia_codec_t* self)
{
        tdav_codec_h264_t* h264 = (tdav_codec_h264_t*)self;
       if (!tmedia_defaults_get_use_water_mark_func_flg()){
            return -1;
       }
       if (tmedia_defaults_get_water_mark_strvalue() == tsk_null){
            TSK_DEBUG_ERROR("tmedia_defaults_get_water_mark_strvalue() is nulln");
            tmedia_defaults_set_use_water_mark_func_flg(tsk_false, tmedia_defaults_get_water_mark_position_x(), tmedia_defaults_get_water_mark_position_y());
             return -1;
       }
       if (strlen(last_wartmark_str) == 0){
            strncpy(last_wartmark_str, tmedia_defaults_get_water_mark_strvalue(), sizeof(last_wartmark_str) - 1);
            last_wartmark_str[124] = '';
       }
       if ((tsk_strcmp(last_wartmark_str, tmedia_defaults_get_water_mark_strvalue()) != 0)){
              tdav_codec_h264_deinit_filters();
                isInited = 0;//refresh filters.                 
       }
       strncpy(last_wartmark_str, tmedia_defaults_get_water_mark_strvalue(), sizeof(last_wartmark_str));
        //TSK_DEBUG_INFO("init filters ,Picture size: %u ** %u", h264->encoder.context->width, h264->encoder.context->height);
        if(!self){
                TSK_DEBUG_ERROR("self is nulln");
                return -1;
        }
        int in_width=h264->encoder.context->width;
        int in_height=h264->encoder.context->height;
        int format = PIX_FMT_YUV420P;
        if(!in_width || !in_height) {
                TSK_DEBUG_ERROR("in_widthin_height is nulln");
                return -1;
        }
        if( in_width != origin_in_width ||  in_height != origin_in_height){
              tdav_codec_h264_deinit_filters();
                isInited = 0;
        }
        if(isInited){
                TSK_DEBUG_INFO("here init graphfilter ok.n");
                return -1;
        }
        if(filter_graph) {
                avfilter_graph_free(&filter_graph);
        }
        
	//static char *filters_descr = "drawbox=x=100:y=100:w=50:h=50:color=pink@0.5";
	//static char *filters_descr = "drawtext=fontfile=/sdcard/arialbd.ttf:fontsize=30:text='6102124695':x=100:y=x/dar:fontcolor=red@0.5:shadowy=2";
	//static char *filters_descr = "drawtext=fontfile=/sdcard/arialbd.ttf:fontsize=30:text='6102124695':x=100:y=x/dar";
	//static char *filters_descr = "drawtext=fontsize=30:text='6102124695':fontcolor=red";
   char *font_color = "red";
   switch(tmedia_defaults_get_water_font_color()){
       case 0: //red
        font_color = "red";
        break;
        case 1://green
        font_color = "green";
        break;
        case 2://blue
        font_color = "blue";
            break;
        case 3://black
        font_color = "black";
            break;
            case 4://yello
        font_color = "yello";
            break;
            case 5://oreage
        font_color = "oreage";
            break;
            case 6://white
        font_color = "White";
            break;
            default:
                break;
   };
   if (tmedia_defaults_get_water_font_path() == tsk_null){
       snprintf(filters_descr, sizeof(filters_descr), "drawtext=fontfile=/sdcard/arialbd.ttf:fontsize=%d:text=%s:x=%d:y=%d:fontcolor=%s@0.6:borderw=2:bordercolor=black@0.6",
            tmedia_defaults_get_water_font_size(),
            tmedia_defaults_get_water_mark_strvalue(),
            tmedia_defaults_get_water_mark_position_x(),
            tmedia_defaults_get_water_mark_position_y(),
            font_color);
   }else{
       snprintf(filters_descr, sizeof(filters_descr), "drawtext=fontfile=%s:fontsize=%d:text=%s:x=%d:y=%d:fontcolor=%s@0.6:borderw=2:bordercolor=black@0.6",
            tmedia_defaults_get_water_font_path(),
            tmedia_defaults_get_water_font_size(),
            tmedia_defaults_get_water_mark_strvalue(),
            tmedia_defaults_get_water_mark_position_x(),
            tmedia_defaults_get_water_mark_position_y(),
            font_color);
   }
   avfilter_register_all();
   char args[512];
   int ret = 0;
   AVFilter *buffersrc  = avfilter_get_by_name("buffer");
   AVFilter *buffersink = avfilter_get_by_name("buffersink");
   AVFilterInOut *outputs = avfilter_inout_alloc();
   AVFilterInOut *inputs  = avfilter_inout_alloc();
   filter_graph = avfilter_graph_alloc();
   if (!outputs || !inputs || !filter_graph) {
       ret = AVERROR(ENOMEM);
       goto end;
   }
   /* buffer video source: the decoded frames from the decoder will be inserted here. */
   snprintf(args, sizeof(args),
           "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
           in_width, in_height, PIX_FMT_YUV420P,
           1, tmedia_defaults_get_video_fps(),
           1, 1);
   ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in",
                                      args, NULL, filter_graph);
   if (ret < 0) {
           TSK_DEBUG_ERROR("Cannot create buffer source, %d, n", ret);
       av_log(NULL, AV_LOG_ERROR, "Cannot create buffer sourcen");
       goto end;
   }
   /* buffer video sink: to terminate the filter chain. */
   ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out",
                                      NULL, NULL, filter_graph);
   if (ret < 0) {
           TSK_DEBUG_ERROR("Cannot create buffer sink, %d, n", ret);
       av_log(NULL, AV_LOG_ERROR, "Cannot create buffer sinkn");
       goto end;
   }
   ret = av_opt_set_int_list(buffersink_ctx, "pix_fmts", pix_fmts,
                             PIX_FMT_NONE, AV_OPT_SEARCH_CHILDREN);
   if (ret < 0) {
           TSK_DEBUG_ERROR("Cannot set output pixel format, %d, n", ret);
       av_log(NULL, AV_LOG_ERROR, "Cannot set output pixel formatn");
       goto end;
   }
   /*
    * Set the endpoints for the filter graph. The filter_graph will
    * be linked to the graph described by filters_descr.
    */
   /*
    * The buffer source output must be connected to the input pad of
    * the first filter described by filters_descr; since the first
    * filter input label is not specified, it is set to "in" by
    * default.
    */
   outputs->name       = av_strdup("in");
   outputs->filter_ctx = buffersrc_ctx;
   outputs->pad_idx    = 0;
   outputs->next       = NULL;
   /*
    * The buffer sink input must be connected to the output pad of
    * the last filter described by filters_descr; since the last
    * filter output label is not specified, it is set to "out" by
    * default.
    */
   inputs->name       = av_strdup("out");
   inputs->filter_ctx = buffersink_ctx;
   inputs->pad_idx    = 0;
   inputs->next       = NULL;
   if ((ret = avfilter_graph_parse_ptr(filter_graph, filters_descr,
                                   &inputs, &outputs, NULL)) < 0){
           TSK_DEBUG_ERROR("avfilter_graph_parse_ptr failed, ret:%d.n", ret);
         goto end;
   }
   if ((ret = avfilter_graph_config(filter_graph, NULL)) < 0){
           TSK_DEBUG_ERROR("avfilter_graph_config failed, ret:%d.n", ret);
                 goto end;
   }
   if (ret == 0){
        frame_in = av_frame_alloc();
        unsigned char* frame_buffer_in = (unsigned char*)av_malloc(av_image_get_buffer_size(format,in_width, in_height, 1));
        av_image_fill_arrays(frame_in->data, frame_in->linesize, frame_buffer_in, format,in_width, in_height, 1);
        frame_out = av_frame_alloc();
/*      unsigned char* frame_buffer_out = (unsigned char*)av_malloc(av_image_get_buffer_size(format,in_width, in_height, 1));
        av_image_fill_arrays(frame_out->data, frame_out->linesize, frame_buffer_out,format,in_width, in_height, 1);
    */
   }
   TSK_DEBUG_INFO("init graphfilter ok.n");
   isInited = 1;
end:
   avfilter_inout_free(&inputs);
   avfilter_inout_free(&outputs);
   return ret;
}
// 
static int tdav_codec_h264_deinit_filters(){
    if (!isInited){
        return;
    }
    isInited = 0;
    if (frame_in != NULL){
        av_frame_free(&frame_in);
    }
    if (frame_out != NULL){
        av_frame_free(&frame_out);
    }
    if (buffersink_ctx != NULL){
        avfilter_free(buffersink_ctx);
        buffersink_ctx = NULL;
    }
    if (buffersrc_ctx != NULL){
        avfilter_free(buffersrc_ctx);
        buffersrc_ctx = NULL;
    }
    if (filter_graph != NULL){
         //avfilter_graph_free(&filter_graph);
    }
    last_wartmark_str[0] = '';
}

// 
static int tdav_codec_h264_add_water_marker(tmedia_codec_t* self,  AVFrame* frame_src, const unsigned char* in_data){
        //TSK_DEBUG_ERROR("enter tdav_codec_h264_add_water_markern");
       if (!tmedia_defaults_get_use_water_mark_func_flg()){
            return -1;
       }
        if(!isInited) {
                TSK_DEBUG_INFO(" graphfilter not inited.n");
                return -1;
        }
        int ret;
       //AVFrame* frame_out = NULL;
        tdav_codec_h264_t* h264 = (tdav_codec_h264_t*)self;
        //TSK_DEBUG_INFO("Picture size: %u ** %u", h264->encoder.context->width, h264->encoder.context->height);
        if(!self){
                TSK_DEBUG_ERROR("self is nulln");
                return -1;
        }
        int in_width=h264->encoder.context->width;
        int in_height=h264->encoder.context->height;
        int format = PIX_FMT_YUV420P;
        if(!in_width || !in_height) {
                TSK_DEBUG_ERROR("in_widthin_height is nulln");
                return -1;
        }
        //init frame out
        //frame_out = av_frame_alloc();
       /*
        unsigned char* frame_buffer_out = (unsigned char*)av_malloc(av_image_get_buffer_size(format,in_width, in_height, 1));
        av_image_fill_arrays(frame_out->data, frame_out->linesize, frame_buffer_out,format,in_width, in_height, 1);
       */
                           
        if (!frame_in || !frame_out) {
            TSK_DEBUG_ERROR("Could not allocate framen");
            return -1;
       }
         frame_in->width=in_width;
         frame_in->height=in_height;
         frame_in->format=format;
        // TSK_DEBUG_ERROR("frame_in width is %d, height is %d n",frame_in->width, frame_in->height );
        //copy data
         //0. copy data to frame_in
      memcpy(frame_in->data[0], frame_src->data[0], in_width*in_height);
      memcpy(frame_in->data[1], frame_src->data[1], in_width*in_height*1/4);
      memcpy(frame_in->data[2], frame_src->data[2], in_width*in_height*1/4);
        // 1.add frame to filtergraph
        // ret = av_buffersrc_add_frame_flags(buffersrc_ctx, frame_in, 0);
        ret = av_buffersrc_add_frame_flags(buffersrc_ctx, frame_in, AV_BUFFERSRC_FLAG_KEEP_REF);
        if(ret < 0) {
                TSK_DEBUG_ERROR("Cannot add frame to graph n");
                TSK_DEBUG_ERROR("Cannot add frame to graph,ret is %d n",ret);
                goto end;
        }
        // 2.pull filtered pictures from the  filtergraph
        ret = av_buffersink_get_frame(buffersink_ctx, frame_out);
        if(ret < 0) {
                TSK_DEBUG_ERROR("Cannot get frame from graph n");
                TSK_DEBUG_ERROR("Cannot get frame from graph,ret is %d n",ret);
                goto end;
        }
        //3. copy data to frame_src
        //ret = av_frame_copy(frame_src, frame_out);
        //memcpy(frame_src->data[0], frame_out->data[0], in_width*in_height);    
        //memcpy(frame_src->data[1], frame_out->data[1], in_width*in_height*1/4);
       //memcpy(frame_src->data[2], frame_out->data[2], in_width*in_height*1/4);
       av_image_copy(frame_src->data, frame_src->linesize, frame_out->data, frame_out->linesize, PIX_FMT_YUV420P, in_width, in_height);
end:
       //av_frame_free(&frame_out);
       av_frame_unref(frame_out);
        return ret;
} 
        //编码前先将bitmap的yuv数据添加水印
        #if ADD_WATER_MARKER
                //tdav_codec_h264_init_filters(self);
                init_filters(self);
                tdav_codec_h264_add_water_marker(self, h264->encoder.picture, (const unsigned char*)in_data);
                //tdav_codec_h264_add_water_marker2((const unsigned char*)in_data, in_size);
        #endif

ffmpeg编码参数优化:

做了一段时间的视频后,最先碰到的是花屏,解码端丢包的花屏,先是通过抓取编码后的BITMAP,发现解码出来就是花屏的,所以考虑优化编码来减少因为丢包产生的花屏;另外调整丢包策略规避解码花屏的问题.

1、X264编码参数调整:

H264 FF_PROFILE_H264_BASELINE、 FF_PROFILE_H264_MAIN两种编码差异,其中最明显的差异是profile_idc_baseline没有B帧,而profile_idc_main带B帧,这个差异体现在解码时,带B帧的不仅依赖之前的帧,还依赖之后到来的帧,通常在实时视频类应用中不建议带B帧的编码。

质量和码率控制:

最开始也是用bit_rate 来控制:

encoder.context->bit_rate = (self->encoder.max_bw_kpbs * 1024);// bps

但bit_rate是平均码率,总是达不到理想的结果(包括编码后的视频帧大小和质量),后来查看网上关于移动设备X264编码优化,提到了通过CRF来控制质量和码率,认为: x264默认是使用”crf”压缩算法,  默认值为23, 代表了编码速度,画质与码流的均衡.并且对各种取值做了编码大小和帧率的比较: 

ultrafast baseline crf 28 encoded 467 frames, 58.94 fps, 515.58 kb/s 2006474

superfast baseline crf 26 encoded 467 frames, 41.73 fps, 460.02 kb/s 1790244

superfast baseline crf 28 encoded 467 frames, 43.64 fps, 366.28 kb/s 1425436

配置crf

代码语言:javascript复制
if((ret = av_opt_set_double(self->encoder.context->priv_data, "crf", (double)30, 0))){
            TSK_DEBUG_ERROR("Failed to set x264 crf 28.0");
	    return;
}
//ultrafast veryfast superfast
if((ret = av_opt_set(self->encoder.context->priv_data, "preset", "superfast", 0))){
	TSK_DEBUG_ERROR("Failed to set x264 preset to veryfast");
}

编码后视频NALU单元大小控制:

代码语言:javascript复制
encoder.context->rtp_payload_size = H264_RTP_PAYLOAD_SIZE;//H264_RTP_PAYLOAD_SIZE大小为1300if((ret = av_opt_set_int(self->encoder.context->priv_data, "slice-max-size", H264_RTP_PAYLOAD_SIZE, 0))){	TSK_DEBUG_ERROR("Failed to set x264 slice-max-size to %d", H264_RTP_PAYLOAD_SIZE);}

两个I帧之间帧个数的控制:

encoder.context->gop_size = TMEDIA_CODEC_VIDEO(self)->out.fps * TDAV_H264_GOP_SIZE_IN_SECONDS

2、丢包策略:

基于BP的H264编码,P帧只依赖之前的帧就能解码,所以出现丢包时的处理策略会比较简单,如果发现有P帧丢了,则丢弃后面的所有P帧,直到有I帧到来;如果是I帧丢了,则丢弃I帧及之后的P帧,直到有I帧到来。 

0 人点赞