街猫自研多媒体能力介绍

背景

哈啰街猫移动团队在支撑业务发展过程中,已有的多媒体基础能力存在一些问题/瓶颈:

  • 猫屋直播 - 三方直播sdk,在MTK芯片的机型上存在兼容问题(hevc硬解报错),导致直播流无法播放,用户无法使用app的核心功能
  • 音视频流合成、滤镜 - 需要能够灵活的支持用户去触发对猫屋直播流的截取、合成、添加滤镜等,使用系统多媒体Api,在可扩展性,流处理效率,兼容性,以及滤镜的支持上,都存在问题
  • 视频转码 - 猫友圈上传的用户视频,需要对其做转码后发布,转码最大的问题是,源视频的格式和参数是不确定的(Android),如果只用系统多媒体Api去处理,会存在各种各样的兼容性问题,结果要么是转码失败,要么视频发布后,其他端的用户观看异常

所以需要自研多媒体框架去解决/优化上述问题,以便后续能够更好的支撑业务发展。

街猫自研多媒体架构

硬编解码

支持Android 7.0以后,将media codec部分从media player service里抽离出来,单独开了一条新的binder服务media.codec来开放系统的硬编解码能力,系统要开放,必须要有标准,让各个硬件平台根据标准来开发其硬编解码器,然后集成到media.codec中,这个协议就是OpenMax。

OpenMax分为三层:

  • DL - 开发层
  • IL - 集成层
  • AL- 应用层

OpenMax是多媒体框架标准,目前应用最广泛的是IL层,各个硬件平台只需要依托IL层协议,提供统一的抽象层接口,屏蔽各自在底层适配时存在的差异,最终打包到libstagefrighthw.so,由media codec service加载后,以binder服务的形式,将硬编解码能力开放给有需要的多媒体应用,包括OpenMax AL,ffmpeg,nuplayer,exoplayer,街猫多媒体组件等等。

多媒体底层框架 - FFMPEG

街猫多媒体底层依托于ffmpeg,从而具备了覆盖多媒体全应用场景的底层能力,基于ffmpeg 4.2.2源码,我们目前主要做了如下定制化:

  • 修改编解码配置,使ffmpeg支持flv&h265视频流
  • 添加h264&h265编解码codec,依托Android Ndk MediaCodec Api,使ffmpeg具备h264&h265格式的硬编解码能力
  • 添加h264软编码codec,使ffmpeg具备h264软编码能力

自研多媒体组件

组件名称 描述
街猫多媒体核心组件 包含街猫多媒体基础java和c++代码实现, 包含两个核心的c++库:1. libpet-media-core.so 包含ffmpeg4.2.2的代码和街猫多媒体转码,直播,视频合成的核心实现(平台无关),2. libpet-media-compat.so android和ios的代理库,包含android和ios平台兼容性c++实现
街猫多媒体转码组件 依赖多媒体核心组件,包含转码的java层实现核心能力: 支持全格式转码,优先使用硬编码,如果设备不支持,则自动降级为软编码 支持码率,分辨率,fps,gop等参数配置 支持对源视频指定区间转码 转码后视频编码格式默认为h264,音频aac 转码完成后,对生成视频做有效性校验,确保转码符合要求 转码后MP4视频全部moov前置
街猫多媒体直播组件 依赖多媒体核心组件,包含flv直播流的java层实现,核心能力: 支持flv hevc格式的直播流的播放,支持软硬解码动态切换,相对三方库纯硬解码,具有更好的兼容性, api的设计跟三方sdk完全保持一致,业务层无缝接入
街猫多媒体视频合成组件 依赖多媒体核心组件,包含音视频合成的java层实现, 核心能力: 支持输入视频和音频数据流,合成h264编码格式的mp4文件 视频格式支持yuv420p&nv12(格式可扩展) 音频输入pcm数据,支持Packed和Planar两种格式,也可不设置音频(合成视频无声音) 支持添加logo和名称水印滤镜(滤镜可扩展) 支持配置合成视频的片尾视频 合成视频的编码格式,码率、软硬编码等可配置

业务成果

街猫转码

  • 将转码覆盖率从原先的50%以下,提高到99%以上,统一转码后的视频格式,通过转码后视频格式的有效性校验,确保99%可播放,从而彻底解决5+线上遗留问题
  • 覆盖率提高后,视频的平均压缩率提高30+pp,降低了云端存储和流量费用,提高用户的观看体验
  • 转码后视频moov全部前置,提高转码后视频的秒开率

街猫直播

  • 完美解决了由于三方播放器兼容性不足导致部分机型无法播放的问题,播放率提高5+pp

