iOS ReplayKit 50M限制处理策略

iOS录屏在之前一直是个难题,但是在官方推出ReplayKit之后,iOS进行录屏方便了很多。

业务层面上,进行游戏直播,屏幕共享,远程协助等等。

而目前App Store中相关的App也一抓一大把,主要分为以下两类:

  1. 远程屏幕直播类
  2. 本地录屏保存类

具体的工程实现时,ReplayKit2采取了Extension子进程的方式,但是系统给了50M内存限制,一旦超过50M,录屏的子进程就会崩溃。

就是由于这个限制,业界相似的处理方案都会限制其视频质量不超过720P,或者视频帧数在30之内。例如腾讯的直播SDK。


为了解决此问题,戴着镣铐舞蹈。

我们首先来看看子进程中都做了什么:

@implementation SampleHandler

- (void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *, NSObject *> *)setupInfo {
    // User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional.

}

- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
    switch (sampleBufferType) {
        case RPSampleBufferTypeVideo:
            // Handle video sample buffer
            break;
        case RPSampleBufferTypeAudioApp:
            // Handle audio sample buffer for app audio
            break;
        case RPSampleBufferTypeAudioMic:
            // Handle audio sample buffer for mic audio
            break;

        default:
            break;
    }
}

@end

重要函数只有两个:

  1. broadcastStartedWithSetupInfo:(NSDictionary<NSString *, NSObject *> *)setupInfo

    子进程开启回调

  2. processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType

    视频/音频的数据回调

从函数中可以看出来,回调的数据类型为CMSampleBufferRef,其本身几乎不占用内存。

但是我们将其转化为位图信息时,尤其是直接转化为二进制流信息时,才会相对耗费内存。


所以,为了保证内存消耗,我们的思路是,将子进程中的数据发送到主进程中,对于图像或者其他的操作都由主进程进行。

由此,我们引入了“进程通信”

可以满足我们要求的,使子进程和主进程可以通信的方式有:

  1. CFMachPort

    iOS7之后不再可用

  2. CFNotificationCenterRef

    只能发送简单点的字符串数据。

    如果发送复杂数据,对于数据组装要求较高。可以使用三方封装库MMWormhole实现。其原理是将数据归档到文件,然后在进程间发送文件标识,在接收端读取文件。效率比较低

  3. Local Socket

    在进程间建立本地Socket,进程TCP通信。

    使用灵活,效率高。

    我们使用GCDAsyncSocket实现,可以直接传输NSData数据流。


进程间通信的传输方式,我们最终决定使用本地Socket实现。

接下来我们需要考虑如何组装数据。

从系统API可以看到,回调函数中系统为我们提供的数据类型是CMSampleBufferRef

其实每一帧的视频数据,并且它是一种压缩过的,用于存储媒体文件属性的数据结构,它的组成部分如下:

CMTime:64位的value,32位的scale, media的时间格式

CMVideoFormatDesc:video的格式,包括宽高、颜色空间、编码格式、SPS、PPS

CVPixelBuffer: 包含未压缩的像素格式,宽高

CMBlockBuffer: 压缩的的图像数据

CMSampleBuffer: 存放一个或多个压缩或未压缩的媒体文件

如果可以将其发送到主进程再好不过,但是在不对其进行解码的情况下,目前还没有办法进行数据格式的转换,从而进行通信发送。

因此,进一步我们需要解决的问题的是,如何高效轻量的解码。

首先,直接转换成位图不可行,因为在比较大的屏幕分辨率下,每一帧都很吃内存。

所以,我们需要一种中间数据结构,来进行传输,它需要满足以下几个条件:

  1. 能够从CMSampleBufferRef中获取到图像信息,但是比imageData本身要轻量
  2. 从子进程传输到主进程后,可以将其还原为图片信息,并且可以再针对图片进行旋转,裁剪,压缩等操作

当然,解码的选择我们也有很多,无论是硬解,软解,YUV还是RGB。

但是无论怎样,我们都需要先解码。

曾经也想过,是否可以在CMSampleBufferRef本身上直接进行图片压缩等操作,但是最后放弃了。

基于以上,我们最终参考了网易云通信屏幕共享的处理方式,使用了YUV解码,与它的NTESI420Frame中间数据结构,来承载CMSampleBufferRef,就像载波信号一样。

