音视频同步:
(1)音频向视频同步
视频维持一定的刷新频率,当我们向AudioOutput模块填充音频数据的时候,会与当前渲染的视频帧的时间戳进行比较,这个差值如果不在阈值的范围内,就需要做对齐操作;如果其在阈值范围内,那么就可以直接将本帧音频帧填充到AudioOutput模块,进而让用户听到该声音。
那如果不在阈值范围内,又该如何进行对齐操作呢?
1.这就需要我们去调整音频帧了,也就是说如果要填充的音频帧的时间戳比当前渲染的视频帧的时间戳小,那就需要进行跳帧操作(具体的跳帧操作可以是加快速度播放的实现,也可以是丢弃一部分音频帧的实现);
2.如果音频帧的时间戳比当前渲染的视频帧的时间戳大,那么就需要等待,具体实现可以是向AudioOutput模块填充空数据并进行播放,也可以是将音频的速度放慢播放给用户听,而此时视频帧是继续一帧一帧进行渲染的,
3.一旦视频的时间戳赶上了音频的时间戳,就可以将本帧音频帧的数据填充到AudioOutput模块了。
(2)视频向音频同步
我们可以依赖于音频的顺序播放为我们提供的时间戳,当客户端代码请求发送视频帧的时候,会先计算出当前视频队列头部的视频帧元素的时间戳与当前音频播放帧的时间戳的差值。如果在阈值范围内,就可以渲染这一帧视频帧;如果不在阈值范围内,则要进行对齐操作。
具体的对齐操作方法就是:
如果当前队列头部的视频帧的时间戳小于当前播放音频帧的时间戳,那么就进行跳帧操作;如果大于当前播放音频帧的时间戳,那么就进行等待(重复渲染上一帧或者不进行渲染)的操作。
(3)选取外部时间作为参考时钟源
如果当前视频帧落后于主时钟源,则需要减小下一帧画面的等待时间;
如果视频帧超前,并且上一帧的显示时间小于显示更新门槛,则采取加倍延时的策略;
考虑人对视频、和音频的敏感度,在存在音频的情况下,优先选择音频作为主时钟源。ijkplayer在默认情况下也是使用音频作为参考时钟源,处理同步的过程主要在视频渲染video_refresh_thread的线程中,该方法中主要循环做两件事情:
- 休眠等待,remaining_time的计算在video_refresh中
- 调用video_refresh方法,刷新视频帧
平滑发送:
udp传输一般都是不断地、循环地取数据,然后发送,这就会造成了两个问题,第一:数据传送一窝蜂,如此,若网络带宽不够,则丢包,而要是对端接收处理不过来,则造成数据人为性丢失;
udp是不可靠传输,因此要实现可靠传输,必须有丢失重传机制,因此,在接收端就得在接收到数据后,把数据头返回,我们把它称为确认 ack,而发送端接收到这个确认,才释放数据内存,如果在规定时间内未接收到确认,则重传。
传输速度取决于两个方面,一个是传输数据的长度,另一个是发送的频率。
控制传输数据的长度:
在丢包率小于packet_len_rate_min时,我们增加包长,当丢包率大于packet_len_rate_max时我们减短包长度。当然包长度不小于576,不大于1500。丢包率计算方法是:在单位时间内,丢失的包占总发包数的百分比。这样就实现了从数据包长度方面来动态调节发送速度。
控制发送频率:
1.我们从源数据a上每次获取一块数据,然后把数据放入到发送队列b里面,我们的发送线程再在b里获取数据并发送出去,然后等待,一直等待到接收到返回确认后,再去从数据源a读取数据,然后放入到发送队列b里。
2.当接收到返回确认后,我们从源数据a里面读取倍的数据,放入到发送队列b里面,由此,1变2,2变4,4变8......发送的速度是越来越快。但发送速度太快可能会带宽不够,或对端处理能力不足而丢包,此时,packet_send_rate_min与packet_send_rate_max就用上了。
这样实现的发送频率非常均匀,而且在发送数据的时候,传输速度是由慢变快,自适应变化,达到极速.
回声消除的原理是什么?
声学回音是由于在免提或者会议应用中,扬声器的声音多次反馈到麦克风引起的;尽管回声消除是非常复杂的技术,但我们可以简单的描述这种处理方法:
1.房间A收到房间B传来的声音
2.收到B的声音时先进行采样,这一采样作为回音消除的参考
3.随后声音被送到A房间的音箱和回声消除器中
4.A房间同时拾取A的声音和由音箱播放出来B的声音
5.声音到达回声消除器中,会与原始的采样进行比较,移除B房间的声音
H264硬解码
Apple在iOS8以后支持了硬解码
VTDecompressionSessionCreate 创建解码 session
VTDecompressionSessionDecodeFrame 解码一个frame
VTDecompressionSessionInvalidate 销毁解码 session
1.首先创建解码 session
OSStatus status = VTDecompressionSessionCreate(kCFAllocatorDefault,
decoderFormatDescription,
NULL,
attrs, //attr是传递给decode session的属性词典
&callBackRecord, //callBackRecord 是用来指定回调函数的,解码器支持异步模式,解码后会调用这里的回调函数。
&deocderSession);
参数decoderFormatDescription创建:
其中 decoderFormatDescription 是 CMVideoFormatDescriptionRef 类型的视频格式描述,这个需要用H.264的 sps 和 pps数据来创建,调用以下函数创建 decoderFormatDescription
CMVideoFormatDescriptionCreateFromH264ParameterSets
需要注意的是,这里用的 sps和pps数据是不包含“00 00 00 01”的start code的。
参数attr是传递给decode session的属性词典:
CFDictionaryRef attrs = NULL;
const void *keys[] = { kCVPixelBufferPixelFormatTypeKey };
// kCVPixelFormatType_420YpCbCr8Planar is YUV420
// kCVPixelFormatType_420YpCbCr8BiPlanarFullRange is NV12
uint32_t v = kCVPixelFormatType_420YpCbCr8BiPlanarFullRange;
const void *values[] = { CFNumberCreate(NULL, kCFNumberSInt32Type, &v) };
attrs = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
其中重要的属性就一个,kCVPixelBufferPixelFormatTypeKey,指定解码后的图像格式,必须指定成NV12,苹果的硬解码器只支持NV12。
2.decoderSession创建成功就可以开始解码了
VTDecodeFrameFlags flags = 0; // flags 用0 表示使用同步解码
VTDecodeInfoFlags flagOut = 0;
CVPixelBufferRef outputPixelBuffer = NULL;
OSStatus decodeStatus = VTDecompressionSessionDecodeFrame(deocderSession,
sampleBuffer, //sampleBuffer是输入的H.264视频数据,每次输入一个frame,
flags, //flags 用0 表示使用同步解码
&outputPixelBuffer, //解码成功之后,outputPixelBuffer里就是一帧 NV12格式的YUV图像了。
&flagOut);
CVPixelBufferRef是可以直接转换成UIImage的
CIImage *ciImage = [CIImage imageWithCVPixelBuffer:pixelBuffer];
UIImage *uiImage = [UIImage imageWithCIImage:ciImage];
3.解码完成后销毁 decoder session
VTDecompressionSessionInvalidate(deocderSession)












网友评论