加入网站会员,全站资源免费获取,每日稳定更新中!

MediaCodec结合FFmpeg实现视频加图片水印(ffmpeg图片合成视频特效)

1、前言最近在研究FFmepg滤镜方面的知识,索性就准备尝试一下代码给视频添加水印一开始想直接FFmpeg直接c代码加水印,写完后测试了一下比较慢,毕竟软解得看CPU即使设置了多线程编解码还是一个吊样,然后想到了另一条路硬解码然后ffmpeg数据处理水印接着送入硬编码这样效率会很高,毕竟GPU还是很快的。

软解永远是兜底方案注:这不是一篇单纯的FFmpeg水印命令文章 注:本篇使用JNI开发2、效果

3、流程仅核心流程具体细节参照示例原视频AAC解码H264编码YUV编码H264合成MediaCodecAVFilterMediaCodecMediaMuxerAudioAACMPEG4AVFilter

是FFmpeg库下的一个流媒体过滤器,它用于对组件常用于多媒体处理与编辑,包含多种滤镜,比如旋转,加水印,多宫格等等,源码位于ffmpeg/libavfilter中4、准备FFmpeg so库 音视频(1) – FFmpeg4.3.4编译。

libyuv so库 libyuv 库编译5、示例5.1 提取音频/视频流轨道MediaFormat//视频流提取器
MediaExtractor mediaExtractor = new MediaExtractor();

//设置视频源
mediaExtractor.setDataSource(path);
//寻找视频流for (int i = 0; i < mediaExtractor.getTrackCount(); i++) {
MediaFormat mediaFormat = mediaExtractor.getTrackFormat(i);

if (mediaFormat.getString(MediaFormat.KEY_MIME).contains(“video/”)) {
//视频流
videoMediaFmt = mediaFormat;

//选择当前视频轨道
mediaExtractor.selectTrack(i);
} elseif (mediaFormat.getString(MediaFormat.KEY_MIME).contains(

“audio/”)) {
//音频流
readAudioMediaFormat = mediaFormat;
//记录音频流索引
audioExtractorSelectIndex = i;
}
}

//音频流提取器
MediaExtractor audioMediaExtractor = new MediaExtractor();
audioMediaExtractor.setDataSource(path);

//直接选择音频流
audioMediaExtractor.selectTrack(audioExtractorSelectIndex);MediaExtractor视频信息的提取类:selectTrack 选择轨道 完毕后所有API都将基于改轨道进行信息提取

getTrackFormat 根据索引获取当前轨道的MediaFormat5.2 创建视频解码器int colorFmtType = MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible;

//设置解码器解码的YUV类型
videoMediaFmt.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFmtType);
//重新设置缓冲区大小
videoMediaFmt.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, videoMediaFmt.getInteger(MediaFormat.KEY_WIDTH) * videoMediaFmt.getInteger(MediaFormat.KEY_HEIGHT) *

3 / 2);
//根据类型创建合适的硬解码器
MediaCodec decode = MediaCodec.createDecoderByType(videoMediaFmt.getString(MediaFormat.KEY_MIME));
finalVideoMediaFmt = videoMediaFmt;
decode.configure(finalVideoMediaFmt,

null, null, 0);
//设置解码异步回调
decode.setCallback(mediaCallback);
decode.start();5.3 创建视频编码器MediaCodec encode = MediaCodec.createEncoderByType(videoMediaFmt.getString(MediaFormat.KEY_MIME));
MediaCodec mediaFormat = MediaFormat.createVideoFormat(videoMediaFmt.getString(MediaFormat.KEY_MIME),videoMediaFmt.getInteger(MediaFormat.KEY_WIDTH), videoMediaFmt.getInteger(MediaFormat.KEY_HEIGHT));

// 编码器接受的YUV格式
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);

//设置比特率 越大越清晰
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, videoMediaFmt.getInteger(MediaFormat.KEY_WIDTH) * videoMediaFmt.getInteger(MediaFormat.KEY_HEIGHT) *

3);
//设置帧率FPS
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, videoMediaFmt.getInteger(MediaFormat.KEY_FRAME_RATE));

//设置I帧
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
//设置采样率
mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE,