其转换源码如下:

+ (NTESI420Frame *)pixelBufferToI420:(CVImageBufferRef)pixelBuffer
                            withCrop:(float)cropRatio
                          targetSize:(CGSize)size
                      andOrientation:(NTESVideoPackOrientation)orientation
{
    if (pixelBuffer == NULL) {
        return nil;
    }

    CVPixelBufferLockBaseAddress(pixelBuffer, 0);

    OSType sourcePixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer);

    size_t bufferWidth = 0;
    size_t bufferHeight = 0;
    size_t rowSize = 0;
    uint8_t *pixel = NULL;

    if (CVPixelBufferIsPlanar(pixelBuffer)) {
        int basePlane = 0;
        pixel = (uint8_t *)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, basePlane);
        bufferHeight = CVPixelBufferGetHeightOfPlane(pixelBuffer, basePlane);
        bufferWidth = CVPixelBufferGetWidthOfPlane(pixelBuffer, basePlane);
        rowSize = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, basePlane);
    } else {
        pixel = (uint8_t *)CVPixelBufferGetBaseAddress(pixelBuffer);
        bufferWidth = CVPixelBufferGetWidth(pixelBuffer);
        bufferHeight = CVPixelBufferGetHeight(pixelBuffer);
        rowSize = CVPixelBufferGetBytesPerRow(pixelBuffer);
    }

    NTESI420Frame *convertedI420Frame = [[NTESI420Frame alloc] initWithWidth:(int)bufferWidth height:(int)bufferHeight];

    int error = -1;

    if (kCVPixelFormatType_32BGRA == sourcePixelFormat) {
        error = libyuv::ARGBToI420(
            pixel, (int)rowSize,
            [convertedI420Frame dataOfPlane:NTESI420FramePlaneY], (int)[convertedI420Frame strideOfPlane:NTESI420FramePlaneY],
            [convertedI420Frame dataOfPlane:NTESI420FramePlaneU], (int)[convertedI420Frame strideOfPlane:NTESI420FramePlaneU],
            [convertedI420Frame dataOfPlane:NTESI420FramePlaneV], (int)[convertedI420Frame strideOfPlane:NTESI420FramePlaneV],
            (int)bufferWidth, (int)bufferHeight);
    } else if (kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange == sourcePixelFormat || kCVPixelFormatType_420YpCbCr8BiPlanarFullRange == sourcePixelFormat) {
        error = libyuv::NV12ToI420(
            pixel,
            (int)rowSize,
            (const uint8 *)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1),
            (int)CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1),
            [convertedI420Frame dataOfPlane:NTESI420FramePlaneY],
            (int)[convertedI420Frame strideOfPlane:NTESI420FramePlaneY],
            [convertedI420Frame dataOfPlane:NTESI420FramePlaneU],
            (int)[convertedI420Frame strideOfPlane:NTESI420FramePlaneU],
            [convertedI420Frame dataOfPlane:NTESI420FramePlaneV],
            (int)[convertedI420Frame strideOfPlane:NTESI420FramePlaneV],
            (int)bufferWidth,
            (int)bufferHeight);
    }

    if (error) {
        CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
        NSLog(@"error convert pixel buffer to i420 with error %d", error);
        return nil;
    } else {
        rowSize = [convertedI420Frame strideOfPlane:NTESI420FramePlaneY];
        pixel = convertedI420Frame.data;
    }

    CMVideoDimensions inputDimens = { (int32_t)bufferWidth, (int32_t)bufferHeight };
    CMVideoDimensions outputDimens = [NTESVideoUtil outputVideoDimensEnhanced:inputDimens crop:cropRatio];
