IOS technology sharing | adding beauty filters in iOS WebRTC

When using WebRTC, there are generally two ways to beautify the video: replacing the acquisition module in WebRTC and beautifying the video data.

1, Replace the acquisition module in WebRTC

It is relatively simple to replace the acquisition module in WebRTC. Use GPUImageVideoCamera to replace the video acquisition in WebRTC, get the image after adding beauty processing by GPUImage, and send it to the OnFrame method of WebRTC.

Refer to the full platform push-pull stream SDK developed based on WebRTC framework: Github

Set beauty

- (void)setBeautyFace:(BOOL)beautyFace{
    if(_beautyFace == beautyFace) return;

    _beautyFace = beautyFace;
    [_emptyFilter removeAllTargets];
    [_filter removeAllTargets];
    [_videoCamera removeAllTargets];

     if(_beautyFace){
         _filter = [[GPUImageBeautifyFilter alloc] init];
         _emptyFilter = [[GPUImageEmptyFilter alloc] init];
     }else{
         _filter = [[GPUImageEmptyFilter alloc] init];
     }

     __weak typeof(self) _self = self;
     [_filter setFrameProcessingCompletionBlock:^(GPUImageOutput *output, CMTime time) {
         [_self processVideo:output];
     }];
       [_videoCamera addTarget:_filter];
  if (beautyFace) {
      [_filter addTarget:_emptyFilter];
      if(_gpuImageView) [_emptyFilter addTarget:_gpuImageView];
  } else {
      if(_gpuImageView) [_filter addTarget:_gpuImageView];
  }
}

format conversion

The Pixel format after GPUImage processing is BGRA. After processing, it needs to be converted to I420 format for internal processing and rendering.

WebRTC uses the Pixel in NV12 format when encoding, so secondary format conversion will be carried out during encoding

-(void) processVideo:(GPUImageOutput *)output{
  rtc::CritScope cs(&cs_capture_);
  if (!_isRunning) {
      return;
  }
  @autoreleasepool {
      GPUImageFramebuffer *imageFramebuffer = output.framebufferForOutput;

  size_t width = imageFramebuffer.size.width;
  size_t height = imageFramebuffer.size.height;
  uint32_t size = width * height * 3 / 2;

  if(self.nWidth != width || self.nHeight != height)
  {
      self.nWidth = width;
      self.nHeight = height;
      if(_dst)
          delete[] _dst;
      _dst = NULL;
  }
  if(_dst == NULL)
  {
      _dst = new uint8_t[size];
  }
  uint8_t* y_pointer = (uint8_t*)_dst;
  uint8_t* u_pointer = (uint8_t*)y_pointer + width*height;
  uint8_t* v_pointer = (uint8_t*)u_pointer + width*height/4;
  int y_pitch = width;
  int u_pitch = (width + 1) >> 1;
  int v_pitch = (width + 1) >> 1;

  libyuv::ARGBToI420([imageFramebuffer byteBuffer], width * 4, y_pointer, y_pitch, u_pointer, u_pitch, v_pointer, v_pitch, width, height);
  if(self.bVideoEnable)
      libyuv::I420Rect(y_pointer, y_pitch, u_pointer, u_pitch, v_pointer, v_pitch, 0, 0, width, height, 32, 128, 128);

  if(_capturer != nil)
      _capturer->CaptureYUVData(_dst, width, height, size);
  }
}

Send the data after beauty to the OnFrame method of WebRTC

Gpuimagevideocamera class is a camera class encapsulated by GPUImage, which is consistent with the collection class function in WebRTC. By inheriting the cricket:: videocamera class, you can insert the collected audio and video stream into WebRTC.

namespace webrtc {
  // Inherit cricket:: videocamera
	class GPUImageVideoCapturer : public cricket::VideoCapturer {
		...
	}
}
void GPUImageVideoCapturer::CaptureYUVData(const webrtc::VideoFrame& frame, int width, int height)
{
	VideoCapturer::OnFrame(frame, width, height);
}

2, Beautify video data

The idea of video data beauty is the traditional third-party beauty SDK, Process the internally collected audio and video data: internally collected data (CVPixelBufferRef) - "convert to texture" - "beautify the texture of audio and video" - "convert the texture of beautify to the collected data of iOS (CVPixelBufferRef) -" return to WebRTC for internal rendering, coding and transmission.

Synchronous thread

Generally, synchronous threads are used for internal processing, which can ensure the linear flow of data. See the code snippet in GPUImage

 runSynchronouslyOnVideoProcessingQueue(^{
 // Beauty treatment
 });