44100);
//设置csd-0 1 H264需要写入头部
mediaFormat.setByteBuffer(“csd-0”, videoMediaFmt.getByteBuffer(“csd-0”

));
mediaFormat.setByteBuffer(“csd-1”, videoMediaFmt.getByteBuffer(“csd-1”));
encode.configure(mediaFormat,

null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
encode.start();编码器的设置有些值可以直接去MediaExtractor提取的原视频的MediaFormat

进行读取填充编码器 例如: 帧率 KEY_FRAME_RATE 比特率 KEY_BIT_RATEcsd-0以及csd-1这些信息在原视频文件已经给出不需要在自己设置注:csd-0以及csd-1在H264开头必须要写在头部的(在MediaFormat中写入setByteBuffer()),否则MediaMxure生成MP4会出现错误。

C++音视频学习资料免费获取方法:关注音视频开发T哥,点击「链接」即可免费获取2023年最新C++音视频开发进阶独家免费学习大礼包!5.4 解码与编码MediaCodec工作流程图

5.4.1 发送到解码器@OverridepublicvoidonInputBufferAvailable(@NonNull MediaCodec codec, int index){

if (index >= 0) {
ByteBuffer inputBuffer = codec.getInputBuffer(index);
if (inputBuffer !=

null) {
inputBuffer.clear();
}
int sampleSize = mediaExtractor.readSampleData(inputBuffer,

0);
if (sampleSize < 0) {
//读取完毕
codec.queueInputBuffer(index,

0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
} else {
codec.queueInputBuffer(index,

0, sampleSize, mediaExtractor.getSampleTime(), 0);
//读取下一帧
mediaExtractor.advance();
}
}
}

getInputBuffer(index)根据可用索引获取一个缓冲区ByteBuffermediaExtractor.readSampleData(inputBuffer, 0) 提取一帧数据到buffer中

codec.queueInputBuffer读取的数据发送到解码器中queueInputBuffer 要正确填入时间戳PTS表示帧显示的时间5.4.2 取出解码YUV@Overridepublicvoid

onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info){

//若达到文件尾部if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
videoEncodeStop =

true;
if (audioEncodeStop && !isStop) {
encode.stop();
decode.stop();
mediaMuxer.stop();
isStop =

true;
playVideo();
}
return;
}
if (index >= 0

) {
//Image用于提取YUV
Image image = decode.getOutputImage(index);
if

(image == null) {
codec.releaseOutputBuffer(index, true);
return;
}

//获取类型I420的YUV byte[] i420 = getDataFromImage(image, COLOR_FormatI420);
//…加水印 送入编码器//….

//释放解码后数据
codec.releaseOutputBuffer(index, false);
}
}首先判断是否读取到视频尾部进行后续mp4输出和播放

getOutputImage拿到输出的Image类型,它包含了YUV各个分量数据基于上面解码器配置的COLOR类型COLOR_FormatYUV420Flexible这表示YUV420各个类型集合 接着转为

I420类型YUV类型可以参考: Camera2录制视频(音视频合成)及其YUV数据提取(二)- YUV提取及图像转化5.4.3 native加水印后并同步执行编码//…加水印 送入编码器byte[] nv12 = native_filter(i420);

// start encodeint inputBufferIndex = encode.dequeueInputBuffer(100000);
if (inputBufferIndex >=

0) {
ByteBuffer inputBuffer = encode.getInputBuffer(inputBufferIndex);
inputBuffer.put(nv12);

//数据送入编码器
encode.queueInputBuffer(inputBufferIndex, 0, nv12.length, info.presentationTimeUs,

0);
}
// 获取编码后的输出数据
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();

int outputBufferIndex = encode.dequeueOutputBuffer(bufferInfo, 100000);
if (outputBufferIndex >=

0) {
ByteBuffer outputBuffer = encode.getOutputBuffer(outputBufferIndex);

//处理编码后的数据if (isMediaMuxerStatr && !videoEncodeStop) {
//写入视频轨道编码后的数据
mediaMuxer.writeSampleData(mVideoTrackIndex, outputBuffer, bufferInfo);
}

//释放缓冲区
encode.releaseOutputBuffer(outputBufferIndex, false);
}这里重点要关注isMediaMuxerStatr