//        CMVideoDimensions outputDimens = {(int32_t)738,(int32_t)1312};
    CMVideoDimensions sizeDimens = { (int32_t)size.width, (int32_t)size.height };
    CMVideoDimensions targetDimens = [NTESVideoUtil outputVideoDimensEnhanced:sizeDimens crop:cropRatio];
    int cropX = (inputDimens.width - outputDimens.width) / 2;
    int cropY = (inputDimens.height - outputDimens.height) / 2;

    if (cropX % 2) {
        cropX += 1;
    }

    if (cropY % 2) {
        cropY += 1;
    }
    float scale = targetDimens.width * 1.0 / outputDimens.width;

    NTESI420Frame *croppedI420Frame = [[NTESI420Frame alloc] initWithWidth:outputDimens.width height:outputDimens.height];

    error = libyuv::ConvertToI420(pixel, bufferHeight * rowSize * 1.5,
                                  [croppedI420Frame dataOfPlane:NTESI420FramePlaneY], (int)[croppedI420Frame strideOfPlane:NTESI420FramePlaneY],
                                  [croppedI420Frame dataOfPlane:NTESI420FramePlaneU], (int)[croppedI420Frame strideOfPlane:NTESI420FramePlaneU],
                                  [croppedI420Frame dataOfPlane:NTESI420FramePlaneV], (int)[croppedI420Frame strideOfPlane:NTESI420FramePlaneV],
                                  cropX, cropY,
                                  (int)bufferWidth, (int)bufferHeight,
                                  croppedI420Frame.width, croppedI420Frame.height,
                                  libyuv::kRotate0, libyuv::FOURCC_I420);

    if (error) {
        CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
        NSLog(@"error convert pixel buffer to i420 with error %d", error);
        return nil;
    }

    NTESI420Frame *i420Frame;

    if (scale == 1.0) {
        i420Frame = croppedI420Frame;
    } else {
        int width = outputDimens.width * scale;
        width &= 0xFFFFFFFE;
        int height = outputDimens.height * scale;
        height &= 0xFFFFFFFE;

        i420Frame = [[NTESI420Frame alloc] initWithWidth:width height:height];

        libyuv::I420Scale([croppedI420Frame dataOfPlane:NTESI420FramePlaneY], (int)[croppedI420Frame strideOfPlane:NTESI420FramePlaneY],
                          [croppedI420Frame dataOfPlane:NTESI420FramePlaneU], (int)[croppedI420Frame strideOfPlane:NTESI420FramePlaneU],
                          [croppedI420Frame dataOfPlane:NTESI420FramePlaneV], (int)[croppedI420Frame strideOfPlane:NTESI420FramePlaneV],
                          croppedI420Frame.width, croppedI420Frame.height,
                          [i420Frame dataOfPlane:NTESI420FramePlaneY], (int)[i420Frame strideOfPlane:NTESI420FramePlaneY],
                          [i420Frame dataOfPlane:NTESI420FramePlaneU], (int)[i420Frame strideOfPlane:NTESI420FramePlaneU],
                          [i420Frame dataOfPlane:NTESI420FramePlaneV], (int)[i420Frame strideOfPlane:NTESI420FramePlaneV],
                          i420Frame.width, i420Frame.height,
                          libyuv::kFilterBilinear);
    }

    int dstWidth, dstHeight;
    libyuv::RotationModeEnum rotateMode = [NTESYUVConverter rotateMode:orientation];

    if (rotateMode != libyuv::kRotateNone) {
        if (rotateMode == libyuv::kRotate270 || rotateMode == libyuv::kRotate90) {
            dstWidth = i420Frame.height;
            dstHeight = i420Frame.width;
        } else {
            dstWidth = i420Frame.width;
            dstHeight = i420Frame.height;
        }
        NTESI420Frame *rotatedI420Frame = [[NTESI420Frame alloc]initWithWidth:dstWidth height:dstHeight];

        libyuv::I420Rotate([i420Frame dataOfPlane:NTESI420FramePlaneY], (int)[i420Frame strideOfPlane:NTESI420FramePlaneY],
                           [i420Frame dataOfPlane:NTESI420FramePlaneU], (int)[i420Frame strideOfPlane:NTESI420FramePlaneU],
                           [i420Frame dataOfPlane:NTESI420FramePlaneV], (int)[i420Frame strideOfPlane:NTESI420FramePlaneV],
                           [rotatedI420Frame dataOfPlane:NTESI420FramePlaneY], (int)[rotatedI420Frame strideOfPlane:NTESI420FramePlaneY],
                           [rotatedI420Frame dataOfPlane:NTESI420FramePlaneU], (int)[rotatedI420Frame strideOfPlane:NTESI420FramePlaneU],
                           [rotatedI420Frame dataOfPlane:NTESI420FramePlaneV], (int)[rotatedI420Frame strideOfPlane:NTESI420FramePlaneV],
                           i420Frame.width, i420Frame.height,
                           rotateMode);
        i420Frame = rotatedI420Frame;
    }

    CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
    return i420Frame;
}