街猫合成

  • 合成和水印能力,深度应用于投喂一刻和直播间聊天视频录制,功能上线后,大幅提高了内容点击率,猫友圈转发率和发言提升率

FFMPEG介绍

核心库

  • libavcodec - 包含音频和视频的编解码器
  • libavutil - 包含编码所需的各种使用工具,包括随机数生成,常用数据结构,多媒体相关基础工具等等
  • libavformat - 包含封装和解封装的实现
  • libavfilter - 包含多媒体滤镜的实现
  • libavdevice - 包含设备输入和输出相关实现
  • libswscale - 包含视频格式转码实现
  • libswresample - 包含音频重采样的实现

音视频播放流程

上图是视频播放器的基本流程,source、demux、decoder、output

  • source:数据源,数据的来源不一定都是本地file,也有可能是网路上的各种协议例如:http、rtsp、HLS等。source的任务就是把数据源抽象出来,为下一个demux模块提供它需要的稳定的数据流。demux不用关信数据到底是从什么地方来的。
  • demux解复用:视频文件一般情况下都是把音视频的encoded streams交织的通过某种规则放在一起。这种规则就是容器规则。现在有很多不同的容器格式。如ts、mp4、flv、mkv、avi、rmvb等等。demux的功能就是把音视频的ES流从容器中剥离出来,然后分别送到不同的解码器中。其实音频和视频本身就是2个独立的系统。容器把它们包在了一起。但是他们都是独立解码的,所以解码之前,需要把它分别 独立出来。demux就是干这活的,他为下一步decoder解码提供了数据流。
  • decoder解码:解码器 - 播放器的核心模块,分为音频和视频解码器。影像在录制后, 原始的音视频都是占用大量空间, 而且是冗余度较高的数据. 因此, 通常会在制作的时候就会进行某种压缩 ( 压缩技术就是将数据中的冗余信息去除数据之间的相关性 ). 这就是我们熟知的音视频编码格式, 包括MPEG1(VCD)\ MPEG2(DVD)\ MPEG4 \ H.264 等等. 音视频解码器的作用就是把这些压缩了的数据还原成原始的音视频数据. 当然, 编码解码过程基本上都是有损的 .解码器的作用就是把编码后的数据还原成原始数据。
  • output输出:输出部分分为音频和视频输出。解码后的音频(pcm)和视频(yuv)的原始数据需要得到音视频的output模块的支持才能真正的让人的感官系统(眼和耳)辨识到。

市面上绝大多数播放器的基本结构都是如此,不同的是在实现方式上会存在差异。

一个简单的FFMPEG工程

下面拿ffmpeg/examples/transcoding.c做介绍,这是一个转码的参考工程:

ini 复制代码
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavfilter/buffersink.h>
#include <libavfilter/buffersrc.h>
#include <libavutil/opt.h>
#include <libavutil/pixdesc.h>

static AVFormatContext *ifmt_ctx;
static AVFormatContext *ofmt_ctx;
typedef struct FilteringContext {
    AVFilterContext *buffersink_ctx;
    AVFilterContext *buffersrc_ctx;
    AVFilterGraph *filter_graph;
} FilteringContext;
static FilteringContext *filter_ctx;

typedef struct StreamContext {
    AVCodecContext *dec_ctx;
    AVCodecContext *enc_ctx;
} StreamContext;
static StreamContext *stream_ctx;

