ffmpeg播放流程:
1、解封装,拿到音视频信息,创建编码器。
2、拿到音频Packet和视频Packet。
3、解码,拿到音频Frame和视频Frame。
4、对于音频原始帧,使用opensl ES进行播放。
5、对于视频帧,需要 swscale库进行格式转换,转换成 ANativeWindow能够接收的数据类型RGBA_8888。
6、音频视频同步。
7、对于本地文件或则点播,支持seek操作。
交叉编译ffmpeg博客地址:https://blog.csdn.net/wangchao1412/article/details/103241932
github地址:https://github.com/wangchao0837/FFmpegPlayer
1、配置ffmpeg,解封装拿到音视频信息。
void FFmpeg::p_prepare() {
isPlaying = 1;
duration = 0;
//初始化网络
int ret = avformat_network_init();
LOGE("初始化网络:%s", av_err2str(ret));
formatContext = avformat_alloc_context();
AVDictionary *opts = 0;
//设置超时时间
av_dict_set(&opts, "timeout", "3000000", 0);
//datasource:传入的播放地址 可以为本地文件,也可以为网络地址
//打开网络地址,赋值formatContext
ret = avformat_open_input(&formatContext, datasource, 0, &opts);
av_dict_free(&opts);
opts = 0;
if (ret) {
LOGE("打开媒体失败:%s", av_err2str(ret));
callHelper->onError(THREAD_CHILD, FFMPEG_CAN_NOT_OPEN_URL, av_err2str(ret));
return;
}
//拿到总时长,对于本地文件和点播,有效。对于直播地址,为0
duration = formatContext->duration / 1000000;
LOGE("duration is :%d", duration);
//获取流信息,赋值formatContext
ret = avformat_find_stream_info(formatContext, 0);
if (ret < 0) {
LOGE("查找流失败:%s", av_err2str(ret));
if (isPlaying) {
callHelper->onError(THREAD_CHILD, FFMPEG_CAN_NOT_FIND_STREAMS, av_err2str(ret));
isPlaying = 0;
}
return;
}
//formatContext->nb_streams 表示有几条stream,一般有:audiostream 和 videostream
for (int i = 0; i < formatContext->nb_streams; ++i) {
AVStream *stream = formatContext->streams[i];
//stream->codecpar : 编码参数
AVCodecParameters *codecPar = stream->codecpar;
//查找解码器
AVCodec *avCodec = avcodec_find_decoder(codecPar->codec_id);
if (NULL == avCodec) {
LOGE("查找解码器失败:%s", av_err2str(ret));
if (isPlaying) {
callHelper->onError(THREAD_CHILD, FFMPEG_FIND_DECODER_FAIL, av_err2str(ret));
isPlaying = 0;
}
return;
}
//创建解码上下文
AVCodecContext *codecContext = avcodec_alloc_context3(avCodec);
if (codecContext == NULL) {
LOGE("创建解码上下文失败:%s", av_err2str(ret));
if (isPlaying) {
callHelper->onError(THREAD_CHILD, FFMPEG_ALLOC_CODEC_CONTEXT_FAIL, av_err2str(ret));
isPlaying = 0;
}
return;
}
//设置解码上下文参数
ret = avcodec_parameters_to_context(codecContext, codecPar);
if (ret < 0) {
LOGE("设置解码上下文参数失败:%s", av_err2str(ret));
if (isPlaying) {
callHelper->onError(THREAD_CHILD, FFMPEG_CODEC_CONTEXT_PARAMETERS_FAIL,
av_err2str(ret));
isPlaying = 0;
}
return;
}
//打开解码器
ret = avcodec_open2(codecContext, avCodec, 0);
if (ret != 0) {
LOGE("打开解码器失败:%s", av_err2str(ret));
if (isPlaying) {
callHelper->onError(THREAD_CHILD, FFMPEG_OPEN_DECODER_FAIL, av_err2str(ret));
isPlaying = 0;
}
return;
}
if (codecPar->codec_type == AVMEDIA_TYPE_VIDEO) {
AVRational frameRate = stream->avg_frame_rate;
//获取视频帧率
double fps = av_q2d(frameRate);
videoChannel = new VideoChannel(i, codecContext, stream->time_base, fps);
} else if (codecPar->codec_type == AVMEDIA_TYPE_AUDIO) {
audioChannel = new AudioChannel(i, codecContext, stream->time_base, callHelper,
duration);
}
}
if (!audioChannel && !videoChannel) {
LOGE("未找到音视频流");
const char *msg = "未找到音视频流";
if (isPlaying) {
callHelper->onError(THREAD_CHILD, FFMPEG_NOMEDIA, msg);
isPlaying = 0;
}
return;
}
if (isPlaying) {
//通知java层,prepare成功,可以start解码播放了
callHelper->onPrepare(THREAD_CHILD);
}
}
创建好解码器后,根据streamId,判断是音频流还是视频流,创建对应的audiochannel和videochannel来处理。
2、开始解码,拿到Packet
void FFmpeg::p_decode() {
int ret;
while (isPlaying) {
if (!isPlaying) {
break;
}
pthread_mutex_lock(&seekMutext);
AVPacket *packet = av_packet_alloc();
if (packet == 0) {
continue;
}
ret = av_read_frame(formatContext, packet);
if (ret == 0) {
if (videoChannel && packet->stream_index == videoChannel->streamId) {
//将视频Packet交给videochannel处理
videoChannel->packets.push(packet);
} else if (audioChannel && packet->stream_index == audioChannel->streamId) {
//将音频Packet交给videochannel处理
audioChannel->packets.push(packet);
}
}
//ACERROR_EOF:读取结束,表示读取到文件末尾标志
else if (ret == AVERROR_EOF) {
while (isPlaying) {
if (videoChannel->packets.empty() && audioChannel->packets.empty()) {
break;
}
//等待10毫秒再次检查
av_usleep(10 * 1000);
}
DELETE(packet)
pthread_mutex_unlock(&seekMutext);
continue;
} else {
DELETE(packet)
pthread_mutex_unlock(&seekMutext);
break;
}
pthread_mutex_unlock(&seekMutext);
}
}
这里将一下接下来的代码设计,首先ffmpeg解码音视频Packet,音频Packet交给AudioChannel,视频Packet交给VideoChannel,AudioChannel和VideoChannel都继承自BaseChannel。各个类负责的事物。
BaseChannel:负责将音视频Packet解码Frame的过程。然后加入到FrameQueue中。
VideoChannel:从FrameQueue取出Frame,将图像转RGBA,播放。
AudioChannel:从FrameQueue中取出Frame,从采样,使用opensl ES播放。
3、下来看一下BaseChannel,解码过程(播放视频)。
void BaseChannel::p_decode() {
AVPacket *packet = 0;
while (isPlaying) {
int ret = packets.pop(packet);
if (!isPlaying) {
break;
}
if (!ret) {
continue;
}
ret = avcodec_send_packet(avCodecContext, packet);
releaseAvPackage(&packet);
if (ret != 0) {
break;
}
//送入编码器成功
AVFrame *avFrame = av_frame_alloc();
ret = avcodec_receive_frame(avCodecContext, avFrame);
if (ret == AVERROR(EAGAIN)) {
DELETE(avFrame)
continue;
} else if (ret != 0) {
break;
}
//解码完一帧 数据,放入frames 队列中
frames.push(avFrame);
}
releaseAvPackage(&packet);
}
VideoChannel:处理Frame,转换RGBA。
先看一下构造函数:初始化SwsContext
VideoChannel::VideoChannel(int i, AVCodecContext *avCodecContext, AVRational time_base, double fps)
: BaseChannel(i, avCodecContext, time_base) {
this->fps = fps;
frame_delay = 1.0 / fps;
//目标: RGBA
//关键帧率间隔
LOGE("av gop_size %d",avCodecContext->gop_size);
//初始化SwsContext
swsContext = sws_getContext(
avCodecContext->width, avCodecContext->height, avCodecContext->pix_fmt,
avCodecContext->width, avCodecContext->height, AV_PIX_FMT_RGBA,
SWS_BILINEAR, 0, 0, 0);
av_image_alloc(dst_data, dst_linesize,
avCodecContext->width, avCodecContext->height, AV_PIX_FMT_RGBA, 1);
}
然后在开始编码时候,开启whiile循环,处理frame数据。交给SwsContext转格式。
void VideoChannel::p_render() {
AVFrame *frame = 0;
while (isPlaying) {
//队列中取出frame
int ret = frames.pop(frame);
if (!isPlaying) {
if (ret != 0) {
releaseAvFrame(&frame);
}
break;
}
if (ret == 0) {
continue;
}
sws_scale(swsContext, reinterpret_cast<const uint8_t *const *>(frame->data),
frame->linesize, 0, avCodecContext->height, dst_data,
dst_linesize);
//callback是一个函数指针,数据交给外层,做预览
callback(dst_data[0], dst_linesize[0], avCodecContext->width,
avCodecContext->height);
releaseAvFrame(&frame);
}
releaseAvFrame(&frame);
}
callback:函数指针,将swsContext转换后的数据交给外界错处理。
看一下CallBack在外界的处理。首先在开始编码前,先把surface传入。
extern "C"
JNIEXPORT void JNICALL
Java_com_example_ffmpegplayer_Player_native_1surface(JNIEnv *env, jobject thiz, jobject surface) {
pthread_mutex_lock(&mutex);
if (window) {
ANativeWindow_release(window);
window = 0;
}
window = ANativeWindow_fromSurface(env, surface);
pthread_mutex_unlock(&mutex);
}
创建了window,用来做预览。
CallBack,播放视频
void render(uint8_t *data, int lineszie, int w, int h) {
//互斥锁
pthread_mutex_lock(&mutex);
if (!window) {
pthread_mutex_unlock(&mutex);
return;
}
//设置windowbuffer的大小和格式
ANativeWindow_setBuffersGeometry(window, w, h, WINDOW_FORMAT_RGBA_8888);
ANativeWindow_Buffer windowBuffer;
if (ANativeWindow_lock(window, &windowBuffer, 0)) {
ANativeWindow_release(window);
window = 0;
pthread_mutex_unlock(&mutex);
return;
}
uint8_t *dst_data = static_cast<uint8_t *>(windowBuffer.bits);
int dst_linesize = windowBuffer.stride * 4;
for (int i = 0; i < windowBuffer.height; ++i) {
//一行一行拷贝到windowBuffer.bits 中
memcpy(dst_data + i * dst_linesize, data + i * lineszie, dst_linesize);
}
ANativeWindow_unlockAndPost(window);
pthread_mutex_unlock(&mutex);
}
4、AudioChannel,处理Frame,从采样后使用opensl ES播放
构造中初始化SwrContext
AudioChannel::AudioChannel(int i, AVCodecContext *avCodecContext, AVRational time_base,
JavaCallHelper *javaCallHelper, int duration)
: BaseChannel(i,
avCodecContext, time_base) {
this->avCodecContext = avCodecContext;
this->javaCallHelper = javaCallHelper;
this->duration = duration;
out_channels = av_get_channel_layout_nb_channels(AV_CH_LAYOUT_STEREO);
out_samplesize = av_get_bytes_per_sample(AV_SAMPLE_FMT_S16);
out_sample_rate = 44100;
//44100个16位 44100 * 2
// 44100*(双声道)*(16位)
data = static_cast<uint8_t *>(malloc(out_sample_rate * out_channels * out_samplesize));
memset(data, 0, out_sample_rate * out_channels * out_samplesize);
swrContext = swr_alloc_set_opts(0, AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_S16, out_sample_rate,
avCodecContext->channel_layout, avCodecContext->sample_fmt,
avCodecContext->sample_rate, 0, 0);
//初始化
swr_init(swrContext);
}
配置opensl播放器和队列信息等。
void AudioChannel::_playVoice() {
/**
* 1、创建引擎并获取引擎接口
*/
SLresult result;
// 1.1 创建引擎 SLObjectItf engineObject
result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
if (SL_RESULT_SUCCESS != result) {
return;
}
// 1.2 初始化引擎 init
result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
if (SL_RESULT_SUCCESS != result) {
return;
}
// 1.3 获取引擎接口SLEngineItf engineInterface
result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE,
&engineInterface);
if (SL_RESULT_SUCCESS != result) {
return;
}
/**
* 2、设置混音器
*/
// 2.1 创建混音器SLObjectItf outputMixObject
result = (*engineInterface)->CreateOutputMix(engineInterface, &outputMixObject, 0,
0, 0);
if (SL_RESULT_SUCCESS != result) {
return;
}
// 2.2 初始化混音器outputMixObject
result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
if (SL_RESULT_SUCCESS != result) {
return;
}
/**
* 3、创建播放器
*/
//3.1 配置输入声音信息
//创建buffer缓冲类型的队列 2个队列
SLDataLocator_AndroidSimpleBufferQueue android_queue = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,
2};
//pcm数据格式
//pcm+2(双声道)+44100(采样率)+ 16(采样位)+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};
//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};
//3.3 创建播放器
(*engineInterface)->CreateAudioPlayer(engineInterface, &bqPlayerObject, &slDataSource,
&audioSnk, 1,
ids, req);
//初始化播放器
(*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE);
//得到接口后调用 获取Player接口
(*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_PLAY, &bqPlayerInterface);
/**
* 4、设置播放回调函数
*/
//获取播放器队列接口
(*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE,
&bqPlayerBufferQueueInterface);
//设置回调
(*bqPlayerBufferQueueInterface)->RegisterCallback(bqPlayerBufferQueueInterface,
bqPlayerCallback, this);
/**
* 5、设置播放状态
*/
(*bqPlayerInterface)->SetPlayState(bqPlayerInterface, SL_PLAYSTATE_PLAYING);
/**
* 6、手动激活一下这个回调
*/
bqPlayerCallback(bqPlayerBufferQueueInterface, this);
}
注:opensl ES需要给播放队列接口,设置一个函数指针,opensl ES会根据设置的配置,动态执行。
看一下bqPlayerCallback 函数指针。
void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *context) {
AudioChannel *audioChannel = static_cast<AudioChannel *>(context);
//获得pcm 数据 多少个字节 data
int dataSize = audioChannel->getPcm();
if (dataSize > 0) {
// 接收16位数据
(*bq)->Enqueue(bq, audioChannel->data, dataSize);
}
}
getPcm:对音频pcm执行从采样,返回从采样后的数据大小。
拿到数据之后,加入到opensl ES的播放队列中。
getPCM方法
int AudioChannel::getPcm() {
int data_size = 0;
AVFrame *frame;
int ret = frames.pop(frame);
if (!isPlaying) {
if (ret) {
releaseAvFrame(&frame);
}
return data_size;
}
if (ret == 0) {
return 0;
}
//48000HZ 8位 =》 44100 16位
//重采样
// 假设我们输入了10个数据 ,swrContext转码器 这一次处理了8个数据
// 那么如果不加delays(上次没处理完的数据) , 积压
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);
releaseAvFrame(&frame);
//获得 samples 个 * 2 声道 * 2字节(16位)
data_size = samples * out_samplesize * out_channels;
return data_size;
}
这样播放也就完成了。
5、音视频同步
同步的方式有几种:
1、音频根据视频的播放时间来同步。
2、视频根据音频的播放时间来同步。
3、音频和视频基于同一个时间来同步。
我们是用视频根据音频来做同步。
思路:在视频播放的时候,拿到当前播放音频帧的相对时间,来做延迟或则丢包处理。而且还要控制ffmpeg读取packet的速度。
第一步:在getPcm中,拿到当前帧的时间。
audio_clock = frame->pts * av_q2d(BaseChannel::time_base);
第二步,在视频播放时候,拿到音频时间做延迟或丢包处理。
VideoChannel中 p_render方法中 加入如下处理。
//拿到当前视频帧的时间
double clock = frame->best_effort_timestamp * av_q2d(time_base);
//ffmpeg规定,y默认延迟需要加上这个extra_dela
double extra_delay = frame->repeat_pict / (2 * fps);
if (!audioChannel) {
// frame_delay = 1.0 / fps;
//frame_delay,比如一秒30帧,默认需要延迟0.033秒
av_usleep((frame_delay + extra_delay) * 1000000);
} else {
if (clock == 0) {
av_usleep((frame_delay + extra_delay) * 1000000);
} else {
double audio_clock = audioChannel->audio_clock;
double diff = audio_clock - clock;
//音频快了
if (diff > 0) {
if (diff >= 0.04) {
releaseAvFrame(&frame);
frames.sync();
continue;
}
} else {
if (fabs(diff) > 0.1) diff = 0;
av_usleep((fabs(diff) + frame_delay) * 1000000);
}
}
}
和音频对比,如果视频快了,就延迟,音频快了,不延迟,快太多就丢包。
第三步:控制ffmpeg读取packet速度。
可以根据frameQueue中的数量,对ffmpeg读取packet时,做休眠。
if (audioChannel && audioChannel->frames.size() > 100) {
av_usleep(100 * 1000);
}
if (videoChannel && videoChannel->frames.size() > 50) {
av_usleep(100 * 1000);
}
if (!isPlaying) {
break;
}
pthread_mutex_lock(&seekMutext);
AVPacket *packet = av_packet_alloc();
if (packet == 0) {
continue;
}
ret = av_read_frame(formatContext, packet);
...
...
...
...
6、对于播放本地文件,或者点播的时候,支持seek操作。
void FFmpeg::seekToPosition(int i) {
if (i < 0 || i >= duration) {
return;
}
if (!audioChannel && !videoChannel) {
return;
}
if (!formatContext) {
return;
}
pthread_mutex_lock(&seekMutext);
if (audioChannel) {
audioChannel->stopWork();
audioChannel->clear();
audioChannel->startWork();
}
if (videoChannel) {
videoChannel->stopWork();
videoChannel->clear();
videoChannel->startWork();
}
int64_t seek = i * AV_TIME_BASE;
//seek ,参数-1表示音频和视频都执行seek。
avformat_seek_file(formatContext, -1, INT64_MIN, seek, INT64_MAX, 0);
// int64_t seektime = av_rescale_q(seek, av_get_time_base_q(), videoChannel->time_base);
// int64_t a_seektime = av_rescale_q(seek, av_get_time_base_q(), audioChannel->time_base);
//
// avformat_seek_file(formatContext, videoChannel->streamId, INT64_MIN, seektime, INT64_MAX, 0);
// avformat_seek_file(formatContext, audioChannel->streamId, INT64_MIN, a_seektime, INT64_MAX, 0);
//
avcodec_flush_buffers(videoChannel->avCodecContext);
avcodec_flush_buffers(audioChannel->avCodecContext);
// int ret = av_seek_frame(formatContext,-1,seek,AVSEEK_FLAG_BACKWARD);
// av_seek_frame(formatContext, videoChannel->streamId, (int64_t) (i /av_q2d(videoChannel->time_base)), AVSEEK_FLAG_BACKWARD);
// av_seek_frame(formatContext, audioChannel->streamId, (int64_t) (i /av_q2d(audioChannel->time_base)), AVSEEK_FLAG_BACKWARD);
pthread_mutex_unlock(&seekMutext);
}
执行seek的时候,需要和ffmpeg读取packet做同步处理,使用互斥锁。然后把packetQueue和frameQueue中的数据都清空掉。