这个函数中,主要进行了针对原始图像信息的YUV解码,解码后再进行裁剪,压缩,旋转。

从代码量就能看出来,此函数对于我们来说很有很多冗余,我们的目的是尽可能减少子进程中的任何处理,以及内存使用,所以,我们只保留其解码功能,其他剔除,如下:

+ (NTESI420Frame *)pixelBufferToI420:(CVImageBufferRef)pixelBuffer {
    if (pixelBuffer == NULL) {
        return nil;
    }

    CVPixelBufferLockBaseAddress(pixelBuffer, 0);

    OSType sourcePixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer);

    size_t bufferWidth = 0;
    size_t bufferHeight = 0;
    size_t rowSize = 0;
    uint8_t *pixel = NULL;

    if (CVPixelBufferIsPlanar(pixelBuffer)) {
        int basePlane = 0;
        pixel = (uint8_t *)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, basePlane);
        bufferHeight = CVPixelBufferGetHeightOfPlane(pixelBuffer, basePlane);
        bufferWidth = CVPixelBufferGetWidthOfPlane(pixelBuffer, basePlane);
        rowSize = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, basePlane);
    } else {
        pixel = (uint8_t *)CVPixelBufferGetBaseAddress(pixelBuffer);
        bufferWidth = CVPixelBufferGetWidth(pixelBuffer);
        bufferHeight = CVPixelBufferGetHeight(pixelBuffer);
        rowSize = CVPixelBufferGetBytesPerRow(pixelBuffer);
    }
    NTESI420Frame *convertedI420Frame = [[NTESI420Frame alloc] initWithWidth:(int)bufferWidth height:(int)bufferHeight];

    int error = -1;
    if (kCVPixelFormatType_32BGRA == sourcePixelFormat) {
        error = libyuv::ARGBToI420(
            pixel, (int)rowSize,
            [convertedI420Frame dataOfPlane:NTESI420FramePlaneY], (int)[convertedI420Frame strideOfPlane:NTESI420FramePlaneY],
            [convertedI420Frame dataOfPlane:NTESI420FramePlaneU], (int)[convertedI420Frame strideOfPlane:NTESI420FramePlaneU],
            [convertedI420Frame dataOfPlane:NTESI420FramePlaneV], (int)[convertedI420Frame strideOfPlane:NTESI420FramePlaneV],
            (int)bufferWidth, (int)bufferHeight);
    } else if (kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange == sourcePixelFormat || kCVPixelFormatType_420YpCbCr8BiPlanarFullRange == sourcePixelFormat) {
        error = libyuv::NV12ToI420(
            pixel,
            (int)rowSize,
            (const uint8 *)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1),
            (int)CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1),
            [convertedI420Frame dataOfPlane:NTESI420FramePlaneY],
            (int)[convertedI420Frame strideOfPlane:NTESI420FramePlaneY],
            [convertedI420Frame dataOfPlane:NTESI420FramePlaneU],
            (int)[convertedI420Frame strideOfPlane:NTESI420FramePlaneU],
            [convertedI420Frame dataOfPlane:NTESI420FramePlaneV],
            (int)[convertedI420Frame strideOfPlane:NTESI420FramePlaneV],
            (int)bufferWidth,
            (int)bufferHeight);
    }

    if (error) {
        CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
        NSLog(@"error convert pixel buffer to i420 with error %d", error);
        return nil;
    }
    CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
    return convertedI420Frame;
}

我们现在有了中间的数据载体,接下来就要考虑如何进行传输。

进行Socket通信之前,我们需要对上面获取到的数据结构进行二进制转换,网易的源码如下:

//NTESI420Frame.m

