笔者前段时间参加了一个摄像头项目,在做的过程中遇到了一些播放卡顿、掉线问题,在此记录下来,以便后台人避坑。
一、断连、丢包
先说一下我们的网络架构,因为是APP直连摄像头,为了提高NAT的穿透率,我们使用了libcoturn作为p2p连接的探测库,然后通过UDP传输连接、控制指令和音视频数据。由于STUN服务器在香港,加上UDP不稳定性,会经常出现丢包、断连的情况。主要有以下几个问题:
1.APP有时候连上设备几秒后就会断开链接,提示网络中断,而检查网络发现并没有问题,可以正常访问百度等网站。
我分析估计是有什么死循环导致线程阻塞而无法收包了。看了下Xcode的性能指标,发现APP的收包线程CPU占用率较高,有时会发现连续几十秒达到100%,然后再分析了一下代码,发现是在收包线程做了解码,而没有开新线程去做。具体代码比较多,这里就不贴出来了。
2. APP跟设备端的连接多数情况下并没有走P2P模式,而是走了TURN 模式, 也就是服务器转发,而服务器的带宽有限,当并发请求较多时丢包率就会上升。
这个主要还是服务器的问题,穿透率调整的空间不大,这块我研究也不多,就不展开讲了,主要优化手段还是服务器,加大带宽,调整节点。
3. 视频关键帧太大,分了太多包,导致丢了部分包的情况下出现了花屏现象。
这个一开始尝试过通过setsockopt加大接收端缓冲区,但是好像并没有效果,丢包还是很厉害,后面加上了ACK、NAK等回包和重传机制,丢包率有所下降。但是依然有改进空间,主要是发送端的重传机制不完善,没有拥塞控制,导致网络抖动很大时,一下子发送了太多包,接收端处理不过来。
二、 卡死
卡死主要是APP方面的,设备端其实也有卡死,但不是我负责的范围,就不详细说了。
1. lock和unlock未匹配导致的卡死。
这个问题主要发生在从设备列表切换到其他页面时整个APP卡住了,一开始我认为是UI线程被阻塞了,但是查了很久,没有看到可疑的代码,然后再看了下Xcode,发现卡死是主线程的CPU占用率为0,这就说明是加锁导致的。唯一用到锁的地方就是packetqueue的push和pop方法,通过在每个lock和unlock的地方加日志的方式,我定位到了问题点,在 debugPrint("packet全部pop!") 这一行,没有unlock,也就是队列长度为0时,会发生卡死。
mutating func push(val:T) ->Int {
mutex_.lock();
if (1 == abort_) {
return -1;
}
queue_.append(val)
cond_.unlock(withCondition: 1)
mutex_.unlock()
return 0;
}
mutating func pop(timeout: Int, lock:Bool) throws -> T? {
if (lock) {
mutex_.lock()
if(queue_.count == 0) {
// 等待push或超时唤醒
cond_.lock(whenCondition: 1, before: Date.init(timeIntervalSinceNow: TimeInterval(timeout/1000))) } if (abort_ != 0) { mutex_.unlock() throw NSError.init(domain: kErrorDomainQueue, code: -1) }
if (queue_.count == 0){
mutex_.unlock()
// debugPrint("packet全部pop!")
throw NSError.init(domain: kErrorDomainQueue, code: -2)
}
let val = queue_[0]
queue_.remove(at: 0)
mutex_.unlock()
return val;
}else{
if (queue_.count == 0) {
// debugPrint("packet全部pop!")
throw NSError.init(domain: kErrorDomainQueue, code: -2)
}
let val = queue_[0]
queue_.remove(at: 0)
return val
}
}
当把unlock加上之后,问题解决了。不过我一直没明白的一点是,这个push和pop都不在主线程调用,为什么会导致卡死。
2. 线程休眠时间过短导致的卡死
这个问题跟上面的问题有一点关联,主要表现是,在解码的while循环里加日志会导致APP卡死。 一开始我以为还是加锁导致的,后面仔细看了下代码和CPU使用率,发现并不是,因此这时候CPU使用率是接近100%的,而且很快就会导致APP崩溃。
- (void)run {
while(_abort!=1) {
NSLog(@"packet pop");
AVPacket* pkt = [_packet_queue pop:10];
if(pkt) {
NSLog(@"decode packet");
[self decodePacket:pkt];
}
}
}
@objc public func push(_ value: UnsafeMutablePointer<AVPacket>) ->Int{
let tmp_pkt = av_packet_alloc();
av_packet_move_ref(tmp_pkt, value);
return queue_.push(val: tmp_pkt!)
}
@objc public func pop(_ timeout: Int) -> UnsafeMutablePointer<AVPacket>? {
do {
let tmp_pkt = try queue_.pop(timeout: timeout, lock: true)
return tmp_pkt;
}catch (let error){
// debugPrint("PacketQueue pop failed", error.localizedDescription);
}
return nil;
}
看了下这段代码,也并不复杂,就是先从队列中取出第一个packet,如果队列为空就休眠10毫秒,如果取到的packet不为空,就进行解码。这里会有什么问题呢?从代码上我一时间发现问题, 于是就看了下打印的日志,发现1秒钟竟然打印了几千行,而且基本上是同一时间点。这就说明完全没有休眠。 再回过头来看代码, 就发现cond_.lock 这句在计算TimeInterval的时候,由于timeout 是整数并且不大于1000,所以除以1000后 结果四舍五入成了0。而没有休眠的while循环最终导致了CPU被耗干。