static int open_input_file(const char *filename)
{
    int ret;
    unsigned int i;

    ifmt_ctx = NULL;
    if ((ret = avformat_open_input(&ifmt_ctx, filename, NULL, NULL)) < 0) {
        av_log(NULL, AV_LOG_ERROR, "Cannot open input file\n");
        return ret;
    }

    if ((ret = avformat_find_stream_info(ifmt_ctx, NULL)) < 0) {
        av_log(NULL, AV_LOG_ERROR, "Cannot find stream information\n");
        return ret;
    }

    stream_ctx = av_mallocz_array(ifmt_ctx->nb_streams, sizeof(*stream_ctx));
    if (!stream_ctx)
        return AVERROR(ENOMEM);

    for (i = 0; i < ifmt_ctx->nb_streams; i++) {
        AVStream *stream = ifmt_ctx->streams[i];
        AVCodec *dec = avcodec_find_decoder(stream->codecpar->codec_id);
        AVCodecContext *codec_ctx;
        if (!dec) {
            av_log(NULL, AV_LOG_ERROR, "Failed to find decoder for stream #%u\n", i);
            return AVERROR_DECODER_NOT_FOUND;
        }
        codec_ctx = avcodec_alloc_context3(dec);
        if (!codec_ctx) {
            av_log(NULL, AV_LOG_ERROR, "Failed to allocate the decoder context for stream #%u\n", i);
            return AVERROR(ENOMEM);
        }
        ret = avcodec_parameters_to_context(codec_ctx, stream->codecpar);
        if (ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "Failed to copy decoder parameters to input decoder context "
                   "for stream #%u\n", i);
            return ret;
        }
        /* Reencode video & audio and remux subtitles etc. */
        if (codec_ctx->codec_type == AVMEDIA_TYPE_VIDEO
                || codec_ctx->codec_type == AVMEDIA_TYPE_AUDIO) {
            if (codec_ctx->codec_type == AVMEDIA_TYPE_VIDEO)
                codec_ctx->framerate = av_guess_frame_rate(ifmt_ctx, stream, NULL);
            /* Open decoder */
            ret = avcodec_open2(codec_ctx, dec, NULL);
            if (ret < 0) {
                av_log(NULL, AV_LOG_ERROR, "Failed to open decoder for stream #%u\n", i);
                return ret;
            }
        }
        stream_ctx[i].dec_ctx = codec_ctx;
    }

    av_dump_format(ifmt_ctx, 0, filename, 0);
    return 0;
}

static int open_output_file(const char *filename)
{
    AVStream *out_stream;
    AVStream *in_stream;
    AVCodecContext *dec_ctx, *enc_ctx;
    AVCodec *encoder;
    int ret;
    unsigned int i;

    ofmt_ctx = NULL;
    avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, filename);
    if (!ofmt_ctx) {
        av_log(NULL, AV_LOG_ERROR, "Could not create output context\n");
        return AVERROR_UNKNOWN;
    }


    for (i = 0; i < ifmt_ctx->nb_streams; i++) {
        out_stream = avformat_new_stream(ofmt_ctx, NULL);
        if (!out_stream) {
            av_log(NULL, AV_LOG_ERROR, "Failed allocating output stream\n");
            return AVERROR_UNKNOWN;
        }

        in_stream = ifmt_ctx->streams[i];
        dec_ctx = stream_ctx[i].dec_ctx;

        if (dec_ctx->codec_type == AVMEDIA_TYPE_VIDEO
                || dec_ctx->codec_type == AVMEDIA_TYPE_AUDIO) {
            /* in this example, we choose transcoding to same codec */
            encoder = avcodec_find_encoder(dec_ctx->codec_id);
            if (!encoder) {
                av_log(NULL, AV_LOG_FATAL, "Necessary encoder not found\n");
                return AVERROR_INVALIDDATA;
            }
            enc_ctx = avcodec_alloc_context3(encoder);
            if (!enc_ctx) {
                av_log(NULL, AV_LOG_FATAL, "Failed to allocate the encoder context\n");
                return AVERROR(ENOMEM);
            }

            /* In this example, we transcode to same properties (picture size,
             * sample rate etc.). These properties can be changed for output
             * streams easily using filters */
            if (dec_ctx->codec_type == AVMEDIA_TYPE_VIDEO) {
                enc_ctx->height = dec_ctx->height;
                enc_ctx->width = dec_ctx->width;
                enc_ctx->sample_aspect_ratio = dec_ctx->sample_aspect_ratio;
                /* take first format from list of supported formats */
                if (encoder->pix_fmts)
                    enc_ctx->pix_fmt = encoder->pix_fmts[0];
                else
                    enc_ctx->pix_fmt = dec_ctx->pix_fmt;
                /* video time_base can be set to whatever is handy and supported by encoder */
                enc_ctx->time_base = av_inv_q(dec_ctx->framerate);
            } else {
                enc_ctx->sample_rate = dec_ctx->sample_rate;
                enc_ctx->channel_layout = dec_ctx->channel_layout;
                enc_ctx->channels = av_get_channel_layout_nb_channels(enc_ctx->channel_layout);
                /* take first format from list of supported formats */
                enc_ctx->sample_fmt = encoder->sample_fmts[0];
                enc_ctx->time_base = (AVRational){1, enc_ctx->sample_rate};
            }

            if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
                enc_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

            /* Third parameter can be used to pass settings to encoder */
            ret = avcodec_open2(enc_ctx, encoder, NULL);
            if (ret < 0) {
                av_log(NULL, AV_LOG_ERROR, "Cannot open video encoder for stream #%u\n", i);
                return ret;
            }
            ret = avcodec_parameters_from_context(out_stream->codecpar, enc_ctx);
            if (ret < 0) {
                av_log(NULL, AV_LOG_ERROR, "Failed to copy encoder parameters to output stream #%u\n", i);
                return ret;
            }

            out_stream->time_base = enc_ctx->time_base;
            stream_ctx[i].enc_ctx = enc_ctx;
        } else if (dec_ctx->codec_type == AVMEDIA_TYPE_UNKNOWN) {
            av_log(NULL, AV_LOG_FATAL, "Elementary stream #%d is of unknown type, cannot proceed\n", i);
            return AVERROR_INVALIDDATA;
        } else {
            /* if this stream must be remuxed */
            ret = avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar);
            if (ret < 0) {
                av_log(NULL, AV_LOG_ERROR, "Copying parameters for stream #%u failed\n", i);
                return ret;
            }
            out_stream->time_base = in_stream->time_base;
        }

    }
    av_dump_format(ofmt_ctx, 0, filename, 1);

    if (!(ofmt_ctx->oformat->flags & AVFMT_NOFILE)) {
        ret = avio_open(&ofmt_ctx->pb, filename, AVIO_FLAG_WRITE);
        if (ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "Could not open output file '%s'", filename);
            return ret;
        }
    }

    /* init muxer, write output file header */
    ret = avformat_write_header(ofmt_ctx, NULL);
    if (ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Error occurred when opening output file\n");
        return ret;
    }

    return 0;
}