- (NSData *)bytes {
    int structSize = sizeof(self.width) + sizeof(self.height) + sizeof(self.i420DataLength) + sizeof(self.timetag);

    void *buffer = malloc(structSize + self.i420DataLength);

    memset(buffer, 0, structSize + self.i420DataLength);
    int offset = 0;

    memcpy(buffer + offset, &_width, sizeof(_width));
    offset += sizeof(_width);

    memcpy(buffer + offset, &_height, sizeof(_height));
    offset += sizeof(_height);

    memcpy(buffer + offset, &_i420DataLength, sizeof(_i420DataLength));
    offset += sizeof(_i420DataLength);

    memcpy(buffer + offset, &_timetag, sizeof(_timetag));
    offset += sizeof(_timetag);

    memcpy(buffer + offset, [self dataOfPlane:NTESI420FramePlaneY], [self strideOfPlane:NTESI420FramePlaneY] * self.height);
    offset += [self strideOfPlane:NTESI420FramePlaneY] * self.height;
    
    memcpy(buffer + offset, [self dataOfPlane:NTESI420FramePlaneU], [self strideOfPlane:NTESI420FramePlaneU] * self.height / 2);
    offset += [self strideOfPlane:NTESI420FramePlaneU] * self.height / 2;

    memcpy(buffer + offset, [self dataOfPlane:NTESI420FramePlaneV], [self strideOfPlane:NTESI420FramePlaneV] * self.height / 2);
    offset += [self strideOfPlane:NTESI420FramePlaneV] * self.height / 2;
    NSData *data = [NSData dataWithBytes:buffer length:offset];
    free(buffer);
    return data;
}

从函数本身来看,没有任何问题,将数据结构中包含的所有信息打包成一个NSData二进制流,最后组成Socket的一帧进行发送就好。

但是,不要忘记,这些操作我们都是在子进程中进行的,在分辨率过高尺寸过大的设备上,一旦图片中信息本身就很丰富的情况下,CPU来不及处理释放这些临时变量时,依然很容易导致内存暴增,最终超过50M,导致录屏进程崩溃。

就像一条河,水量过大,流速太慢,河道本身太窄,都会导致河堤的崩溃。

因此,我们的处理方向可以集中在以下三点:

  1. 减少水流

    a. 利用NTESI420Frame来承载图片信息,而不是位图本身的二进制流信息

    b. 减少临时变量的使用

    c. 拆分数据,大数据拆开成小数据进行处理

  2. 加快流速

    a. 加快子进程中处理信息速度,这一条是在“减少水流”的基础上,数据越小,处理越快

    b. 加快进程通信间的传输速度。使用本地Socket,而不是CFNotificationCenterRef。

    c. 多任务处理数据

    d. 多通道传输数据

  3. 扩宽河道

    由于系统限制50M,我们针对此条无法做处理。

基于以上,我们针对NTESI420Framebyte方法进行了以下优化:

- (void)getBytesQueue:(void (^)(NSData *data,NSInteger index))complete {
    int offset = 0;
    {
        int structSize = sizeof(self.width) + sizeof(self.height) + sizeof(self.i420DataLength) + sizeof(self.timetag);

        void *buffer = malloc(structSize + self.i420DataLength);

        memset(buffer, 0, structSize + self.i420DataLength);

        memcpy(buffer + offset, &_width, sizeof(_width));
        offset += sizeof(_width);

        memcpy(buffer + offset, &_height, sizeof(_height));
        offset += sizeof(_height);

        memcpy(buffer + offset, &_i420DataLength, sizeof(_i420DataLength));
        offset += sizeof(_i420DataLength);

        memcpy(buffer + offset, &_timetag, sizeof(_timetag));
        offset += sizeof(_timetag);
        NSData *data = [NSData dataWithBytes:buffer length:offset];
        if (complete) {
            complete(data,0);
        }
        free(buffer);
        data = NULL;
    }
    
    {
        void *buffer = malloc([self strideOfPlane:NTESI420FramePlaneY] * self.height);
        offset = 0;
        memset(buffer, 0, [self strideOfPlane:NTESI420FramePlaneY] * self.height);
        memcpy(buffer + offset, [self dataOfPlane:NTESI420FramePlaneY], [self strideOfPlane:NTESI420FramePlaneY] * self.height);
        offset += [self strideOfPlane:NTESI420FramePlaneY] * self.height;
        NSData *data = [NSData dataWithBytes:buffer length:offset];
        if (complete) {
            complete(data,0);
        }
        free(buffer);
        data = NULL;
    }
    
    {
        void *buffer = malloc([self strideOfPlane:NTESI420FramePlaneU] * self.height / 2);
        offset = 0;
        memset(buffer, 0, [self strideOfPlane:NTESI420FramePlaneU] * self.height / 2);
        memcpy(buffer + offset, [self dataOfPlane:NTESI420FramePlaneU], [self strideOfPlane:NTESI420FramePlaneU] * self.height / 2);
        offset += [self strideOfPlane:NTESI420FramePlaneU] * self.height / 2;
        NSData *data = [NSData dataWithBytes:buffer length:offset];
        if (complete) {
            complete(data,1);
        }
        free(buffer);
        data = NULL;
    }
    
    {
        void *buffer = malloc([self strideOfPlane:NTESI420FramePlaneV] * self.height / 2);
        offset = 0;
        memset(buffer, 0, [self strideOfPlane:NTESI420FramePlaneV] * self.height / 2);
        memcpy(buffer + offset, [self dataOfPlane:NTESI420FramePlaneV], [self strideOfPlane:NTESI420FramePlaneV] * self.height / 2);
        offset += [self strideOfPlane:NTESI420FramePlaneV] * self.height / 2;
        NSData *data = [NSData dataWithBytes:buffer length:offset];
        if (complete) {
            complete(data,2);
        }
        free(buffer);
        data = NULL;
    }
}

