NDK学习笔记:FFmpeg音视频同步1(视频解码+音频解码)

NDK学习笔记:FFmpeg音视频同步1(视频解码+音频解码)

 

0、开篇先来点废话

本篇文章开始,我和大家一起来讨论这个经久不衰的音视频开发的难点 —— 音视频同步。囊括内容比较多,大到代码组织,小至C语法糖,尽力做到每一个像我一样的菜鸡都能掌握解决方法。

正式开始之前,我又想起了之前利用OpenGLES+MediaCodec的水印录制系列文章,当时没有处理音频,合成出来的mp4只有图像。其实随后我已经添加上去音频处理的代码,就是目录当中的视频编码工作类的核心CameraRecordEncoderCore2,版本2是在CameraRecordEncoderCore的基础上增加AudioRecord处理音频。使用方法也很简单,只要在编码工作类CameraRecordEncoder当中把CameraRecordEncoderCore的引用改为CameraRecordEncoderCore2,并且启动音频录制audioRecord,喂养音频数据drainAudioEncoder就可以了。有疑问的同学,详细细节可以私信联系。

为何我会提起这个呢,其实是因为 FFmpeg音视频同步解决方案的设计思路,是借鉴上方CameraRecordEncoderCore2的工作模式。所以我建议同学们如果有时间,还是去看看CameraRecordEncoderCore2。

 

1、如何线程分离

之前的文章,都是一些FFmpeg的教学例子,全都是运行在主线程,并不专业。来到这里我们就要开始向专业靠近了,所以我们第一步就是要改造以前的代码,结合POSIX线程进行音视频的解码流程。

线程解码要怎么做了?思维敏捷的同学可能第一反正就是,一个线程读取数据包(av_read_frame)并发送到对应解码器(avcodec_send_packet);另一个线程就不断的从解码上接收AVFrame(avcodec_receive_frame)并做对应的处理。balabala的就把线程解码改造好了,堪称完美的代码逻辑大致如下:

// run on thread
void read_avpakcet()
{
    while (av_read_frame(pFormatContext, pkt) >= 0)
    {
        if (pkt->stream_index == video_stream_index)
        {
            avcodec_send_packet(videoCodecCtx, packet);
        }
        if (pkt->stream_index == audio_stream_index)
        {
            avcodec_send_packet(audioCodecCtx, packet);
        }
    }
}
// run on the other thread
void handle_video_frame()
{
    while(1)
    {
        int ret = avcodec_receive_frame(videoCodecCtx, yuv_frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF){
            LOGD("avcodec_receive_frame:%d\n", ret);
            break;
        }else if (ret < 0) {
            LOGW("avcodec_receive_frame:%d\n", AVERROR(ret));
            goto end;  //end处进行资源释放等善后处理
        }	
		
		if (ret >= 0) {
			// handle avframe ...
		}
    }
}

运行发现黑屏!并且avcodec_receive_frame的返回值一直是 -541478725 ???通过FFMPEG的错误速查,发现是AVERROR_EOF?!这里深层的原因需要对avcodec_send_packet 和 avcodec_receive_frame进行源码分析,暂且不在本篇文章的内容范围,有兴趣的同学可以参考这篇文章,看看自己是否能参悟透彻,不明白的可以联系我一起讨论讨论。

现在我们暂且认为AVCodecContext是一个非线程安全的对象吧。 既然不能这样子做,那该怎么改造?既然AVCodecContext非线程安全,那么肯定的是 avcodec_send_packet 和 avcodec_receive_frame要在同一线程下工作。那么我只能在解封装格式上下文(AVFormatContext)之后,就要开始分开两路工作线程,并且把对应的数据包(AVPacket)分别缓存。建议流程如图所示:

虽然简陋,但是简单明了。显而易见的第一个问题重点,怎样缓存AVPacket ?

 

 

 

在解决问题之前,还是先让我把重构的代码呈现出来,这些整理规范后的代码,或多或少已经可以用到实际的开发当中了,希望能帮助到大家。

public class SyncPlayer {
    private Context context;
    private String media_input_str;
    private Surface surface;