这个变量因为在MediaMxure.start后才能writeSampleData5.4.4 native_filter水印函数实现init初始化滤镜void init_avfilter() {

//buffer滤镜 负责将原始视频帧添加到滤镜图中
buffer_filter = avfilter_get_by_name(“buffer”);
//buffersink滤镜 用于从滤镜图中获取处理后的视频帧。

buffersink_filter = avfilter_get_by_name(“buffersink”);

char videoInfoArgs[256];
//buffer滤镜参数

snprintf(videoInfoArgs, sizeof(videoInfoArgs),
“video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d:%d”

,
video_parametres->width, video_parametres->height, video_parametres->format,
fmt_ctx->streams[videoIdx]->time_base.num, fmt_ctx->streams[videoIdx]->time_base.den,
video_parametres->sample_aspect_ratio.num, video_parametres->sample_aspect_ratio.den);

//创建滤镜图
filterGraph = avfilter_graph_alloc();
if (!filterGraph) {
return;
}
//创建滤镜输入端口

int ret = avfilter_graph_create_filter(&buffer_filter_ctx, buffer_filter, “in”, videoInfoArgs,
nullptr, filterGraph);

if (ret < 0) {
return;
}
ret = avfilter_graph_create_filter(&buffersink_filter_ctx, buffersink_filter,

“out”, nullptr,
nullptr, filterGraph);
if (ret < 0) {

return;
}

enumAVPixelFormat pixelFormat[] = {AV_PIX_FMT_YUV420P,
(AVPixelFormat) (video_parametres->format)};

//它被用来设置 buffersink 滤镜的像素格式 在处理视频帧时,buffersink 滤镜将尝试使用 AV_PIX_FMT_YUV420P 或 video_parametres->format 这两种像素格式之一。

av_opt_set_int_list(buffersink_filter_ctx, “pix_fmts”, pixelFormat, AV_PIX_FMT_YUV420P,
AV_OPT_SEARCH_CHILDREN);
in_filter = avfilter_inout_alloc();
in_filter->next = nullptr;
in_filter->name = av_strdup(

“out”);
in_filter->filter_ctx = buffersink_filter_ctx;
in_filter->pad_idx = 0;

out_filter = avfilter_inout_alloc();
out_filter->next = nullptr;
out_filter->name = av_strdup(

“in”);
out_filter->filter_ctx = buffer_filter_ctx;
out_filter->pad_idx = 0;

char filters[

256];
snprintf(filters, sizeof(filters), “movie=%s,scale=200:-1[wm];[in][wm]overlay=50:10[out]”,
logo_path);
ret = avfilter_graph_parse_ptr(filterGraph, filters, &in_filter, &out_filter, nullptr);

if (ret < 0) {
return;
}
ret = avfilter_graph_config(filterGraph, nullptr);

if (ret <

0) {
return;
}
}这里的需要一个输入端口buffer和输出端口bufferskin需要用到avfilter_get_by_name去获取AVFilter 滤镜输入端口需要设置视频的一些参数,这里参数用的是

avformat_find_stream_infoFFmpeg的函数去查找视频信息 avfilter_graph_parse_ptr此函数将一串通过字符串描述的Graph添加到AVFilterGraph中,这里主要是filters参数即

snprintf(filters, sizeof(filters), “movie=%s,scale=200:-1[wm];[in][wm]overlay=50:10[out]”,logo_path);

