最近在做播放器软件,接触到FFMEPEG的采集部分,中间也走了很多弯路,记录下,也给后来者插个眼
首先,初始化FFMEPEG和SDL,我这里使用的方法是视频数据上抛到接口,音频数据则通过SDL直接播放
FFMEPEG及SDL初始化
int initFFmpeg() {
//初始化设备号
if (!pAVFormatContext) {
pAVFormatContext = avformat_alloc_context(); //申请一个AVFormatContext结构的内存,并进行简单初始化
}
pAVFormatContext->interrupt_callback.callback = decode_interrupt_cb;
pAVFormatContext->interrupt_callback.opaque = this;
if (!pAVFrame) {
pAVFrame = av_frame_alloc();
}
AVDictionary *avdic = NULL;
//av_dict_set(&avdic, "bufsize", "2000k", 0);
if (rtspTcp_) {
av_dict_set(&avdic, "rtsp_transport", "tcp", 0);
}
lasttime = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
//打开视频流
int result = avformat_open_input(&pAVFormatContext, url_.c_str(), NULL, &avdic);
if (avdic) {
av_dict_free(&avdic);
avdic = NULL;
}
if (result < 0) {
//"打开视频流失败";
return -1;
}
//获取视频流信息
result = avformat_find_stream_info(pAVFormatContext, NULL);
if (result < 0) {
//"获取视频流信息失败";
return -2;
}
av_dump_format(pAVFormatContext, 0, url_.c_str(), 0);
//获取视频流索引
videoStreamIndex = -1;
for (int i = 0; i < pAVFormatContext->nb_streams; i++) {
if (pAVFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
videoStreamIndex = i;
break;
}
}
if (videoStreamIndex == -1) {
//"获取视频流索引失败";
return -3;
}
//获取视频流的分辨率大小
pAVCodecContext = avcodec_alloc_context3(NULL);
if (!pAVCodecContext)
{
return -4;
}
result = avcodec_parameters_to_context(pAVCodecContext, pAVFormatContext->streams[videoStreamIndex]->codecpar);
if (result < 0) {
return -5;
}
videoWidth = pAVCodecContext->width;
videoHeight = pAVCodecContext->height;
mtx_.lock();
frame_.width = videoWidth;
frame_.height = videoHeight;
frame_.fmt = VideoFormat::VIDEO_FMT_I420;
mtx_.unlock();
//获取视频流解码器
pAVCodec = avcodec_find_decoder(pAVCodecContext->codec_id);
//打开对应解码器
result = avcodec_open2(pAVCodecContext, pAVCodec, NULL);
if (result < 0) {
//打开解码器失败
return -6;
}
audioStreamIndex = -1;
for (int i = 0; i < pAVFormatContext->nb_streams; i++) {
if (pAVFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
audioStreamIndex = i;
break;
}
}
//将音频流信息拷贝到新的AVCodecContext
AVCodecContext* pCodecCtxOrg = nullptr;
pCodecCtxOrg = pAVFormatContext->streams[audioStreamIndex]->codec; // codec context
// 找到audio stream的 decoder
pAuCodec = avcodec_find_decoder(pCodecCtxOrg->codec_id);
if (!pAuCodec)
{
cout << "Unsupported codec!" << endl;
return -1;
}
// 不直接使用从AVFormatContext得到的CodecContext,要复制一个
pAUCodecContext = avcodec_alloc_context3(pAuCodec);
if (avcodec_copy_context(pAUCodecContext, pCodecCtxOrg) != 0)
{
cout << "Could not copy codec context!" << endl;
return -1;
}
result = avcodec_open2(pAUCodecContext, pAuCodec, NULL);
if (result < 0) {
//打开解码器失败
return -6;
}
//SDL初始化
if (SDL_Init(SDL_INIT_AUDIO))
{
return -1;
}
//重采样SwrContext初始化
resampler = swr_alloc_set_opts(NULL,
pAUCodecContext->channel_layout,
AV_SAMPLE_FMT_S16,
44100,
pAUCodecContext->channel_layout,
pAUCodecContext->sample_fmt,
pAUCodecContext->sample_rate,
0,
NULL);
swr_init(resampler);
//打开设备,使用的时SDL_OpenAudioDevice而不是SDL_OpenAudio.为了后续的音频设备采样播出
//其中SDL_AudioSpec的回调callback设置为空,由系统自动采集音频数据并播出,而不是自己处理
SDL_AudioSpec want, have;
SDL_zero(want);
SDL_zero(have);
want.freq = 44100;
want.channels = pAUCodecContext->channels;
want.format = AUDIO_S16SYS;
audioDeviceNum = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
SDL_PauseAudioDevice(audioDeviceNum, 0);
return 0;
}
其中包含了FFMEPEG对音频和视频数据的初始化内容,这部分与其他文章并无不同,就略过了
SDL初始化,我只是用了音频部分,需要注意的点,主要实在打开音频设备这一步上,
在SDL2中,共提供了两套用于播放音频数据的API:
SDL_AudioSpec | 打开音频设备 | 数据播放 | 数据填充 | |
第一套 | callback = mCallback | SDL_OpenAudio | SDL_PauseAudio | SDL_AudioCallback |
第二套 | callback = NULL | SDL_OpenAudioDevice | SDL_PauseAudioDevice | SDL_QueueAudio |
两套API,同时使用时混音处理会出现问题,无法正常播放,若有信心解决,可混用,否则不建议
区别:
单路流时,没有显著区别,均可正常使用,其中第一套需手动完成回调部分,实现数据传入音频设备,写入时,需调用SDL_MixAudio。
多路流时,SDL_OpenAudio只能打开一次,再次调用无条件返回错误,所以除第一次打开外,后续流仍需调用SDL_OpenAudioDevice,且同时存在时,SDL_PauseAudio和SDL_CloseAudio调用会对其他对应的音频设备造成影响。
数据填充:
我用的是第二套方案,以实现多路音频流播放,如需第一套实现,请自行查找
void loop() {
std::unique_lock<mutex> lck(loopMtx_);
running_ = true; //开始运行
int result = initFFmpeg();
if (result < 0) {
//初始化失败
running_ = false;
return;
}
int ret = 0;
AVFrame* frame = av_frame_alloc();
AVFrame* audioframe = av_frame_alloc();
while (running_)
{
uint64_t ts = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
if (av_read_frame(pAVFormatContext, &avPacket) >= 0) {
if (avPacket.stream_index == videoStreamIndex)
{
//视频部分忽略
}
else if (avPacket.stream_index == audioStreamIndex)
{
AVStream* stream = pAVFormatContext->streams[avPacket.stream_index];
if (stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
ret = avcodec_send_packet(pAUCodecContext, &avPacket);
while (ret >= 0) {
ret = avcodec_receive_frame(pAUCodecContext, frame);
if (ret >= 0) {
int dst_samples = frame->channels * av_rescale_rnd(
swr_get_delay(resampler, frame->sample_rate)
+ frame->nb_samples,
44100,
frame->sample_rate,
AV_ROUND_UP);
uint8_t* audiobuf = NULL;
ret = av_samples_alloc(&audiobuf,
NULL,
1,
dst_samples,
AV_SAMPLE_FMT_S16,
1);
dst_samples = frame->channels * swr_convert(
resampler,
&audiobuf,
dst_samples,
(const uint8_t**)frame->data,
frame->nb_samples);
ret = av_samples_fill_arrays(audioframe->data,
audioframe->linesize,
audiobuf,
1,
dst_samples,
AV_SAMPLE_FMT_S16,
1);
SDL_QueueAudio(audioDeviceNum,
audioframe->data[0],
audioframe->linesize[0]);
}
}
}
}
}
av_packet_unref(&avPacket);//释放资源,否则内存会一直上升
}
}
};
通过FFMEPEG获取音频数据frame,在通过swr重采样生成新的audioframe ,最后用SDL_QueueAudio填充,直接缓存进播放设备。
备注:
懒得去扒重复代码,要复用的同学可以先参考下其他的实现方案,修改初始化和读取部分的代码
总结:
国内能找到的SDL文档,主要是采用的第一套方法,单路流时使用正常,无非就是手动实现回调时繁琐了些。但是多路流的播放相关的资料很少,我也是看到一些线索后在谷歌上找到了相关的开发资料。实验发现可行,实现上简单了很多,多路音频播放正常。
记录下,也为在学习SDL 的同学多留点可用资料