此函数在之前基础上,将一大块数据拆成了四份:

  1. 图片头部信息
  2. Y通道信息
  3. U通道信息
  4. V通道信息

转换一条数据,就发送一条,减少数据量,提高数据处理速度,尽快释放临时变量,保持内存值一直处于一个平均水平。


此时,数据已经准备好,接下来,在Socket传输中,我们如何组织它们呢?

由于以上的操作,我们将一张图片分成了四部分:

  1. 图片头部信息
  2. Y通道信息
  3. U通道信息
  4. V通道信息

我们从子进程中分别发送每一条数据到主进程,等到主进程收到一个完成图片信息时再进行后续处理。

虽然是分开发送的,但是我们需要将这四部分数据,在Socket传输中,组成一个完整的帧,这样子,主进程才能知道它得到了一张完整的图片信息。

因此,我们将这四部分数据分别发送之后,最后向子程序发送一个类似HTTP header的数据帧,告诉主进程一张图片信息结束。

- (void)sendVideoBufferToHostApp:(CMSampleBufferRef)sampleBuffer {
    if (!self.socket) {
        return;
    }
    if (self.frameCount > 0) {
        //每次只处理1帧画面
        return;
    }
    long curMem = [self getCurUsedMemory];
    NSLog(@"curMem:%@", @(curMem / 1024.0 / 1024.0));
    if (evenlyMem > 0
        && ((curMem - evenlyMem) > (5 * 1024 * 1024)
            || curMem > 45 * 1024 * 1024)) {
        //当前内存暴增5M以上,或者总共超过45M,则不处理
        return;
    }
    self.frameCount++;

    CFRetain(sampleBuffer);
    dispatch_async(self.videoQueue, ^{ // queue optimal
        @autoreleasepool {
            // To data
            NTESI420Frame *videoFrame = [NTESYUVConverter pixelBufferToI420:CMSampleBufferGetImageBuffer(sampleBuffer)];
            CFRelease(sampleBuffer);

            // To Host App
            if (videoFrame) {
                __block NSUInteger length = 0;
                [videoFrame getBytesQueue:^(NSData *data, NSInteger index) {
                        length += data.length;
                        [self.socket writeData:data withTimeout:5 tag:0];
                }];
                @autoreleasepool {
                    NSData *headerData = [NTESSocketPacket packetWithBufferLength:length];
                    [self.socket writeData:headerData withTimeout:5 tag:0];
                }
            }
        };
        if (self->evenlyMem <= 0) {
            self->evenlyMem = [self getCurUsedMemory];
            NSLog(@"平均内存:%@", @(self->evenlyMem));
        }
        self.frameCount--;
    });
}

以上,基本解决了50M的系统限制问题。

测试机型:

  • iPhone 5s
  • iPhone 6s Plus
  • iPhone 7
  • iPad mini4
  • iPad Air2

尽量使用图像复杂,变化快的方式进行暴力测试。

总结来看,依然是那条河的问题,映射到我们的计算机世界来看,就是处理数据过大,CPU处理不过来,内存释放不及时。

源码Demo可以参考:https://github.com/yifriday/ReplayKitDemo

Let's think!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342