static int init_filter(FilteringContext* fctx, AVCodecContext *dec_ctx,
        AVCodecContext *enc_ctx, const char *filter_spec)
{
    char args[512];
    int ret = 0;
    const AVFilter *buffersrc = NULL;
    const AVFilter *buffersink = NULL;
    AVFilterContext *buffersrc_ctx = NULL;
    AVFilterContext *buffersink_ctx = NULL;
    AVFilterInOut *outputs = avfilter_inout_alloc();
    AVFilterInOut *inputs  = avfilter_inout_alloc();
    AVFilterGraph *filter_graph = avfilter_graph_alloc();

    if (!outputs || !inputs || !filter_graph) {
        ret = AVERROR(ENOMEM);
        goto end;
    }

    if (dec_ctx->codec_type == AVMEDIA_TYPE_VIDEO) {
        buffersrc = avfilter_get_by_name("buffer");
        buffersink = avfilter_get_by_name("buffersink");
        if (!buffersrc || !buffersink) {
            av_log(NULL, AV_LOG_ERROR, "filtering source or sink element not found\n");
            ret = AVERROR_UNKNOWN;
            goto end;
        }

        snprintf(args, sizeof(args),
                "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
                dec_ctx->width, dec_ctx->height, dec_ctx->pix_fmt,
                dec_ctx->time_base.num, dec_ctx->time_base.den,
                dec_ctx->sample_aspect_ratio.num,
                dec_ctx->sample_aspect_ratio.den);

        ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in",
                args, NULL, filter_graph);
        if (ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "Cannot create buffer source\n");
            goto end;
        }

        ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out",
                NULL, NULL, filter_graph);
        if (ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "Cannot create buffer sink\n");
            goto end;
        }

        ret = av_opt_set_bin(buffersink_ctx, "pix_fmts",
                (uint8_t*)&enc_ctx->pix_fmt, sizeof(enc_ctx->pix_fmt),
                AV_OPT_SEARCH_CHILDREN);
        if (ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "Cannot set output pixel format\n");
            goto end;
        }
    } else if (dec_ctx->codec_type == AVMEDIA_TYPE_AUDIO) {
        buffersrc = avfilter_get_by_name("abuffer");
        buffersink = avfilter_get_by_name("abuffersink");
        if (!buffersrc || !buffersink) {
            av_log(NULL, AV_LOG_ERROR, "filtering source or sink element not found\n");
            ret = AVERROR_UNKNOWN;
            goto end;
        }

        if (!dec_ctx->channel_layout)
            dec_ctx->channel_layout =
                av_get_default_channel_layout(dec_ctx->channels);
        snprintf(args, sizeof(args),
                "time_base=%d/%d:sample_rate=%d:sample_fmt=%s:channel_layout=0x%"PRIx64,
                dec_ctx->time_base.num, dec_ctx->time_base.den, dec_ctx->sample_rate,
                av_get_sample_fmt_name(dec_ctx->sample_fmt),
                dec_ctx->channel_layout);
        ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in",
                args, NULL, filter_graph);
        if (ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "Cannot create audio buffer source\n");
            goto end;
        }

        ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out",
                NULL, NULL, filter_graph);
        if (ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "Cannot create audio buffer sink\n");
            goto end;
        }

        ret = av_opt_set_bin(buffersink_ctx, "sample_fmts",
                (uint8_t*)&enc_ctx->sample_fmt, sizeof(enc_ctx->sample_fmt),
                AV_OPT_SEARCH_CHILDREN);
        if (ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "Cannot set output sample format\n");
            goto end;
        }

        ret = av_opt_set_bin(buffersink_ctx, "channel_layouts",
                (uint8_t*)&enc_ctx->channel_layout,
                sizeof(enc_ctx->channel_layout), AV_OPT_SEARCH_CHILDREN);
        if (ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "Cannot set output channel layout\n");
            goto end;
        }

        ret = av_opt_set_bin(buffersink_ctx, "sample_rates",
                (uint8_t*)&enc_ctx->sample_rate, sizeof(enc_ctx->sample_rate),
                AV_OPT_SEARCH_CHILDREN);
        if (ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "Cannot set output sample rate\n");
            goto end;
        }
    } else {
        ret = AVERROR_UNKNOWN;
        goto end;
    }

    /* Endpoints for the filter graph. */
    outputs->name       = av_strdup("in");
    outputs->filter_ctx = buffersrc_ctx;
    outputs->pad_idx    = 0;
    outputs->next       = NULL;

    inputs->name       = av_strdup("out");
    inputs->filter_ctx = buffersink_ctx;
    inputs->pad_idx    = 0;
    inputs->next       = NULL;

    if (!outputs->name || !inputs->name) {
        ret = AVERROR(ENOMEM);
        goto end;
    }

    if ((ret = avfilter_graph_parse_ptr(filter_graph, filter_spec,
                    &inputs, &outputs, NULL)) < 0)
        goto end;

    if ((ret = avfilter_graph_config(filter_graph, NULL)) < 0)
        goto end;

    /* Fill FilteringContext */
    fctx->buffersrc_ctx = buffersrc_ctx;
    fctx->buffersink_ctx = buffersink_ctx;
    fctx->filter_graph = filter_graph;

