WebRTC音视频之美颜开启后小流闪烁问题

Posted hbblzjy

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了WebRTC音视频之美颜开启后小流闪烁问题相关的知识,希望对你有一定的参考价值。

美颜开启后,打开双流,远端查看小流时,小流闪烁问题

项目中使用的是GPUImage进行的美颜处理,添加了美白、磨皮、亮光,但是当开启美颜效果后,如果只使用大流进行查看视频,视频美颜正常,画面正常,但是如果使用小流查看美颜画面,视频画面中会偶尔闪烁黑块,类似于大块马赛克效果,然后进行分析,如果将美颜效果关闭,小流视频画面正常,视频处理流程:将采集到的视频画面,进行YUV或者RGB美颜渲染处理,然后将画面帧传递给WebRTC进行编码,最后上行给服务器,下发给远端观看。

猜测,既然关闭美颜画面,小流画面没有问题,那么有可能是美颜处理出现的问题,于是,将美颜相关的代码进行注释,发现画面黑屏,但是依然有黑块闪动,所以大概率不是美颜处理造成的问题。

继续分析代码,发现先美颜,后编码的过程中,都是处理的同一个地址的视频帧数据,但是美颜和编码是在两个不同的异步线程进行处理,这就会导致,编码处理的数据和美颜处理的数据不一致,因为美颜会改变YUV数据,所以有可能小流时造成黑块闪动问题。于是添加了一个copy视频数据的方法,使编码处理的数据和美颜处理的数据保持一致,然后检测发现小流不再闪动,问题解决了(无比开心!无比激动!)。

copy视频数据地址的方法
/// RGB/BGR buffer copy
+ (CVPixelBufferRef)RGBBuffereCopyWithPixelBuffer:(CVPixelBufferRef)pixelBuffer 
    // Get pixel buffer info
    CVPixelBufferLockBaseAddress(pixelBuffer, 0);
    int bufferWidth = (int)CVPixelBufferGetWidth(pixelBuffer);
    int bufferHeight = (int)CVPixelBufferGetHeight(pixelBuffer);
    size_t bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer);
    uint8_t *baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer);
    OSType pixelFormat = kCVPixelFormatType_32BGRA;
    
    // Copy the pixel buffer
    CVPixelBufferRef pixelBufferCopy = NULL;
    CFDictionaryRef empty = CFDictionaryCreate(kCFAllocatorDefault, NULL, NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); // our empty iosurface properties dictionary
    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                             [NSNumber numberWithBool:YES], kCVPixelBufferCGImageCompatibilityKey,
                             [NSNumber numberWithBool:YES], kCVPixelBufferCGBitmapContextCompatibilityKey,
                             empty, kCVPixelBufferIOSurfacePropertiesKey,
                             nil];
    CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, bufferWidth, bufferHeight, pixelFormat, (__bridge CFDictionaryRef) options, &pixelBufferCopy);
    if (status == kCVReturnSuccess) 
        CVPixelBufferLockBaseAddress(pixelBufferCopy, 0);
        uint8_t *copyBaseAddress = CVPixelBufferGetBaseAddress(pixelBufferCopy);
        memcpy(copyBaseAddress, baseAddress, bufferHeight * bytesPerRow);
        
        CVPixelBufferUnlockBaseAddress(pixelBufferCopy, 0);
    else 
        SmoothLogE("RGBBuffereCopyWithPixelBuffer :: failed");
    
    
    CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
    return pixelBufferCopy;

然而高兴的太早了,测试时发现,稳定性变差了,原因是copy了一份数据,运行时间稍微长点儿,内存就会增长,在旧iPhone上体验不好,于是继续深入研究......

根据闪动黑块的现象和注释美颜代码后的画布现象,感觉像是编码的画面和美颜的画面不是同一个尺寸大小,于是分析编码代码,然后发现有一个同播编码适配器的方法(int SimulcastEncoderAdapter::Encode)进行编码,方法中有对视频画面进行缩放处理,于是尝试将此方法进行更改。

