ffmpeg合并时音画不同步问题及音频软编码实现记录

最近因为耳机3.5mm接口的一些干扰问题,舍弃了之前的接入方式,需要重新实现网络音频流的接入,在这个过程中遇到了一些问题,特来记录一下~

这里使用的是ffmpeg的autogen库实现的,这个开源的库可以更为灵活的实现音视频的操作(当然就是复杂了一些~)
一、网络音频流的接入

这个音频流来源各不相同,我这里是udp广播到10001端口上的,直接监听本机10001端口就可以看到音频信息。

需要提前知道的是这个网络音频流原本的采样率和位深是多少,这里我已经知道了61000hz的采样率和16的位深

这时候我们借助ffmpeg下的ffplay程序可以直接播放这个音频,命令如下
.\ffplay.exe -f s16le -ar 61000 -ac 2 udp://0.0.0.0:10001

二、音频流和视频流合并

通过ffmpeg对网络音频流进行读取,读取之后和rtsp视频流进行合并,

这一步简单讲一讲:

首先跟rtsp读取流的方式类似,ffmpeg对音频流的读取方式也是支持url直接读取的

如下所示:

复制代码
if ((ret = ffmpeg.avformat_open_input(&formatContext, _audioUrl, _inputAudioFormat, &options)) < 0)
{
    ffmpeg.av_dict_free(&options);
    ffmpeg.avformat_close_input(&formatContext);
    throw new Exception($"Could not open input path: {_audioUrl} (error '{FFmpegHelper.av_err2str(ret)}')");
}

需要注意的是,因为是网络音频流,这个音频流是不带位深和采样率信息的,需要我们自己提前指定

复制代码
AVCodecParameters* codecpar = formatContext->streams[_audioIndex]->codecpar;

if (SampleRate > 0)
{
    codecpar->sample_rate = 61000;
    codecpar->channels = ffmpeg.av_get_channel_layout_nb_channels((ulong)_channelLayout);
}
_inputAudioFormat = ffmpeg.av_find_input_format("s16le");

由上面初始化开启探针读取网络音频流和视频流之后,剩下的处理就是合并了。

合并这里本质上就是合并读取出来的audioPacket和videoPacket,然后将packet转为frame,这时候转换为要输出的格式,再构建到packet中,最终将packet交错写入到同一个_formatContext进行保存。(很大白话了23333)
ffmpeg.av_interleaved_write_frame(_formatContext, outputPacket);

三、音画不同步的问题

上面的功能实现之后又发现保存后的视频存在音画不同步的问题

现象是:

①实时播放过程中,耳机反馈实时播放声音,实际动作比声音略晚,视频略早于声音

②播放视频过程中,声音比动作先出现了,也就是音频早于视频。

这里①很正常,因为本来音频通过wave过去并播放,肯定是需要时间的

但是②就不太对了,这是摆明了就是视频音画不同步。

而音画不同步的原因有以下几种可能:

①网络延迟:音频和视频数据在传输过程中可能会受到网络延迟的影响,导致数据到达接收端的时间不同步。这可能是由于网络拥塞、传输路径不稳定等原因引起的。

②编解码延迟:音频和视频的编解码过程可能会引入一定的延迟,导致数据的播放时间不同步。不同的编解码算法和参数设置可能会对延迟产生影响。

③媒体同步机制:RTP流中的音频和视频数据通常是分开传输的,接收端需要根据时间戳等信息将它们进行同步播放。如果同步机制实现不正确或者缺失,就会导致音频和视频不同步。

其实原因看起来很多,但归根到底都还是同步问题。

这里上面的现象让我直接推断是视频进入帧的时候比较晚,于是直接就怀疑了是视频读包要晚于音频的情况,想到这我们直接验证:

只需要在写入第一个视频帧和写入第一个音频帧的地方打印一个距流开启的时间就能观察到问题,这里我使用了_stopwatch进行准确的打印。

结果确实也验证了上面的想法------音频的偏移大概是35ms左右,可以忽略不计,而视频相较于音频大概延迟400-500ms。

那么我们这时候就可以进行音画同步了。

四、音画同步

首先说明:这里的同步是针对于探针时间偏差和编解码延迟导致的音画不同步问题设计的。

原理很简单:通过音频和视频写入第一帧的时间和流开启的时间的偏差,移动视频和音频的pts和dts进行对齐。

首先从上面的例子中就可以获取_stopwatch.ElapsedMilliseconds,分别是音频写入第一帧的时间和视频写入第一帧的时间与流开启的时间的偏差,这里分别是音频35ms,视频400ms。这时候我们统一对音频和视频pts和dts进行左移,将二者的首端都移到视频起始的位置。

这里需要注意的是,我们需要在时基转换之后的outpacket上进行操作,不然无法生效。
ffmpeg.av_packet_rescale_ts(outputPacket, _audioCodecContext->time_base, _audioStream->time_base);

在上面的时基转换后,我们以音频举例,这时获取了_sampleStartPts(音频的偏移),因为ElapsedMilliseconds是毫秒,所以我们需要除1000换算成秒,然后乘以时基,这时候我们就得到了pts实际的需要偏移的量。
_sampleStartPts = _stopwatch.ElapsedMilliseconds * _audioStream->time_base.den / 1000;

(特别提醒:这里的时基我默认分子为1,所以我直接用的分母,如果分子不同需要加入进行换算)

得到了ptsOffset(_sampleStartPts)之后呢,我们直接对所有输出的packet进行pts、dts移动就可以啦

复制代码
outputPacket->pts -= _sampleStartPts;
outputPacket->dts -= _sampleStartPts;

这里因为是要去首端对齐,所以是减去偏移量~

最后再把上面的操作对应到视频流中执行一遍,出来的视频音画就同步了~

ps 文章原创于idealy233,转载请私信哦~