end:
    avfilter_inout_free(&inputs);
    avfilter_inout_free(&outputs);

    return ret;
}

static int init_filters(void)
{
    const char *filter_spec;
    unsigned int i;
    int ret;
    filter_ctx = av_malloc_array(ifmt_ctx->nb_streams, sizeof(*filter_ctx));
    if (!filter_ctx)
        return AVERROR(ENOMEM);

    for (i = 0; i < ifmt_ctx->nb_streams; i++) {
        filter_ctx[i].buffersrc_ctx  = NULL;
        filter_ctx[i].buffersink_ctx = NULL;
        filter_ctx[i].filter_graph   = NULL;
        if (!(ifmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO
                || ifmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO))
            continue;


        if (ifmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
            filter_spec = "null"; /* passthrough (dummy) filter for video */
        else
            filter_spec = "anull"; /* passthrough (dummy) filter for audio */
        ret = init_filter(&filter_ctx[i], stream_ctx[i].dec_ctx,
                stream_ctx[i].enc_ctx, filter_spec);
        if (ret)
            return ret;
    }
    return 0;
}

static int encode_write_frame(AVFrame *filt_frame, unsigned int stream_index, int *got_frame) {
    int ret;
    int got_frame_local;
    AVPacket enc_pkt;
    int (*enc_func)(AVCodecContext *, AVPacket *, const AVFrame *, int *) =
        (ifmt_ctx->streams[stream_index]->codecpar->codec_type ==
         AVMEDIA_TYPE_VIDEO) ? avcodec_encode_video2 : avcodec_encode_audio2;

    if (!got_frame)
        got_frame = &got_frame_local;

    av_log(NULL, AV_LOG_INFO, "Encoding frame\n");
    /* encode filtered frame */
    enc_pkt.data = NULL;
    enc_pkt.size = 0;
    av_init_packet(&enc_pkt);
    ret = enc_func(stream_ctx[stream_index].enc_ctx, &enc_pkt,
            filt_frame, got_frame);
    av_frame_free(&filt_frame);
    if (ret < 0)
        return ret;
    if (!(*got_frame))
        return 0;

    /* prepare packet for muxing */
    enc_pkt.stream_index = stream_index;
    av_packet_rescale_ts(&enc_pkt,
                         stream_ctx[stream_index].enc_ctx->time_base,
                         ofmt_ctx->streams[stream_index]->time_base);

    av_log(NULL, AV_LOG_DEBUG, "Muxing frame\n");
    /* mux encoded frame */
    ret = av_interleaved_write_frame(ofmt_ctx, &enc_pkt);
    return ret;
}

static int filter_encode_write_frame(AVFrame *frame, unsigned int stream_index)
{
    int ret;
    AVFrame *filt_frame;

    av_log(NULL, AV_LOG_INFO, "Pushing decoded frame to filters\n");
    /* push the decoded frame into the filtergraph */
    ret = av_buffersrc_add_frame_flags(filter_ctx[stream_index].buffersrc_ctx,
            frame, 0);
    if (ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Error while feeding the filtergraph\n");
        return ret;
    }

    /* pull filtered frames from the filtergraph */
    while (1) {
        filt_frame = av_frame_alloc();
        if (!filt_frame) {
            ret = AVERROR(ENOMEM);
            break;
        }
        av_log(NULL, AV_LOG_INFO, "Pulling filtered frame from filters\n");
        ret = av_buffersink_get_frame(filter_ctx[stream_index].buffersink_ctx,
                filt_frame);
        if (ret < 0) {
            /* if no more frames for output - returns AVERROR(EAGAIN)
             * if flushed and no more frames for output - returns AVERROR_EOF
             * rewrite retcode to 0 to show it as normal procedure completion
             */
            if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
                ret = 0;
            av_frame_free(&filt_frame);
            break;
        }

        filt_frame->pict_type = AV_PICTURE_TYPE_NONE;
        ret = encode_write_frame(filt_frame, stream_index, NULL);
        if (ret < 0)
            break;
    }

    return ret;
}

static int flush_encoder(unsigned int stream_index)
{
    int ret;
    int got_frame;

    if (!(stream_ctx[stream_index].enc_ctx->codec->capabilities &
                AV_CODEC_CAP_DELAY))
        return 0;

    while (1) {
        av_log(NULL, AV_LOG_INFO, "Flushing stream #%u encoder\n", stream_index);
        ret = encode_write_frame(NULL, stream_index, &got_frame);
        if (ret < 0)
            break;
        if (!got_frame)
            return 0;
    }
    return ret;
}

int main(int argc, char **argv)
{
    int ret;
    AVPacket packet = { .data = NULL, .size = 0 };
    AVFrame *frame = NULL;
    enum AVMediaType type;
    unsigned int stream_index;
    unsigned int i;
    int got_frame;
    int (*dec_func)(AVCodecContext *, AVFrame *, int *, const AVPacket *);

    if (argc != 3) {
        av_log(NULL, AV_LOG_ERROR, "Usage: %s <input file> <output file>\n", argv[0]);
        return 1;
    }

    if ((ret = open_input_file(argv[1])) < 0)
        goto end;
    if ((ret = open_output_file(argv[2])) < 0)
        goto end;
    if ((ret = init_filters()) < 0)
        goto end;

    /* read all packets */
    while (1) {
        if ((ret = av_read_frame(ifmt_ctx, &packet)) < 0)
            break;
        stream_index = packet.stream_index;
        type = ifmt_ctx->streams[packet.stream_index]->codecpar->codec_type;
        av_log(NULL, AV_LOG_DEBUG, "Demuxer gave frame of stream_index %u\n",
                stream_index);

        if (filter_ctx[stream_index].filter_graph) {
            av_log(NULL, AV_LOG_DEBUG, "Going to reencode&filter the frame\n");
            frame = av_frame_alloc();
            if (!frame) {
                ret = AVERROR(ENOMEM);
                break;
            }
            av_packet_rescale_ts(&packet,
                                 ifmt_ctx->streams[stream_index]->time_base,
                                 stream_ctx[stream_index].dec_ctx->time_base);
            dec_func = (type == AVMEDIA_TYPE_VIDEO) ? avcodec_decode_video2 :
                avcodec_decode_audio4;
            ret = dec_func(stream_ctx[stream_index].dec_ctx, frame,
                    &got_frame, &packet);
            if (ret < 0) {
                av_frame_free(&frame);
                av_log(NULL, AV_LOG_ERROR, "Decoding failed\n");
                break;
            }

            if (got_frame) {
                frame->pts = frame->best_effort_timestamp;
                ret = filter_encode_write_frame(frame, stream_index);
                av_frame_free(&frame);
                if (ret < 0)
                    goto end;
            } else {
                av_frame_free(&frame);
            }
        } else {
            /* remux this frame without reencoding */
            av_packet_rescale_ts(&packet,
                                 ifmt_ctx->streams[stream_index]->time_base,
                                 ofmt_ctx->streams[stream_index]->time_base);

            ret = av_interleaved_write_frame(ofmt_ctx, &packet);
            if (ret < 0)
                goto end;
        }
        av_packet_unref(&packet);
    }

    /* flush filters and encoders */
    for (i = 0; i < ifmt_ctx->nb_streams; i++) {
        /* flush filter */
        if (!filter_ctx[i].filter_graph)
            continue;
        ret = filter_encode_write_frame(NULL, i);
        if (ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "Flushing filter failed\n");
            goto end;
        }

        /* flush encoder */
        ret = flush_encoder(i);
        if (ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "Flushing encoder failed\n");
            goto end;
        }
    }

    av_write_trailer(ofmt_ctx);
