FFmpeg视频播放(音视频解码)

上一篇中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方法

//TinaPlayer
public void onPrepare(){
        if (null != listener){
            listener.onPrepare();
        }
 }

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

//TinaPlayer
public void start(){
  native_start();
}

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

extern "C"
JNIEXPORT void JNICALL
Java_tina_com_player_TinaPlayer_native_1start(JNIEnv *env, jobject instance) {
    ffmpeg->start();
}

//TinaFFmpeg
void 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

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

//VideoChannel
void VideoChannel::setRenderFrameCallback(RenderFrameCallback callback) {
    this->callback = callback;
}

//TinaFFmpeg
void TinaFFmpeg::setRenderFrameCallback(RenderFrameCallback callback) {
    this->callback = callback;
}

extern "C"
JNIEXPORT void JNICALL
Java_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
*/
@Override
public 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 engineInterface
result = (*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视频播放(音视频解码) - 掘金

★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。

见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值