Convert CVPixelBufferRef data to texture

Conversion method of RGB format type
  • Method of CoreVideo framework: use this method to create CVOpenGLESTextureRef texture and obtain texture id through CVOpenGLESTextureGetName(texture).

    - (GLuint)convertRGBPixelBufferToTexture:(CVPixelBufferRef)pixelBuffer {
        if (!pixelBuffer) {
            return 0;
        }
        CGSize textureSize = CGSizeMake(CVPixelBufferGetWidth(pixelBuffer),
                                        CVPixelBufferGetHeight(pixelBuffer));
        CVOpenGLESTextureRef texture = nil;
        CVReturn status = CVOpenGLESTextureCacheCreateTextureFromImage(nil,
                                                                        	   [[GPUImageContext sharedImageProcessingContext] coreVideoTextureCache],
                                                                       pixelBuffer,
                                                                       nil,
                                                                       GL_TEXTURE_2D,
                                                                       GL_RGBA,
                                                                       textureSize.width,
                                                                       textureSize.height,
                                                                       GL_BGRA,
                                                                       GL_UNSIGNED_BYTE,
                                                                       0,
                                                                       &texture);
        
        if (status != kCVReturnSuccess) {
            NSLog(@"Can't create texture");
        }
        self.renderTexture = texture;
        return CVOpenGLESTextureGetName(texture);
    }
    
  • OpenGL method: create a texture object and upload the image data in CVPixelBufferRef to the texture object using glTexImage2D method.

        glBindTexture(GL_TEXTURE_2D, [outputFramebuffer texture]);
        glTexImage2D(GL_TEXTURE_2D, 0, _pixelFormat==GPUPixelFormatRGB ? GL_RGB : GL_RGBA, (int)uploadedImageSize.width, (int)uploadedImageSize.height, 0, (GLint)_pixelFormat, (GLenum)_pixelType, bytesToUpload);
    
Conversion method of YUV format type
- (GLuint)convertYUVPixelBufferToTexture:(CVPixelBufferRef)pixelBuffer {
    if (!pixelBuffer) {
        return 0;
    }
    
    CGSize textureSize = CGSizeMake(CVPixelBufferGetWidth(pixelBuffer),
                                    CVPixelBufferGetHeight(pixelBuffer));

    [EAGLContext setCurrentContext:self.context];
    
    GLuint frameBuffer;
    GLuint textureID;
    
    // FBO
    glGenFramebuffers(1, &frameBuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
    
    // texture
    glGenTextures(1, &textureID);
    glBindTexture(GL_TEXTURE_2D, textureID);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, textureSize.width, textureSize.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
    
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    
    
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureID, 0);
    
    glViewport(0, 0, textureSize.width, textureSize.height);
    
    // program
    glUseProgram(self.yuvConversionProgram);
    
    // texture
    CVOpenGLESTextureRef luminanceTextureRef = nil;
    CVOpenGLESTextureRef chrominanceTextureRef = nil;

    CVReturn status = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
                                                                   self.textureCache,
                                                                   pixelBuffer,
                                                                   nil,
                                                                   GL_TEXTURE_2D,
                                                                   GL_LUMINANCE,
                                                                   textureSize.width,
                                                                   textureSize.height,
                                                                   GL_LUMINANCE,
                                                                   GL_UNSIGNED_BYTE,
                                                                   0,
                                                                   &luminanceTextureRef);
    if (status != kCVReturnSuccess) {
        NSLog(@"Can't create luminanceTexture");
    }
    
    status = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
                                                          self.textureCache,
                                                          pixelBuffer,
                                                          nil,
                                                          GL_TEXTURE_2D,
                                                          GL_LUMINANCE_ALPHA,
                                                          textureSize.width / 2,
                                                          textureSize.height / 2,
                                                          GL_LUMINANCE_ALPHA,
                                                          GL_UNSIGNED_BYTE,
                                                          1,
                                                          &chrominanceTextureRef);
    
    if (status != kCVReturnSuccess) {
        NSLog(@"Can't create chrominanceTexture");
    }
    
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, CVOpenGLESTextureGetName(luminanceTextureRef));
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glUniform1i(glGetUniformLocation(self.yuvConversionProgram, "luminanceTexture"), 0);
    
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, CVOpenGLESTextureGetName(chrominanceTextureRef));
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glUniform1i(glGetUniformLocation(self.yuvConversionProgram, "chrominanceTexture"), 1);
    
    GLfloat kXDXPreViewColorConversion601FullRange[] = {
        1.0,    1.0,    1.0,
        0.0,    -0.343, 1.765,
        1.4,    -0.711, 0.0,
    };
    
    GLuint yuvConversionMatrixUniform = glGetUniformLocation(self.yuvConversionProgram, "colorConversionMatrix");
    glUniformMatrix3fv(yuvConversionMatrixUniform, 1, GL_FALSE, kXDXPreViewColorConversion601FullRange);
    
    // VBO
    glBindBuffer(GL_ARRAY_BUFFER, self.VBO);
    
    GLuint positionSlot = glGetAttribLocation(self.yuvConversionProgram, "position");
    glEnableVertexAttribArray(positionSlot);
    glVertexAttribPointer(positionSlot, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
    
    GLuint textureSlot = glGetAttribLocation(self.yuvConversionProgram, "inputTextureCoordinate");
    glEnableVertexAttribArray(textureSlot);
    glVertexAttribPointer(textureSlot, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3* sizeof(float)));
    
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    
    glDeleteFramebuffers(1, &frameBuffer);
    
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    
    glFlush();
    
    self.luminanceTexture = luminanceTextureRef;
    self.chrominanceTexture = chrominanceTextureRef;
    if (luminanceTextureRef) {
        CFRelease(luminanceTextureRef);
    }
    if (chrominanceTextureRef) {
        CFRelease(chrominanceTextureRef);
    }
  
    
    return textureID;
}