end:
    av_packet_unref(&packet);
    av_frame_free(&frame);
    for (i = 0; i < ifmt_ctx->nb_streams; i++) {
        avcodec_free_context(&stream_ctx[i].dec_ctx);
        if (ofmt_ctx && ofmt_ctx->nb_streams > i && ofmt_ctx->streams[i] && stream_ctx[i].enc_ctx)
            avcodec_free_context(&stream_ctx[i].enc_ctx);
        if (filter_ctx && filter_ctx[i].filter_graph)
            avfilter_graph_free(&filter_ctx[i].filter_graph);
    }
    av_free(filter_ctx);
    av_free(stream_ctx);
    avformat_close_input(&ifmt_ctx);
    if (ofmt_ctx && !(ofmt_ctx->oformat->flags & AVFMT_NOFILE))
        avio_closep(&ofmt_ctx->pb);
    avformat_free_context(ofmt_ctx);

    if (ret < 0)
        av_log(NULL, AV_LOG_ERROR, "Error occurred: %s\n", av_err2str(ret));

    return ret ? 1 : 0;
}

代码核心流程介绍:

  • 调用open_input_file打开输入文件,先调用avformat_open_input进行解封装,然后调用avformat_find_stream_info判断是否存在音视频流信息,如果存在,则根据video/audio stream的codec id,调用avcodec_find_decoder->avcodec_alloc_context3->avcodec_open2完成音视频解码器的创建
  • 接着调用open_output_file,先调用avformat_alloc_output_context2创建mux的上下文,然后根据input stream info,调用avformat_new_stream创建对应的输出流,接着再调用avcodec_find_encoder->avcodec_alloc_context3->avcodec_open2创建对应的编码器(上述例子中,mux output stream codec id跟输入是保持一致,实际转码可以不一致)
  • 不管是input AVStream还是output AVStream,它们跟对应的编解码器(AVCodecContext)其实是没有强关联的,但是在创建编解codec之前,需要调用avcodec_parameters_from_context,将stream->codecpar和AVCodecContext中编解码器的配置参数做下同步
  • 调用init_filters创建滤镜(滤镜的创建下面再介绍)
  • 调用av_read_frame读取待解码的原始帧数据,接着调用avcodec_decode_video2/avcodec_decode_audio4进行解码
  • 如果存在filter graph,则将解码后的AVFrame传入filter graph添加滤镜
  • 再调用avcodec_encode_video2/avcodec_encode_audio2对数据帧进行编码
  • 编码完成后,再调用av_packet_rescale_ts设置AVPacket的time stamp
  • 最后调用av_interleaved_write_frame写入output file中

