上一篇中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个步骤:
- 创建引擎与接口
- 设置混音器
- 创建播放器
- 设置播放回调函数
- 设置播放状态
- 启动回调函数
- 释放
初始化相关的类
作者: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)以及音视频学习路线图等等。
见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