意思是:movie表示水印图片的路径,同事按比例缩放200自动缩放,在位置坐标x 50 y 10的地方显示一个logo水印yuv加滤镜水印extern “C”
JNIEXPORT jbyteArray JNICALL
Java_com_mt_mediacodec2demo_MainActivity_native_1filter(JNIEnv *env, jobject thiz, jbyteArray src,
jobject i_native_callback) {
jbyteArray

array;
jclass cls = env->GetObjectClass(i_native_callback);
jmethodID mid = env->GetMethodID(cls,

“onFrame”, “([B)V”);

jsize length = env->GetArrayLength(src);
uint8_t *src_data = (uint8_t *) av_malloc(length);
env->GetByteArrayRegion(src,

0, length, (jbyte *) src_data);
av_image_fill_arrays(src_frame->data, src_frame->linesize, src_data,
(AVPixelFormat) video_parametres->format, video_parametres->width,
video_parametres->height,

1);
src_frame->width = video_parametres->width;
src_frame->height = video_parametres->height;
src_frame->time_base = fmt_ctx->streams[videoIdx]->time_base;
src_frame->sample_aspect_ratio = video_parametres->sample_aspect_ratio;
src_frame->format = video_parametres->format;

//pts = 0 表示每一帧显示的时间是0 直接显示
src_frame->pts = 0;
//添加到滤镜中
int ret = av_buffersrc_add_frame_flags(buffer_filter_ctx, src_frame,
AV_BUFFERSRC_FLAG_KEEP_REF);

if (ret >= 0) {
ret = av_buffersink_get_frame(buffersink_filter_ctx, filter_frame);
if

(ret >= 0) {
//滤镜已经添加了
int width = filter_frame->width;
int height = filter_frame->height;
int y_size = width * height;
int uv_size = y_size /

4;
AVFrame *i420_frame = av_frame_alloc();
uint8_t *out_buf = (uint8_t *) av_malloc(
av_image_get_buffer_size(AV_PIX_FMT_YUV420P, width, height,

1));
av_image_fill_arrays(i420_frame->data, i420_frame->linesize, out_buf,
AV_PIX_FMT_YUV420P, width, height,

1);
struct SwsContext *img_ctx = sws_getContext(
filter_frame->width, filter_frame->height,
(AVPixelFormat) filter_frame->format,

//源地址长宽以及数据格式
filter_frame->width, filter_frame->height, AV_PIX_FMT_YUV420P, //目的地址长宽以及数据格式

SWS_BICUBIC, NULL, NULL, NULL);
sws_scale(img_ctx, filter_frame->data, filter_frame->linesize,

0, height,
i420_frame->data, i420_frame->linesize);

uint8_t *i420_data = (uint8_t *) malloc(y_size *

3 / 2);
memcpy(i420_data, i420_frame->data[0], y_size);
memcpy(i420_data + y_size, i420_frame->data[

1], uv_size);
memcpy(i420_data + y_size + uv_size, i420_frame->data[2], uv_size);
jbyteArray array_nv12 = env->NewByteArray(y_size *

3 / 2);
jbyte *nv12 = env->GetByteArrayElements(array_nv12, 0);
i420ToNv12(i420_data, video_parametres->width, video_parametres->height, nv12);
env->SetByteArrayRegion(array_nv12,

0, y_size * 3 / 2,
(jbyte *) (nv12));
env->CallVoidMethod(i_native_callback, mid, array_nv12);

array = array_nv12;
free(i420_data);
av_frame_free(&i420_frame);
free(out_buf);
sws_freeContext(img_ctx);
}
}
av_frame_unref(filter_frame);
av_frame_unref(src_frame);
av_free(src_data);

returnarray;
}av_buffersrc_add_frame_flags将yuv发送到滤镜器中 av_buffersink_get_frame 将加了水印的yuv取出保存在AVFrame结构体中

i420ToNv12利用libyuv库进行yuv格式转换,libyuv是google开源的yuv转换库效率比较高 av_image_fill_arrays将传来的I420格式YUV对齐并填充到src_frame

的data数据中 sws_getContext获取转换ctx sws_scale保险起见将滤镜后的数据再次转换到I420格式 然后通过I420的YUV分量排列形式以此从AVFrame结构中的data[0][1][2]

存储的YUV分量中拷贝到新的内存区域要设置pts不然水印图片无法显示出来这里本来用callback形式将数据传回到Java层,后来考虑到要同步执行就放弃了5.4.5 添加视频轨道@Overridepublic

void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {
if (mVideoTrackIndex ==

-1) {
MediaFormat outputFormat = encode.getOutputFormat();
mVideoTrackIndex = mediaMuxer.addTrack(outputFormat);

//然后添加音频轨道再写入AAC源数据
writeAAC();
}
}5.4.6 写入音频源数据因为水印只需要给视频帧添加,故而音频内容不需要任何变动所以不需要再次解码然后编码合成

privatevoidwriteAAC(){
new Thread(new Runnable() {
@Overridepublicvoidrun(){
mAudioTrackIndex = mediaMuxer.addTrack(audioFormat);
mediaMuxer.start();
isMediaMuxerStatr =

true;
audioBuf.clear();
int size = audioMediaExtractor.readSampleData(audioBuf,

0);
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
info.size = size;
info.presentationTimeUs = audioMediaExtractor.getSampleTime();
info.flags =

0;
mediaMuxer.writeSampleData(mAudioTrackIndex,audioBuf,info);
while (audioMediaExtractor.advance()) {
size = audioMediaExtractor.readSampleData(audioBuf,

0);
info.size = size;
info.presentationTimeUs = audioMediaExtractor.getSampleTime();
info.flags =

0;
//写入音频数据
mediaMuxer.writeSampleData(mAudioTrackIndex,audioBuf,info);
}
audioEncodeStop =

true;
if (videoEncodeStop && !isStop) {
encode.stop();
decode.stop();
mediaMuxer.stop();
isStop =

true;
endTime = System.nanoTime();
duration = (endTime – startTime) /

1000000000; // 计算结果为秒
Log.d(TAG, “extime = ” + duration + ” s”);
playVideo();
}
}
}).start();
}

audioMediaExtractor.getSampleTime()获取当前轨道的帧时间戳 audioMediaExtractor.advance()指向下一帧音频和视频流要对齐时间戳,不然会出现音画不同步的问题。

Github地址Android-AddImageWatermarkToVideo原文链接:音视频(8)MediaCodec结合FFmpeg实现视频加图片水印 – 掘金

© 版权声明
THE END
喜欢就支持一下吧
点赞357 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容