帧数据存储

ffmpeg使用AVPacket来存储编码的帧数据,解码后的音视频帧数据,统一使用AVFrame来存储。

音频帧数据存储音频解码后的pcm数据,在ffmpeg内部有Packed和Planar两种存储方式:

  • Packed: L R L R L R L
  • RPlanar: L L L L R R R

RPacked格式,frame.data0或frame.extended_data0包含AVFrame保存的所有pcm数据

Planar格式,frame.datai或者frame.extended_datai表示第i个声道的数据

AVFrame.data数组大小固定为8,如果声道数超过8,需要从frame.extended_data获取声道数据, extended_data是为了支持更多的声道数,后期扩展的字段。

Planar是ffmpeg内部的数据格式,常规的都为Packed,从命名上,Planar一般都在Packed命名后+P,比如sample format为16bit,Packed命名:AV_SAMPLE_FMT_S16,Planar:AV_SAMPLE_FMT_S16P。

除了data/extended_data,AVFrame保存音频数据其他四个核心字段:format(AVSampleFormat),sample_rate,channel_layout, nb_samples。

format - 指的是单帧的存储格式,可以通过该值来确定是packed还是planar,以及存储大小,16bit或float等

sample_rate - 采样率

channel_layout - 声道数

nb_samples - AVFrame中包含的采样数(单声道)

