FFmpeg 开发音频播放
音频相关基础知识
1.音频编码的作用
编码的作用就是对数据进行压缩。而音频编码就是将音频设备的采集数据压缩成音频码流。
2.PCM
PCM(Paulse Code Modulation)。音频的裸数据就是(PCM、AMR、MP3)数据。格式为:量化格式、采样率、声道数。
| PCM | AMR | MP3 |
|---|---|---|
| 根据采样率和采样精度就可以播放,不需要帧的概念 | 每20ms就是一帧,每一帧音频都是独立的 | 音频数据帧的个数由文件大小和帧长决定。每一帧的长度可能不固定,由比特率决定。每一帧又分为帧头和数据实体两个部分,帧头记录了MP3的比特率、采样率、版本等信息,帧之间相互独立 |
3.常见的采样率
采样率指的是每秒音频采样点个数,采样率单位为赫兹(HZ)。
- 8khz:电话
- 22.05khz:广播
- 44.1khz: 音频CD
- 48khz: DVD、数字电视中使用
- 96khz-192khz: DVD-Audio、蓝光高清
采样的精度常用范围在8Bit~32Bit之间,一般都是16Bit。
4.比特率
比特率 = 采样率 × 采样深度 × 通道数
例如 某文件的采样率为44100,深度为16bit,双声道 那么他的比特率为:44100 * 16 * 2
5.常见的音频编码格式
WAV
- 在PCM数据格式的前面加上44字节,描述PCM的采样率、声道数、数据格式等信息,不会压缩
- 特点:音质好,大量软件支持
- 使用场合:多媒体开发的中间文件、保存音乐和音效素材
MP3
- 使用LAME编码
- 特点:音质在128kbit/s以上表现不错,压缩比较高,大量软件硬件都支持,兼容性好
- 使用场合:高比特率(传输效率 bps, 这里的b是位,不是比特)对兼容性有要求的音乐欣赏
AAC
- 特点:在小于128Kbit/s的码率下表现优异,并且多用于视频中的音频编码
- 使用场合:128Kbit/s以下的音频编码,多用于视频中音频轨的编码
Ogg
- 特点: 可以用比MP3更小的码率实现比MP3更好的音质,高中低码率下均有良好的表现
- 不足:兼容性不够好,流媒体特性不支持
- 适合场景:语音聊天的音频消息场景
APE
- 无损压缩
FLAC
- 专门针对PCM音频的特点设计的压缩方式,而且可以使用播放器直接播放FLAC压缩的文件
- 免费,支持大多数操作系统
6.封装格式
编码后的音频数据以一定的格式封装到一个容器中,例如mp3、avi、mp4等。他只负责把内部的视频信息或者音频信息集成在一起,并不会对视频或者音频数据进行修改。
7.声道
- 单声道:一个扬声器
- 立体声道:左右对称两个扬声器
- 4声道:前左、前右、后左、后右
8.文件示例
使用ffmpeg -i xxx 可以查看文件信息。 例如采样率 双声道等
视频文件实例,包含1个视频流和1个音频流
FFmpeg音频解码
1.创建封装上下文
mAVFormatContext = avformat_alloc_context();
2.打开文件,解封装
avFormatContextCode = avformat_open_input(&mAVFormatContext, mUrl, nullptr,
nullptr);
3.获取流信息
avformat_find_stream_info(mAVFormatContext, nullptr);
4.根据流的索引找到对应的解码器
for (int i = 0; i < mAVFormatContext->nb_streams; ++i) {
if (mAVFormatContext->streams[i]->codecpar->codec_type == mediaType) {
mStreamIndex = i;
break;
}
}
if (mStreamIndex == -1) {
LOGE("mStreamIndex get Fail,mediaType = %d", mediaType);
break;
}
AVStream *stream = mAVFormatContext->streams[mStreamIndex];
AVCodec *avCodec = avcodec_find_decoder(stream->codecpar->codec_id);
5.打开解码器
mAVCodecContext = avcodec_alloc_context3(avCodec);
avcodec_parameters_to_context(mAVCodecContext, stream->codecpar);
int codecCode = avcodec_open2(mAVCodecContext, avCodec, nullptr);
6.设置重采样参数
mSwrContext = swr_alloc();
swr_alloc_set_opts(mSwrContext,
AUDIO_DST_CHANNEL_LAYOUT, DST_SAMPLT_FORMAT, AUDIO_DST_SAMPLE_RATE,
codeCtx->channel_layout, codeCtx->sample_fmt, codeCtx->sample_rate,
0, NULL);
swr_init(mSwrContext);
7.读取数据包、发送数据包到解码队列、获取解码帧
int ret = av_read_frame(mAVFormatContext, mAvPacket);
while (ret == 0) {
if (mAvPacket->stream_index == mStreamIndex) {
if (avcodec_send_packet(mAVCodecContext, mAvPacket) == AVERROR_EOF) {
ret = -1;
av_packet_unref(mAvPacket);
break;
}
while (avcodec_receive_frame(mAVCodecContext, mAvFrame) == 0) {
doFrame(mAvFrame);
}
av_packet_unref(mAvPacket);
ret = av_read_frame(mAVFormatContext, mAvPacket);
}
}
OpenSLES
OpenSL ES 全称为: Open Sound Library for Embedded Systems,是一个针对嵌入式系统的开放硬件音频加速库,支持音频的采集和播放,它提供了一套高性能、低延迟的音频功能实现方法,并且实现了软硬件音频性能的跨平台部署,大大降低了上层处理音频应用的开发难度。
Object 和 Interface OpenSL ES 中的两大基本概念,可以类比为 Java 中的对象和接口。在 OpenSL ES 中, 每个 Object 可以存在一系列的 Interface ,并且为每个对象都提供了一系列的基本操作,如 Realize,GetState,Destroy 等。
重要的一点,只有通过 GetInterface 方法拿到 Object 的 Interface ,才能使用 Object 提供的功能。
OpenSLES 开发流程
1.创建播放器
slCreateEngine(&mEngineObj, 0, nullptr, 0, nullptr, nullptr);
(*mEngineObj)->Realize(mEngineObj, SL_BOOLEAN_FALSE);
(*mEngineObj)->GetInterface(mEngineObj, SL_IID_ENGINE, &mEngineEngine);
2.创建混音器
(*mEngineEngine)->CreateOutputMix(mEngineEngine, &mOutputMixObj, 1, mids, mreq);
(*mOutputMixObj)->Realize(mOutputMixObj, SL_BOOLEAN_FALSE);
3.创建播放器
(*mEngineEngine)->CreateAudioPlayer(mEngineEngine, &mAudioPlayerObj, &slDataSource,
&slDataSink, 3, ids, req);
(*mAudioPlayerObj)->Realize(mAudioPlayerObj, SL_BOOLEAN_FALSE);
(*mAudioPlayerObj)->GetInterface(mAudioPlayerObj, SL_IID_PLAY, &mAudioPlayerPlay);
//设置缓存队列
(*mAudioPlayerObj)->GetInterface(mAudioPlayerObj, SL_IID_BUFFERQUEUE,
&mBufferQueue);
//注册回调
(*mBufferQueue)->RegisterCallback(mBufferQueue, AudioPlayerCallback, this);
总结
FFmpeg视频
音视频的同步
引起原因:I、B、P数据帧
I帧:帧内编码帧(intra picture),I帧通常是每个GOP(MPEG所使用 的一种视频压缩技术)的第一个帧,经过适度地压缩,作为随机访问的参考点,可以当成静态图像。I帧可以看作一个图像经过压缩后的产物,I帧压缩可以得到6:1的压缩比而不会产生任何可觉察的模糊现象。I帧压缩可去掉视频的空间冗余信息,下面即将介绍的P帧和B帧是为了去掉时间冗余信息。
B:双向预测内插编码帧(bi-directional interpolated prediction frame),以前面的I或P帧和后面的P帧为参考帧,“找出”B帧“某点”的预测值和两个运动矢量,并取预测差值和运动矢量传送。接收端根据运动矢量在两个参考帧中“找出(算出)”预测值并与差值求和,得到B帧“某点”样值,从而可得到完整的B帧。换言之,要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的与本帧数据的叠加取得最终的画面。B帧压缩率高,但是解码时CPU的负荷会比较大。
P:前向预测编码帧(predictive-frame),通过将图像序列中前面已编码帧的时间冗余信息充分去除来压缩传输数据量的编码图像,也称为预测帧。P帧表示的是这一帧跟之前的一个I帧(或P帧)的差别 ,解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面。(也就是差别帧,P帧没有完整画面数据,只有与前一帧的画面差别的数据)
PTS和DTS:
PTS:显示时间戳。
DTS:解码时间戳。
同步方案
1.系统时钟同步
系统时钟的更新是按照时间的增加而增加,获取音视频解码帧时与系统时钟进行对齐操作。当前音频或视频播放时间戳大于系统时钟时,解码线程进行休眠,直到时间戳与系统时钟对齐。
2.音频同步视频
音频向视频同步,就是音频的时间戳向视频的时间戳对齐。由于视频有固定的刷新频率,即 FPS ,我们根据 PFS 确定每帧的渲染时长,然后以此来确定视频的时间戳。
当音频时间戳大于视频时间戳,或者超过一定的阈值,音频播放器一般插入静音帧、休眠或者放慢播放。反之,就需要跳帧、丢帧或者加快音频播放。
3.视频同步音频
视频向音频同步的方式比较常用,刚好利用了人耳朵对声音变化比眼睛对图像变化更为敏感的特点。
音频按照固定的采样率播放,为视频提供对齐基准,当视频时间戳大于音频时间戳时,渲染器不进行渲染或者重复渲染上一帧,反之,进行跳帧渲染。
方案对比:
| 优点 | 缺点 | |
|---|---|---|
| 系统时钟同步 | 音视频向系统时钟同步可以最大限度减少丢帧跳帧现象。 | 如果系统时钟受其他耗时任务影响,同步就会出现问题 |
| 音频向视频同步 | 视频可以将每一帧播放出来,画面流畅度最优 | 人耳对声音相对眼睛对图像更为敏感,音频在与视频对齐时,插入静音帧、丢帧或者变速播放操作,用户可以轻易察觉,体验较差。 |
| 视频向音频同步 | 比较符合敏感特点,无感知。 | 画面流畅度可能受到音频数据影响 |
同步代码(系统时间同步)
long curSysTime = GetSysCurrentTime();
//基于系统时钟计算从开始播放流逝的时间
long elapsedTime = curSysTime - mStartTimeStamp;
if (mediaType == AVMEDIA_TYPE_AUDIO) {
mMsgContext->onProcess(mCurTimeStamp * 1.0f / 1000);
}
// if(m_MsgContext && m_MsgCallback && m_MediaType == AVMEDIA_TYPE_AUDIO)
// m_MsgCallback(m_MsgContext, MSG_DECODING_TIME, m_CurTimeStamp * 1.0f / 1000);
//向系统时钟同步
if (mCurTimeStamp > elapsedTime) {
//休眠时间
auto sleepTime = static_cast<unsigned int>(mCurTimeStamp - elapsedTime);//ms
//限制休眠时间不能过长
sleepTime = sleepTime > DELAY_THRESHOLD ? DELAY_THRESHOLD : sleepTime;
av_usleep(sleepTime * 1000);
}
同步代码(视频同步音频)
static int video_refresh_thread(void *arg)
{
FFPlayer *ffp = arg;
VideoState *is = ffp->is;
double remaining_time = 0.0;
while (!is->abort_request) {
if (remaining_time > 0.0)
av_usleep((int)(int64_t)(remaining_time * 1000000.0));
remaining_time = REFRESH_RATE;
if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh))
video_refresh(ffp, &remaining_time);
}
return 0;
}
开发流程
1.初始化解码器
mRGBAFrame = av_frame_alloc();
int bufferSize = av_image_get_buffer_size(AV_PIX_FMT_RGBA, mRenderWidth, mRenderHeight,
1);
mFrameBuffer = static_cast<uint8_t *>(av_malloc(bufferSize * sizeof(uint8_t)));
av_image_fill_arrays(mRGBAFrame->data, mRGBAFrame->linesize, mFrameBuffer,
AV_PIX_FMT_RGBA, mRenderWidth, mRenderHeight, 1);
mSwsContext = sws_getContext(mVideoWidth, mVideoHeight, avCodec->pix_fmt, mRenderWidth,
mRenderHeight, AV_PIX_FMT_RGBA, SWS_FAST_BILINEAR, nullptr,
nullptr, nullptr);
2.视频的解码
sws_scale(mSwsContext, frame->data, frame->linesize, 0,
mVideoHeight, mRGBAFrame->data, mRGBAFrame->linesize);
1.NativeWindow
ANativeWindow_fromSurface(env,surface)
2.填充数据
ANativeWindow_lock(mNativeWindow, &mNativeWindowBuffer, nullptr);
uint8_t *dstBuffer = static_cast<uint8_t *>(mNativeWindowBuffer.bits);
int srcLineSize = pImage->width * 4;//RGBA
int dstLineSize = mNativeWindowBuffer.stride * 4;
for (int i = 0; i < mDstHeight; ++i) {
memcpy(dstBuffer + i * dstLineSize, pImage->ppPlane[0] + i * srcLineSize, srcLineSize);
}
ANativeWindow_unlockAndPost(mNativeWindow);