Use GPUImageTextureInput to load filters and GPUImageTextureOutput to output data

    [GPUImageContext setActiveShaderProgram:nil];
        GPUImageTextureInput *textureInput = [[ARGPUImageTextureInput alloc] initWithTexture:textureID size:size];
     GPUImageSmoothToonFilter *filter = [[GPUImageSmoothToonFilter alloc] init];
     [textureInput addTarget:filter];
     GPUImageTextureOutput *textureOutput = [[GPUImageTextureOutput alloc] init];
     [filter addTarget:textureOutput];
     [textureInput processTextureWithFrameTime:kCMTimeZero];

Get textureOutput, that is, get the output texture.

GPUImageTextureOutput texture converted to CVPixelBufferRef data

- (CVPixelBufferRef)convertTextureToPixelBuffer:(GLuint)texture
                                    textureSize:(CGSize)textureSize {
    [EAGLContext setCurrentContext:self.context];
    
    CVPixelBufferRef pixelBuffer = [self createPixelBufferWithSize:textureSize];
    GLuint targetTextureID = [self convertRGBPixelBufferToTexture:pixelBuffer];
    
    GLuint frameBuffer;
    
    // FBO
    glGenFramebuffers(1, &frameBuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
    
    // texture
    glBindTexture(GL_TEXTURE_2D, targetTextureID);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, textureSize.width, textureSize.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
    
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, targetTextureID, 0);
    
    glViewport(0, 0, textureSize.width, textureSize.height);
    
    // program
    glUseProgram(self.normalProgram);
    
    // texture
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, texture);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glUniform1i(glGetUniformLocation(self.normalProgram, "renderTexture"), 0);
    
    // VBO
    glBindBuffer(GL_ARRAY_BUFFER, self.VBO);
    
    GLuint positionSlot = glGetAttribLocation(self.normalProgram, "position");
    glEnableVertexAttribArray(positionSlot);
    glVertexAttribPointer(positionSlot, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
    
    GLuint textureSlot = glGetAttribLocation(self.normalProgram, "inputTextureCoordinate");
    glEnableVertexAttribArray(textureSlot);
    glVertexAttribPointer(textureSlot, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3* sizeof(float)));
    
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    
    glDeleteFramebuffers(1, &frameBuffer);
    
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    
    glFlush();
    
    return pixelBuffer;
}

The CVPixelBufferRef after beauty is synchronized and returned to the SDK for rendering and transmission.

3, Summary

The beauty of audio and video has become a common function of audio and video applications. In addition to the above two methods, third-party beauty can also be used. Generally, audio and video manufacturers provide self collection function, while the third-party beauty function provides Beauty Collection camera function. The two can be seamlessly combined. If you don't have high requirements for beauty in your own application, you can use the beauty provided by the audio and video SDK (whitening, beautifying and ruddy). If you use it in entertainment scenes, you must integrate the third-party beauty SDK in addition to beauty (thin face, big eyes) and stickers (2D and 3D).

Keywords: iOS webrtc

Added by br0ken on Sat, 18 Dec 2021 19:33:21 +0200