    public SyncPlayer(Context context) {
        this.context = context;
        nativeInit();
    }
    public void setMediaSource(String media_input_str){
        this.media_input_str = media_input_str;
    }
    public void setRender(Surface surface){
        this.surface = surface;
    }
    public void prepare() {
        nativePrepare(media_input_str, surface);
    }
    public void play() {
        nativePlay();
    }
    public void release() {
        nativeRelease();
        media_input_str = null;
        surface = null;
    }

    private native void nativeInit();
    private native void nativePrepare(String media_input_str, Surface surface);
    private native int nativePlay();
    private native void nativeRelease();

    static
    {
        try {
            System.loadLibrary("yuv");
            System.loadLibrary("avutil");
            System.loadLibrary("swscale");
            System.loadLibrary("swresample");
            System.loadLibrary("avcodec");
            System.loadLibrary("avformat");
            System.loadLibrary("postproc");
            System.loadLibrary("avfilter");
            System.loadLibrary("avdevice");

            System.loadLibrary("sync-player");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public AudioTrack createAudioTrack(int sampleRateInHz, int nb_channels){
        //音频码流编码格式
        int encodingFormat = AudioFormat.ENCODING_PCM_16BIT;
        //声道布局
        int channelConfig;
        if(nb_channels == 1){
            channelConfig = android.media.AudioFormat.CHANNEL_OUT_MONO;
        } else {
            channelConfig = android.media.AudioFormat.CHANNEL_OUT_STEREO;
        }

        AudioManager mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
        int bufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, encodingFormat);
        int sessionId = mAudioManager.generateAudioSessionId();
        AudioAttributes audioAttributes = new AudioAttributes.Builder()
                .setUsage(AudioAttributes.USAGE_MEDIA)
                .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                .build();
        AudioFormat audioFormat = new AudioFormat.Builder().setSampleRate(sampleRateInHz)
                .setEncoding(encodingFormat)
                .setChannelMask(channelConfig)
                .build();
        AudioTrack mAudioTrack = new AudioTrack(audioAttributes, audioFormat, bufferSize + 2048, AudioTrack.MODE_STREAM, sessionId);
        return mAudioTrack;
    }

}

首先是Java层入口类SyncPlayer,和以前的学习例子有点区别,准备四个native方法,分别是nativeInit、nativePrepare、nativePlay和nativeRelease,而且全部声明为private私有的,不对外提供调用。

然后构造函数传入Context,以便createAudioTrack方法中使用新API创建AudioTrack对象,顺带nativeInit。setMediaSource / setRender两个方法只是为nativePrepare作准备,准备之后就可以nativePlay播放了。之后要记住回收资源nativeRelease。       

 

下一步就实现nativeInit / nativeRelease / nativePrepare 三个方法,nativePlay的部分代码。这四个方法其实就是对以前的例子进行一些规范的封装,要注意一点就是对应资源的回收,NDK开发一定一定要做好内存资源的回收,要不然就会造成内存泄漏。

我们新建sync_player.c文件,从 nativeInit 开始。

typedef struct _SyncPlayer {
    // SyncPlayer的java对象,需要建立引用 NewGlobalRef,记得DeleteGlobalRef
    jobject jinstance;
    ... ...
} SyncPlayer;


SyncPlayer* mSyncPlayer; // 全局变量,可以理解为c层的SyncPlayer对象


JNIEXPORT void JNICALL
Java_org_zzrblog_ffmp_SyncPlayer_nativeInit(JNIEnv *env, jobject instance)
{
    av_log_set_callback(ffmpeg_custom_log);
    av_register_all();
    avcodec_register_all();
    avformat_network_init();
    // malloc与calloc区别
    // 1.malloc是以字节为单位,calloc是以item为单位。
    // 2.malloc需要memset初始化为0,calloc默认初始化为0
    mSyncPlayer = (SyncPlayer*)calloc(1, sizeof(SyncPlayer));
    // SyncPlayer的java对象,需要建立引用 NewGlobalRef,记得DeleteGlobalRef
    mSyncPlayer->jinstance = (*env)->NewGlobalRef(env, instance);
}

JNIEXPORT void JNICALL
Java_org_zzrblog_ffmp_SyncPlayer_nativeRelease(JNIEnv *env, jobject instance)
{
    // 释放 SyncPlayer
    (*env)->DeleteGlobalRef(env, mSyncPlayer->jinstance);
    free(mSyncPlayer);
    mSyncPlayer = NULL; //防止野指针
}

这里的SyncPlayer是一个结构体,因为不是写cpp工程,所以我们用struct结构体来代替class类对象。我们暂且不清楚结构体需要什么变量,先保存一个SyncPlayer的全局引用吧。 此时我们就可以立刻在nativeRelease当中删除这个全局引用了,释放结构体内存。 


JNIEXPORT void JNICALL
Java_org_zzrblog_ffmp_SyncPlayer_nativePrepare(JNIEnv *env, jobject instance,
                                               jstring media_input_jstr, jobject jSurface)
{
    if(mSyncPlayer == NULL) {
        LOGE("%s","请调用函数:nativeInit");
        return;
    }
    const char *media_input_cstr = (*env)->GetStringUTFChars(env, media_input_jstr, 0);

    AVFormatContext *pFormatContext = avformat_alloc_context();
    // 打开输入视频文件
    if(avformat_open_input(&pFormatContext, media_input_cstr, NULL, NULL) != 0){
        LOGE("%s","打开输入视频文件失败");
        return;
    }
    // 获取视频信息
    if(avformat_find_stream_info(pFormatContext,NULL) < 0){
        LOGE("%s","获取视频信息失败");
        return;
    }

    int video_stream_idx = -1;
    int audio_stream_idx = -1;
    for(int i=0; i<pFormatContext->nb_streams; i++)
    {
        enum AVMediaType meida_type = pFormatContext->streams[i]->codecpar->codec_type;
        switch(meida_type)
        {
            case AVMEDIA_TYPE_VIDEO:
                video_stream_idx = i;
                break;
            case AVMEDIA_TYPE_AUDIO:
                audio_stream_idx = i;
                break;
            default:
                continue;
        }
    }
    LOGD("VIDEO的索引位置:%d", video_stream_idx);
    LOGD("AUDIO的索引位置:%d", audio_stream_idx);

    mSyncPlayer->input_format_ctx = pFormatContext;
    mSyncPlayer->num_streams = pFormatContext->nb_streams;
    mSyncPlayer->audio_stream_index = audio_stream_idx;
    mSyncPlayer->video_stream_index = video_stream_idx;
    // 开辟nb_streams个空间,每个都是指针 (AVCodecContext* )
    mSyncPlayer->input_codec_ctx = calloc(pFormatContext->nb_streams, sizeof(AVCodecContext* ) );
    // 根据索引初始化对应的AVCodecContext,并放入mSyncPlayer.input_codec_ctx数组 对应的位置
    int ret ;
    ret = alloc_codec_context(mSyncPlayer, video_stream_idx);
    if(ret < 0) return;
    ret = alloc_codec_context(mSyncPlayer, audio_stream_idx);
    if(ret < 0) return;

    // 初始化视频渲染相关 ANativeWindow是NDK对象,不需要NewGlobalRef
    mSyncPlayer->native_window = ANativeWindow_fromSurface(env, jSurface);
    // 初始化音频播放相关 audio_track是java对象,需要NewGlobalRef,记得DeleteGlobalRef
    ret = initAudioTrack(mSyncPlayer, env);
    if(ret < 0) return;

    (*env)->ReleaseStringUTFChars(env, media_input_jstr, media_input_cstr);
}

紧接着我们就可以开始准备工作,进入nativePrepare方法,打开资源文件上下文,检索音视频索引这些都是模板代码了。按着流程走下来,我们就是根据索引值打开对应的解码器,获取解码上下文指针(AVCodecContext*),这样的解码上下文指针起码有两个或者以上,所以我们在SyncPlayer结构体创建一个AVCodecContext* 的数组,即一个AVCodecContext的二级指针。并根据AVFormatContext->nb_streams的流通道数,sizeof(AVCodecContext*)解码上下文指针为单元,创建内存空间。这是因为往后的(字幕)扩展预留的。

// 根据 stream_idx流索引,初始化对应的AVCodecContext,并保存到SyncPlayer
int alloc_codec_context(SyncPlayer *player,int stream_idx)
{
    AVFormatContext *pFormatContext = player->input_format_ctx;

    AVCodec *pCodec = avcodec_find_decoder(pFormatContext->streams[stream_idx]->codecpar->codec_id);
    if(pCodec == NULL){
        LOGE("无法获取 %d 的解码器",stream_idx);
        return -1;
    }
    AVCodecContext *pCodecContext = avcodec_alloc_context3(pCodec);
    if(pCodecContext == NULL) {
        LOGE("创建 %d 解码器对应的上下文失败.", stream_idx);
        return -2;
    }
    int ret = avcodec_parameters_to_context(pCodecContext, pFormatContext->streams[stream_idx]->codecpar);
    if(ret < 0) {
        LOGE("avcodec_parameters_to_context:%d\n", AVERROR(ret));
        return -3;
    }
    if(avcodec_open2(pCodecContext, pCodec, NULL) < 0){
        LOGE("%s","解码器无法打开");
        return -4;
    }
    player->input_codec_ctx[stream_idx] = pCodecContext;
    return 0;
}

// 初始化音频相关的变量
int initAudioTrack(SyncPlayer* player, JNIEnv* env)
{
    AVCodecContext *audio_codec_ctx = player->input_codec_ctx[player->audio_stream_index];
    //重采样设置参数-------------start
    enum AVSampleFormat in_sample_fmt = audio_codec_ctx->sample_fmt;
    enum AVSampleFormat out_sample_fmt = AV_SAMPLE_FMT_S16;
    int in_sample_rate = audio_codec_ctx->sample_rate;
    int out_sample_rate = in_sample_rate;
    uint64_t in_ch_layout = audio_codec_ctx->channel_layout;
    uint64_t out_ch_layout = AV_CH_LAYOUT_STEREO;
    //16bit 44100 PCM 统一音频采样格式与采样率
    SwrContext *swr_ctx = swr_alloc();
    swr_alloc_set_opts(swr_ctx,
                       out_ch_layout,out_sample_fmt,out_sample_rate,
                       in_ch_layout,in_sample_fmt,in_sample_rate,
                       0, NULL);
    int ret = swr_init(swr_ctx);
    if(ret < 0) {
        LOGE("swr_init:%d\n", AVERROR(ret));
        return -1;
    }
    int out_channel_nb = av_get_channel_layout_nb_channels(out_ch_layout);
    //重采样设置参数-------------end
    // 保存设置
    player->in_sample_fmt = in_sample_fmt;
    player->out_sample_fmt = out_sample_fmt;
    player->in_sample_rate = in_sample_rate;
    player->out_sample_rate = out_sample_rate;
    player->out_channel_nb = out_channel_nb;
    player->swr_ctx = swr_ctx;

    //JNI AudioTrack-------------start
    jobject jthiz = player->jinstance;
    jclass player_class = (*env)->GetObjectClass(env, jthiz);
    //AudioTrack对象
    jmethodID create_audio_track_mid = (*env)->GetMethodID(env,player_class,"createAudioTrack","(II)Landroid/media/AudioTrack;");
    jobject audio_track = (*env)->CallObjectMethod(env,jthiz,create_audio_track_mid,player->out_sample_rate,player->out_channel_nb);
    //调用AudioTrack.play方法
    jclass audio_track_class = (*env)->GetObjectClass(env,audio_track);
    jmethodID audio_track_play_mid = (*env)->GetMethodID(env,audio_track_class,"play","()V");
    player->audio_track_play_mid = audio_track_play_mid;
    //AudioTrack.write
    jmethodID audio_track_write_mid = (*env)->GetMethodID(env,audio_track_class,"write","([BII)I");
    player->audio_track_write_mid = audio_track_write_mid;
    //JNI AudioTrack-------------end
    player->audio_track = (*env)->NewGlobalRef(env,audio_track);
    return 0;
}

随后我们进入自定义函数alloc_codec_context,根据 stream_idx流索引,初始化对应的AVCodecContext,并保存到SyncPlayer。紧接着就是初始化初始化音视频相关的变量属性,都是模板代码了,不了解的同学可以查看之前的知识文章。

经过nativePrepare方法,我们已经成功获取音视频解码上下文,音频播放对象AudioTrack,视频渲染对象ANativeWindow,都分别保存到SyncPlayer结构体当中,持有其指针对象。此时的SyncPlayer应该有如下属性:

typedef struct _SyncPlayer {
    // 数据源 格式上下文
    AVFormatContext *input_format_ctx;
    // 流的总个数
    int num_streams;
    // 频视频流索引位置
    int video_stream_index;
    // 音视频流索引位置
    int audio_stream_index;
    // AVCodecContext 二级指针 动态数组
    // 长度为streams_num
    AVCodecContext * * input_codec_ctx;
    // SyncPlayer的java对象,需要建立引用 NewGlobalRef,记得DeleteGlobalRef
    jobject jinstance;
    // 视频渲染相关
    ANativeWindow* native_window;
    SwrContext *swr_ctx;
    // 音频播放相关
    enum AVSampleFormat in_sample_fmt;  //输入的采样格式
    enum AVSampleFormat out_sample_fmt; //输出采样格式16bit PCM
    int in_sample_rate;                 //输入采样率
    int out_sample_rate;                //输出采样率
    int out_channel_nb;                 //输出的声道个数
    // 音频播放对象 java对象,需要 NewGlobalRef,记得DeleteGlobalRef
    jobject* audio_track;
    jmethodID audio_track_play_mid;
    jmethodID audio_track_write_mid;
    // ... ...
} SyncPlayer;

nativeRelease方法也不能放松,各种内存空间的回收要时刻牢记:

JNIEXPORT void JNICALL
Java_org_zzrblog_ffmp_SyncPlayer_nativeRelease(JNIEnv *env, jobject instance)
{
    if(mSyncPlayer == NULL)
        return;
    if(mSyncPlayer->input_format_ctx == NULL){
        return;
    }
    // 释放音频相关
    (*env)->DeleteGlobalRef(env, mSyncPlayer->audio_track);
    swr_free(&(mSyncPlayer->swr_ctx));
    // 释放解码器
    for(int i=0; i<mSyncPlayer->num_streams; i++) {
        // 有可能出现为空,因为只保存了音视频的AVCodecContext,没有处理字幕流的
        // 但是空间还是按照num_streams的个数创建了
        AVCodecContext * pCodecContext = mSyncPlayer->input_codec_ctx[i];
        if(pCodecContext != NULL)
        {
            avcodec_close(pCodecContext);
            avcodec_free_context(&pCodecContext);
            pCodecContext = NULL; //防止野指针
        }
    }
    free(mSyncPlayer->input_codec_ctx);
    // 释放文件格式上下文
    avformat_close_input(&(mSyncPlayer->input_format_ctx));
    avformat_free_context(mSyncPlayer->input_format_ctx);
    mSyncPlayer->input_format_ctx = NULL;
    // 释放 SyncPlayer
    (*env)->DeleteGlobalRef(env, mSyncPlayer->jinstance);
    free(mSyncPlayer);
    mSyncPlayer = NULL;
}

 

由于篇幅关系,文章先到这里结束。四个native方法,剩下最关键的nativePlay还没实现,可以思考nativePlay方法要怎么实现。无非就一个线程采集AVPacket保存到一个缓冲区,一个线程获取视频AVPakcet并渲染到ANativeWindow,另外一线程获取音频AVPacket并进行播放。 那么采集的AVPacket怎么合适的存放到一个缓冲区?怎么高效的从av_read_frame的avpacket 保存 到缓冲区?这些问题留待下一文章解答。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr_Zzr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值