所以,我们通过sample_rate和nb_samples就可以得出AVFrame包含pcm数据的播放时长,通过format channel_layout nb_samples就可以得出AVFrame中buffer的长度

**视频帧数据存储

**视频帧数据采用YUV格式,其中Y指亮度通道,UV指色度通道,主流的采样格式有:YUV444、YUV422、YUV420

YUV4: : , 简单点理解就是,以4个像素为一采样组,每个像素固定有一个Y通道,YUV后面UV对应的数值,以2为单位对应一组UV色度通道,注意,UV是一组,不要将其分开,基于这个去理解YUV4:2:2和YUV4:2:0采样方式,会更容易点

对于大分辨率的视频帧,相邻像素的色度通道差异是极小的,所以YUV420格式在保证图像质量的情况下,又大幅的降低了存储,是目前主流的帧格式,接下去重点介绍下YUV420在ffmpeg,即AVFrame里的存储,YUV420根据Y,U,V数据的存储方式,又细分出YUV420P,YUV420SP(NV12)等子格式

YUV420P: YYYYYYYY UU VV

Y分量、U分量、V分量分别占一个平面空间,4个像素的Y分量共用一个UV分量

YUV420SP: YYYYYYYY UU VV

Y分量占一个平面空间,UV交差存储占一个平面空间,4个像素的Y分量共用一个UV分量

二者的差异,就是UV分量的存储方式,AVFrame保存YUV数据,主要用

ini 复制代码
uint8_t *data[AV_NUM_DATA_POINTERS];
int linesize[AV_NUM_DATA_POINTERS];

data保存平面对应的向量数据,linesize保存平面对应向量数据的长度,所以yuv420p有三个平面,data数组有效长度为3,yuv420sp只有两个平面,有效长度就只有2。

时间基 - TimeBase

ffmpeg音视频处理,有一个很重要的概念,那就是时间基,时间基本质是时间刻度,ffmpeg内部用到的时间戳都是要与对应的time base换算才能拿到准确时间的,ffmpeg内部主要有三种类型的time base

  • tbr - 表示帧率,tbr往往跟fps相同,比如帧率25,time base即为 1/25
  • tbn - 表示视频流(AVStream) 的timebase,在解码时,从视频source demux后,可以拿到video container的stream time base,作为后续视频解码和seek等操作时的基准时间;在编码时,AVPacket在写入文件前,必须要正确的设置AVStream的time base和pts,要不编码完成的视频播放会异常
  • tbc - 表示视频流 codec timebase,编码时,需要设置codec context的time base

有了时间基和时间戳,可以很容易计算出对应的时间,比如时间戳50,时间基1/25
50 * 1 / 25 = 2s

ffmpeg也提供了辅助计算函数:
timestamp(秒) = pts * av_q2d(time_base)

不同时间基之间换算:
av_rescale_q

对AVPacket内部时间基的换算:
av_packet_rescale_ts

滤镜 - Filter

ffmpeg内部filter graph处理流程

filter graph建立后,会监听source filter的source buffer,如果有AVFrame塞入,filter graph就开始运作,通过filter chain处理完后,从sink filter的sink buffer中取出,格式为AVFrame。

上述工程里initFilter代码流程:

  • 先调用avfilter_graph_alloc创建filter graph
  • 接着调用avfilter_get_by_name创建命名的filter
  • 接着基于创建的filter,调用avfilter_graph_create_filter创建AVFilterContext,上下文会关联filter和filter graph等其他信息
  • 最后调用avfilter_graph_parse_ptr,将通过filter spec创建的filter以及咱们创建的input和output filter插入到filter graph

然后在帧处理的时候:

  • 调用av_buffersrc_add_frame_flags将待处理帧塞入buffersrc_ctx,触发filter graph开发工作
  • 接着调用av_buffersink_get_frame从buffersink_ctx处理完成的帧数据

步骤2和3创建了source和sink filter,work filter则是使用avfilter_graph_parse_ptr传入filter spec,ffmpeg会解析spec创建内置的filter,当然,我们也可以实现自定义的filter塞入到filte graph中。

下面是添加水印的filter spec:

scss 复制代码
static const char* filters[] = {
        "movie=/sdcard/0/pet_logo.png[watermark];[in][watermark]overlay=main_w-overlay_w-10:main_h-overlay_h-10[out]"};

filter spec更多介绍,可以去官网查看:ffmpeg.org/ffmpeg-filt...

(本文作者:胡付义)

关注公众号「哈啰技术」,第一时间收到最新技术推文。

相关推荐
weixin_382395231 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__1 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.2 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~3 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华4 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子6 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅6 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni6 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学8 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端