音视频丢包、卡顿问题总结

418 阅读4分钟

笔者前段时间参加了一个摄像头项目,在做的过程中遇到了一些播放卡顿、掉线问题,在此记录下来,以便后台人避坑。

 一、断连、丢包

先说一下我们的网络架构,因为是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被耗干。