int SimulcastEncoderAdapter::Encode(
    const VideoFrame& input_image,
    const std::vector<VideoFrameType>* frame_types) 
  RTC_DCHECK_RUN_ON(&encoder_queue_);

  if (!Initialized()) 
    return WEBRTC_VIDEO_CODEC_UNINITIALIZED;
  
  if (encoded_complete_callback_ == nullptr) 
    return WEBRTC_VIDEO_CODEC_UNINITIALIZED;
  

  // All active streams should generate a key frame if
  // a key frame is requested by any stream.
  bool send_key_frame = false;
  if (frame_types) 
    for (size_t i = 0; i < frame_types->size(); ++i) 
      if (frame_types->at(i) == VideoFrameType::kVideoFrameKey) 
        send_key_frame = true;
        break;
      
    
  
  for (size_t stream_idx = 0; stream_idx < streaminfos_.size(); ++stream_idx) 
    if (streaminfos_[stream_idx].key_frame_request &&
        streaminfos_[stream_idx].send_stream) 
      send_key_frame = true;
      break;
    
  

  int src_width = input_image.width();
  int src_height = input_image.height();
  for (size_t stream_idx = 0; stream_idx < streaminfos_.size(); ++stream_idx) 
    // Don't encode frames in resolutions that we don't intend to send.
    if (!streaminfos_[stream_idx].send_stream) 
      continue;
    

    std::vector<VideoFrameType> stream_frame_types;
    if (send_key_frame) 
      stream_frame_types.push_back(VideoFrameType::kVideoFrameKey);
      streaminfos_[stream_idx].key_frame_request = false;
     else 
      stream_frame_types.push_back(VideoFrameType::kVideoFrameDelta);
    

    int dst_width = streaminfos_[stream_idx].width;
    int dst_height = streaminfos_[stream_idx].height;
    // If scaling isn't required, because the input resolution
    // matches the destination or the input image is empty (e.g.
    // a keyframe request for encoders with internal camera
    // sources) or the source image has a native handle, pass the image on
    // directly. Otherwise, we'll scale it to match what the encoder expects
    // (below).
    // For texture frames, the underlying encoder is expected to be able to
    // correctly sample/scale the source texture.
    // TODO(perkj): ensure that works going forward, and figure out how this
    // affects webrtc:5683.
      /**
       如果不需要缩放,因为输入分辨率与目标匹配,或者输入图像为空(例如,带有内部相机源的编码器的关键帧请求),或者源图像具有本机句柄,则直接传递图像。否则,我们将对其进行缩放以匹配编码器期望的内容(如下所示)。对于纹理帧,底层编码器应能够正确采样/缩放源纹理。TODO(perkj):确保这项工作继续下去,并找出这对webrtc:5683的影响。
       不进行缩放,保持大小流两个数据一致,进行编码,同时注释掉RTCVideoEncoderH264.mm编码方法- (NSInteger)encode:(RTCVideoFrame *)frame
       codecSpecificInfo:(nullable id<RTCCodecSpecificInfo>)codecSpecificInfo
              frameTypes:(NSArray<NSNumber *> *)frameTypes中宽、高判断:
//  RTC_DCHECK_EQ(frame.width, _width);
//  RTC_DCHECK_EQ(frame.height, _height);
可以解决小流闪动的问题
       */
//    if ((dst_width == src_width && dst_height == src_height) /*||
//        input_image.video_frame_buffer()->type() ==
//            VideoFrameBuffer::Type::kNative*/) 
      int ret = streaminfos_[stream_idx].encoder->Encode(input_image,
                                                         &stream_frame_types);
      if (ret != WEBRTC_VIDEO_CODEC_OK) 
        return ret;
      
//     else 
//      rtc::scoped_refptr<I420Buffer> dst_buffer =
//          I420Buffer::Create(dst_width, dst_height);
//      rtc::scoped_refptr<I420BufferInterface> src_buffer =
//          input_image.video_frame_buffer()->ToI420();
//      libyuv::I420Scale(src_buffer->DataY(), src_buffer->StrideY(),
//                        src_buffer->DataU(), src_buffer->StrideU(),
//                        src_buffer->DataV(), src_buffer->StrideV(), src_width,
//                        src_height, dst_buffer->MutableDataY(),
//                        dst_buffer->StrideY(), dst_buffer->MutableDataU(),
//                        dst_buffer->StrideU(), dst_buffer->MutableDataV(),
//                        dst_buffer->StrideV(), dst_width, dst_height,
//                        libyuv::kFilterBilinear);
//
//      // UpdateRect is not propagated to lower simulcast layers currently.
//      // TODO(ilnik): Consider scaling UpdateRect together with the buffer.
//      VideoFrame frame(input_image);
//      frame.set_video_frame_buffer(dst_buffer);
//      frame.set_rotation(webrtc::kVideoRotation_0);
//      frame.set_update_rect(
//          VideoFrame::UpdateRect0, 0, frame.width(), frame.height());
//      int ret =
//          streaminfos_[stream_idx].encoder->Encode(frame, &stream_frame_types);
//      if (ret != WEBRTC_VIDEO_CODEC_OK) 
//        return ret;
//      
//    
  

  return WEBRTC_VIDEO_CODEC_OK;

然后效果测试,发现小流闪动问题解决,并且内存使用情况基本和以前保持一致,于是可以说彻底解决了此问题。(无比开心,终于解决了一个疑难杂症,坚持就是胜利!!!)

以上是关于WebRTC音视频之美颜开启后小流闪烁问题的主要内容,如果未能解决你的问题,请参考以下文章

iOS WebRTC 杂谈之 视频采集添加美颜特效

IOS技术分享| 在iOS WebRTC 中添加美颜滤镜

IOS技术分享| 在iOS WebRTC 中添加美颜滤镜

Android技术分享| 超简单!给 Android WebRTC增加美颜滤镜功能

视频理解开山之作——双流卷积网络

Microsoft 365 - Teams会议时如何开启美颜功能