FFmpeg视频播放:音视频解码技术解析

发表时间: 2023-10-20 20:37

上一篇中FFmpeg解封装中在TinaFFmpeg中的prepare方法里把解码器上下文AVCodecContext交给VideoChannel,AudioChannel后,解码工作就交给它们来处理了,这节我们来看它们是如何处理的。

解码入口

prepare完成后会调用Java中TinaPlayer的onPrepare的方法,然后回调 PlayActivity的start方法,然后进入native层的start方法:

 LOGE("native prepare流程准备完毕");    // 准备完了 通知java 你随时可以开始播放 callHelper->onPrepare(THREAD_CHILD);//prepare完成后会调用Java中TinaPlayer的onPrepare的方法,然后回调 PlayActivity的start方法,然后进入native层的start方法


//TinaPlayerpublic void onPrepare(){        if (null != listener){            listener.onPrepare();        } }//PlayActivitytinaPlayer.setOnPrepareListener(new TinaPlayer.OnPrepareListener() {  @Override  public void onPrepare() {    runOnUiThread(new Runnable() {      @Override      public void run() {        Toast.makeText("开始播放").show();      }    });    //调用native_start    tinaPlayer.start();  } });//TinaPlayerpublic void start(){  native_start();}

native层:调用ffmpeg中的start方法,然后分别调用videoChannel、audioChannel的play()方法

extern "C"JNIEXPORT void JNICALLJava_tina_com_player_TinaPlayer_native_1start(JNIEnv *env, jobject instance) {    ffmpeg->start();}//TinaFFmpegvoid TinaFFmpeg::start() {    //重新开线程    isPlaying = 1;    if (audioChannel) {        //设置为工作状态        audioChannel->play();    }    if (videoChannel) {        //设置为工作状态        videoChannel->setAudioChannel(audioChannel);        videoChannel->play();    }    pthread_create(&pid_play, 0, play, this);}

兵马未动,粮草先行。把解码需要的数据源packet放入到SafeQueue的同步队列中去:

void *play(void *args) {    TinaFFmpeg *fFmpeg = static_cast<TinaFFmpeg *>(args);    fFmpeg->_start();    return 0;}void TinaFFmpeg::_start() {    int ret;    //1.读取媒体数据包(音频、视频数据)    while (isPlaying) {        //读取文件的时候没有网络请求,一下子读完了,可能导致oom        //特别是读本地文件的时候 一下子就读完了        if (audioChannel && audioChannel->packets.size() > 100) {            //10ms            av_usleep(1000 * 10);            continue;        }        if (videoChannel && videoChannel->packets.size() > 100) {            av_usleep(1000 * 10);            continue;        }        AVPacket *avPacket = av_packet_alloc();        ret = av_read_frame(formatContext, avPacket);        //等于0成功,其它失败        if (ret == 0) {            if (audioChannel && avPacket->stream_index == audioChannel->id) {                //todo 音频                //在audioChannel中执行 解码工作                audioChannel->packets.push(avPacket);            } else if (videoChannel && avPacket->stream_index == videoChannel->id) {                //在videoChannel中执行 解码工作                videoChannel->packets.push(avPacket);            }        } else if (ret == AVERROR_EOF) {            //读取完成,但是可能还没有播放完成            if (audioChannel->packets.empty() && audioChannel->frames.empty()                && videoChannel->packets.empty() && videoChannel->frames.empty()){                av_packet_free(&avPacket);//这里不释放会有内存泄漏,崩溃                break;            }        } else {            av_packet_free(&avPacket);            break;        }    }    isPlaying = 0;    audioChannel->stop();    videoChannel->stop();}

其实push队列跟audioChannel、videoChannel的play()的过程是同步的,是基于SafeQueue对象packets的生产者与消费者的问题处理,我们先来看视频解码。

视频解码

解码分为decode,render(渲染),创建两个线程,分别在各自的线程中处理。

void VideoChannel::play() {    isPlaying = 1;    packets.setWork(1);    frames.setWork(1);    //1.解码    pthread_create(&pid_decode, 0, decode_task, this);    //2. 播放    pthread_create(&pid_render, 0, render_task, this);}//解码void VideoChannel::decode() {    AVPacket *packet = 0;    while (isPlaying) {        int ret = packets.pop(packet);        if (!isPlaying) {            break;        }        if (!ret) {            continue;        }        //把包丢给解码器        ret = avcodec_send_packet(avCodecContext, packet);        releaseAvPacket(&packet);        while (ret != 0) {            break;        }        AVFrame *frame = av_frame_alloc();        avcodec_receive_frame(avCodecContext, frame);        if (ret == AVERROR(EAGAIN)) {            continue;        } else if (ret != 0) {            break;        }        //再开一个线程 播放。        frames.push(frame);    }    releaseAvPacket(&packet);}

decode的过程其实很简单,就是把packet交给 AVCodec处理,然后拿到frame,放入到frames队列中去,而frames就是我们render的数据源,下面来看render:

//渲染void VideoChannel::render() {    //目标: RGBA, 没有缩放    swsContext = sws_getContext(avCodecContext->width, avCodecContext->height,                                avCodecContext->pix_fmt,                                avCodecContext->width, avCodecContext->height, AV_PIX_FMT_RGBA,                                SWS_BILINEAR, 0, 0, 0);    //每个画面 刷新的间隔 单位:秒    double frame_delays = 1.0 / fps;    AVFrame *frame = 0;    uint8_t *dst_data[4];    int dst_linesize[4];    av_image_alloc(dst_data, dst_linesize, avCodecContext->width, avCodecContext->height,                   AV_PIX_FMT_RGBA, 1);    while (isPlaying) {        int ret = frames.pop(frame);        if (!isPlaying) {            break;        }        //src_lines: 每一行存放的 字节长度        sws_scale(swsContext, reinterpret_cast<const uint8_t *const *>(frame->data),                  frame->linesize, 0,                  avCodecContext->height,                  dst_data, dst_linesize);        //获取当前 一个画面播放的时间   ------------- 处理音视频同步(后面再讲)---------------        //把解出来的数据给到 window的Surface,回调出去进行播放callback(dst_data[0], dst_linesize[0], avCodecContext->width, avCodecContext>height);        releaseAvFrame(&frame);    }    av_freep(&dst_data[0]);    releaseAvFrame(&frame);    isPlaying = 0;    sws_freeContext(swsContext);    swsContext = 0;}

相关学习资料推荐,点击下方链接免费报名,先码住不迷路~】

音视频免费学习地址:
https://xxetb.xet.tech/s/2cGd0

【免费分享】音视频学习资料包、大厂面试题、技术视频和学习路线图,资料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以点击788280672加群免费领取~

看render的过程其实没有真正的处理渲染,只是将frame的数据data通过回调接口传出去了,TinaFFmpeg调用了这个回调,而这个回调的具体实现在JNI的入口函数里native-lib.cpp中:

//VideoChannelvoid VideoChannel::setRenderFrameCallback(RenderFrameCallback callback) {    this->callback = callback;}//TinaFFmpegvoid TinaFFmpeg::setRenderFrameCallback(RenderFrameCallback callback) {    this->callback = callback;}extern "C"JNIEXPORT void JNICALLJava_tina_com_player_TinaPlayer_native_1prepare(JNIEnv *env, jobject instance,                                                jstring dataSource_) {	....    ffmpeg->setRenderFrameCallback(render);    .....}

那么这个render的回调函数究竟是如何处理的呢,如何把datas给到SurfaceView中显示呢?

void render(uint8_t *data, int linesize, int w, int h) {    pthread_mutex_lock(&mutex);    if (!window) {        pthread_mutex_unlock(&mutex);        return;    }    //设置窗口属性    ANativeWindow_setBuffersGeometry(window, w, h,                                     WINDOW_FORMAT_RGBA_8888);    ANativeWindow_Buffer window_buffer;    if (ANativeWindow_lock(window, &window_buffer, 0)) {        ANativeWindow_release(window);        window = 0;        pthread_mutex_unlock(&mutex);        return;    }    //填充rgb数据给dst_data    uint8_t *dst_data = static_cast<uint8_t *>(window_buffer.bits);    //stride : 一行多少个数据 (RGBA) * 4    int dst_linesize = window_buffer.stride * 4;    //一行一行拷贝    for (int i = 0; i < window_buffer.height; ++i) {        memcpy(dst_data + i * dst_linesize, data + i * linesize, dst_linesize);    }    ANativeWindow_unlockAndPost(window);    pthread_mutex_unlock(&mutex);}

通过memcpy逐行拷贝dst_data中的数据,我们在看一遍render中的传参:

//把解出来的数据给到 window的Surface,回调出去进行播放callback(dst_data[0], dst_linesize[0], avCodecContext->width, avCodecContext->height);

这就把 videoChannel中解压的数据传给了上面的 NativeWindow中的buffer,NativeWindow作为Native层与Java层视频数据展现的承接容器,那具体的数据传输的介质是在TinaPlayer中通过setSurface传过来的SurfaceHolder中的Surface

/** * 画布创建OK*/@Overridepublic void surfaceCreated(SurfaceHolder holder) {  native_setSurface(holder.getSurface());}/*** 画布发生变化(横竖屏)*/@Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {  native_setSurface(holder.getSurface());}

至此,整个VideoChannal的解码、渲染工作完成,可以在PlayActivity中看到视频了,接下来处理声音:

音频解码

同样的两个线程,一个解码,一个播放

void *audio_decode(void *args) {    AudioChannel *audioChannel = static_cast<AudioChannel *>(args);    audioChannel->decode();    return 0;}void *audio_play(void *args) {    AudioChannel *audioChannel = static_cast<AudioChannel *>(args);    audioChannel->_play();    return 0;}void AudioChannel::play() {    packets.setWork(1);    frames.setWork(1);...... // 省略代码后续再讲 初始化音视频同步的Context    isPlaying = 1;    //1. 解码    pthread_create(&pid_audio_decode, 0, audio_decode, this);    //2. 播放    pthread_create(&pid_audio_player, 0, audio_play, this);}

解码过程一样:

//音频解码, 跟视频代码一样void AudioChannel::decode() {    AVPacket *packet = 0;    while (isPlaying) {        int ret = packets.pop(packet);        if (!isPlaying) {            break;        }        if (!ret) {            continue;        }        //把包丢给解码器        ret = avcodec_send_packet(avCodecContext, packet);        releaseAvPacket(&packet);        while (ret != 0) {            break;        }        AVFrame *frame = av_frame_alloc();        avcodec_receive_frame(avCodecContext, frame);        if (ret == AVERROR(EAGAIN)) {            continue;        } else if (ret != 0) {            break;        }        //再开一个线程 播放。        frames.push(frame);    }    releaseAvPacket(&packet);}

decode的工作任务是把frame放入到frames队列中就完成了,下面就是播放声音了。

播放音频

关于OpenSL ES的使用可以进入ndk-sample查看native-audio工程:github.com/googlesampl…
OpenSL ES的开发流程主要有如下7个步骤:

  1. 创建引擎与接口
  2. 设置混音器
  3. 创建播放器
  4. 设置播放回调函数
  5. 设置播放状态
  6. 启动回调函数
  7. 释放

初始化相关的类

作者:cxy107750
链接:
https://juejin.cn/post/6844903703804117006

来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 Android播放声音(声音格式PCM),有两种方式,方式一是通过Java SDK中的AudioTrack(AudioRecorder录音),另一种是通过NDK的OpenSL ES来播放,这里的数据在Native层,所以避免JNI跨层的数据调用以及反射等相关处理,这里我们选择OpenSL ES来播放音频,OpenSL ES是Android NDK中的包,专门为嵌入式设备处理音频的库,引入库需要修改CmakeLists.txt文件(FFmpeg只是编解码库,不支持处理输入输出设备)

class AudioChannel : public BaseChannel {public:    AudioChannel(int id, AVCodecContext *avCodecContext, AVRational time_base);    ~AudioChannel();private:     /**     * OpenSL ES     */    //引擎    SLObjectItf engineObject = 0;    //引擎接口    SLEngineItf engineInterface = 0;    //混音器    SLObjectItf outputMixObject = 0;    //播放器    SLObjectItf bqPlayerObject = 0;    //播放器接口    SLPlayItf bqPlayerInterface = 0;    //队列结构    SLAndroidSimpleBufferQueueItf bqPlayerBufferQueueInterface = 0;};

创建引擎与接口

SLresult result;    // 创建引擎 SLObjectItf engineObject    result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);    if (SL_RESULT_SUCCESS != result) {        return;    }    // 初始化引擎(init)    result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);    if (SL_RESULT_SUCCESS != result) {        return;    } // 获取引擎接口SLEngineItf engineInterfaceresult = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineInterface);    if (SL_RESULT_SUCCESS != result) {        return;    }

设置混音器

result = (*engineInterface)->CreateOutputMix(engineInterface,&outputMixObject, 0, 0, 0);    if (SL_RESULT_SUCCESS != result) {        return;    }    // 初始化混音器outputMixObject    result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);    if (SL_RESULT_SUCCESS != result) {        return;    } //3.2 配置音轨(输出)    //设置混音器    SLDataLocator_OutputMix outputMix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};    SLDataSink audioSnk = {&outputMix, NULL};    //需要的接口, 操作队列的接口,可以添加混音接口    const SLInterfaceID ids[1] = {SL_IID_BUFFERQUEUE};    const SLboolean req[1] = {SL_BOOLEAN_TRUE};

创建播放器

//创建buffer缓冲类型的队列 2个队列SLDataLocator_AndroidSimpleBufferQueue android_queue ={SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,2};//pcm数据格式    //pcm +2(双声道)+ 44100(采样率)+ 16(采样位)+ LEFT|RIGHT(双声道)+ 小端数据SLDataFormat_PCM pcm = {SL_DATAFORMAT_PCM, 2, SL_SAMPLINGRATE_44_1, SL_PCMSAMPLEFORMAT_FIXED_16,SL_PCMSAMPLEFORMAT_FIXED_16,SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,SL_BYTEORDER_LITTLEENDIAN};//数据源 将上述配置信息放到这个数据源中 SLDataSource slDataSource = {&android_queue, &pcm};//创建播放器(*engineInterface)->CreateAudioPlayer(engineInterface, &bqPlayerObject, &slDataSource,&audioSnk, 1,ids, req);//初始化播放器(*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE);

设置播放回调

(*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE,                                    &bqPlayerBufferQueueInterface); //设置回调(*bqPlayerBufferQueueInterface)->RegisterCallback(bqPlayerBufferQueueInterface, bqPlayerCallback, this);

设置播放状态

(*bqPlayerInterface)->SetPlayState(bqPlayerInterface, SL_PLAYSTATE_PLAYING);

启动激活函数

 //6. 手动激活启动回调bqPlayerCallback(bqPlayerBufferQueueInterface, this);

回调接口中处理转码PCM的过程

void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *context) {    AudioChannel *audioChannel = static_cast<AudioChannel *>(context);    //获取pcm数据    int dataSize = audioChannel->getPcm();    if(dataSize > 0){        (*bq)-> Enqueue(bq, audioChannel->data, dataSize);//这里取 16位数据    }}//获取Pcm 数据int AudioChannel::getPcm() {    int data_size = 0;    AVFrame *frame;    int ret = frames.pop(frame);    if (!isPlaying) {        if (ret) {            releaseAvFrame(&frame);        }        return data_size;    }    //48000HZ  8位 =》 44100 16位    //重采样    //假设我们输入了10个数据, swrContext转码器,这一次处理了8个数据    int64_t delays = swr_get_delay(swrContext, frame->sample_rate);    //将nb_samples个数据由 sample_rate 采样率转成 44100后,返回多少个数据    // 10 个 48000 = nb个 44100    //AV_ROUND_UP : 向上取整 1.1 = 2    int64_t max_samples = av_rescale_rnd(delays + frame->nb_samples, out_sample_rate, frame->sample_rate, AV_ROUND_UP);    //上下文 + 输入缓冲区 + 输出缓冲区能接受的最大数据量 + 输入数据 + 输入数据个数    // 返回每一个声道的数据    int samples = swr_convert(swrContext, &data, max_samples,                              (const uint8_t **)frame->data, frame->nb_samples);    //获得 samples个2字节(16位) * 2声道    data_size = samples *  out_samplesize * out_channels;    //获取一个frame的一个相对播放时间    //获得播放这段数据的秒速(时间机)    clock =  frame->pts * av_q2d(time_base);    releaseAvFrame(&frame);    return data_size;}

播放流程处理完成后,记得释放OpenSL中的对象。

void AudioChannel::stop() {    isPlaying = 0;    packets.setWork(0);    frames.setWork(0);    //释放播放器    if(bqPlayerObject){        (*bqPlayerObject)->Destroy(bqPlayerObject);        bqPlayerObject = 0;        bqPlayerInterface = 0;        bqPlayerBufferQueueInterface = 0;    }    //释放混音器    if(outputMixObject){        (*outputMixObject)->Destroy(outputMixObject);        outputMixObject = 0;    }    //释放引擎    if(engineObject){        (*engineObject)->Destroy(engineObject);        engineObject = 0;        engineInterface = 0;    }}

以上就是音频、视频解码及播放的整个流程,但是会存在的问题是音视频他们在各自的线程里播放,互不干扰,但是从视频源来的视频、音频在某一时间点是相互对应的,而我们的播放端是从队列里拿数据进行播放,队列有同步,等待,阻塞等因素,会造成音视频两者不同步,大概率上甚至说基本是不同步的。所以需要处理二者的同步关系,如何来处理二者的同步呢?下一篇详解.

需要源码的,可前往GitHub获取, 顺便点个star呗。

作者:cxy107750
原文 FFmpeg视频播放(音视频解码) - 掘金