|
4 | 4 | #import <FlutterMacOS/FlutterMacOS.h>
|
5 | 5 | #endif
|
6 | 6 |
|
7 |
| - |
8 | 7 | #import "FlutterRTCFrameCapturer.h"
|
9 | 8 |
|
10 |
| -#define clamp(a) (a>255?255:(a<0?0:a)) |
11 |
| - |
12 | 9 | @import CoreImage;
|
13 | 10 | @import CoreVideo;
|
14 | 11 |
|
@@ -36,99 +33,148 @@ - (void)setSize:(CGSize)size
|
36 | 33 | {
|
37 | 34 | }
|
38 | 35 |
|
39 |
| -#if TARGET_OS_IPHONE |
40 |
| -// Thanks Juan Giorello https://groups.google.com/g/discuss-webrtc/c/ULGIodbbLvM |
41 |
| -+ (UIImage *)convertFrameToUIImage:(RTCVideoFrame *)frame { |
42 |
| - // https://chromium.googlesource.com/external/webrtc/+/refs/heads/main/sdk/objc/base/RTCVideoFrame.h |
43 |
| - // RTCVideoFrameBuffer *rtcFrameBuffer = (RTCVideoFrameBuffer *)frame.buffer; |
44 |
| - // RTCI420Buffer *buffer = [rtcFrameBuffer toI420]; |
45 |
| - |
46 |
| - // https://chromium.googlesource.com/external/webrtc/+/refs/heads/main/sdk/objc/base/RTCVideoFrameBuffer.h |
47 |
| - // This guarantees the buffer will be RTCI420Buffer |
48 |
| - RTCI420Buffer *buffer = [frame.buffer toI420]; |
49 |
| - |
50 |
| - |
51 |
| - int width = buffer.width; |
52 |
| - int height = buffer.height; |
53 |
| - int bytesPerPixel = 4; |
54 |
| - uint8_t *rgbBuffer = malloc(width * height * bytesPerPixel);
8000
div> |
55 |
| - |
56 |
| - for(int row = 0; row < height; row++) { |
57 |
| - const uint8_t *yLine = &buffer.dataY[row * buffer.strideY]; |
58 |
| - const uint8_t *uLine = &buffer.dataU[(row >> 1) * buffer.strideU]; |
59 |
| - const uint8_t *vLine = &buffer.dataV[(row >> 1) * buffer.strideV]; |
60 |
| - |
61 |
| - for(int x = 0; x < width; x++) { |
62 |
| - int16_t y = yLine[x]; |
63 |
| - int16_t u = uLine[x >> 1] - 128; |
64 |
| - int16_t v = vLine[x >> 1] - 128; |
65 |
| - |
66 |
| - int16_t r = roundf(y + v * 1.4); |
67 |
| - int16_t g = roundf(y + u * -0.343 + v * -0.711); |
68 |
| - int16_t b = roundf(y + u * 1.765); |
69 |
| - |
70 |
| - uint8_t *rgb = &rgbBuffer[(row * width + x) * bytesPerPixel]; |
71 |
| - rgb[0] = 0xff; |
72 |
| - rgb[1] = clamp(b); |
73 |
| - rgb[2] = clamp(g); |
74 |
| - rgb[3] = clamp(r); |
| 36 | +- (void)renderFrame:(nullable RTCVideoFrame *)frame |
| 37 | +{
8000
|
| 38 | + if (_gotFrame || frame == nil) |
| 39 | + return; |
| 40 | + _gotFrame = true; |
| 41 | + id <RTCVideoFrameBuffer> buffer = frame.buffer; |
| 42 | + CVPixelBufferRef pixelBufferRef; |
| 43 | + bool shouldRelease; |
| 44 | + if (![buffer isKindOfClass:[RTCCVPixelBuffer class]]) { |
| 45 | + pixelBufferRef = [self convertToCVPixelBuffer:frame]; |
| 46 | + shouldRelease = true; |
| 47 | + } else { |
| 48 | + pixelBufferRef = ((RTCCVPixelBuffer *) buffer).pixelBuffer; |
| 49 | + shouldRelease = false; |
| 50 | + } |
| 51 | + CIImage *ciImage = [CIImage imageWithCVPixelBuffer:pixelBufferRef]; |
| 52 | + CGRect outputSize; |
| 53 | + if (@available(iOS 11, macOS 10.13, *)) { |
| 54 | + switch (frame.rotation) { |
| 55 | + case RTCVideoRotation_90: |
| 56 | + ciImage = [ciImage imageByApplyingCGOrientation:kCGImagePropertyOrientationRight]; |
| 57 | + outputSize = CGRectMake(0, 0, frame.height, frame.width); |
| 58 | + break; |
| 59 | + case RTCVideoRotation_180: |
| 60 | + ciImage = [ciImage imageByApplyingCGOrientation:kCGImagePropertyOrientationDown]; |
| 61 | + outputSize = CGRectMake(0, 0, frame.width, frame.height); |
| 62 | + break; |
| 63 | + case RTCVideoRotation_270: |
| 64 | + ciImage = [ciImage imageByApplyingCGOrientation:kCGImagePropertyOrientationLeft]; |
| 65 | + outputSize = CGRectMake(0, 0, frame.height, frame.width); |
| 66 | + break; |
| 67 | + default: |
| 68 | + outputSize = CGRectMake(0, 0, frame.width, frame.height); |
| 69 | + break; |
75 | 70 | }
|
| 71 | + } else { |
| 72 | + outputSize = CGRectMake(0, 0, frame.width, frame.height); |
76 | 73 | }
|
77 |
| - |
78 |
| - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); |
79 |
| - CGContextRef context = CGBitmapContextCreate(rgbBuffer, width, height, 8, width * bytesPerPixel, colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipLast); |
80 |
| - |
81 |
| - CGImageRef cgImage = CGBitmapContextCreateImage(context); |
82 |
| - CGContextRelease(context); |
83 |
| - CGColorSpaceRelease(colorSpace); |
84 |
| - free(rgbBuffer); |
85 |
| - |
86 |
| - UIImageOrientation orientation; |
87 |
| - switch (frame.rotation) { |
88 |
| - case RTCVideoRotation_90: |
89 |
| - orientation = UIImageOrientationRight; |
90 |
| - break; |
91 |
| - case RTCVideoRotation_180: |
92 |
| - orientation = UIImageOrientationDown; |
93 |
| - break; |
94 |
| - case RTCVideoRotation_270: |
95 |
| - orientation = UIImageOrientationLeft; |
96 |
| - default: |
97 |
| - orientation = UIImageOrientationUp; |
98 |
| - break; |
| 74 | + CIContext *tempContext = [CIContext contextWithOptions:nil]; |
| 75 | + CGImageRef cgImage = [tempContext |
| 76 | + createCGImage:ciImage |
| 77 | + fromRect:outputSize]; |
| 78 | + NSData *imageData; |
| 79 | + #if TARGET_OS_IPHONE |
| 80 | + UIImage *uiImage = [UIImage imageWithCGImage:cgImage]; |
| 81 | + if ([[_path pathExtension] isEqualToString:@"jpg"]) { |
| 82 | + imageData = UIImageJPEGRepresentation(uiImage, 1.0f); |
| 83 | + } else { |
| 84 | + imageData = UIImagePNGRepresentation(uiImage); |
99 | 85 | }
|
100 |
| - |
101 |
| - UIImage *image = [UIImage imageWithCGImage:cgImage scale:1 orientation:orientation]; |
| 86 | + #else |
| 87 | + NSBitmapImageRep *newRep = [[NSBitmapImageRep alloc] initWithCGImage:cgImage]; |
| 88 | + [newRep setSize:NSSizeToCGSize(outputSize.size)]; |
| 89 | + NSDictionary<NSBitmapImageRepPropertyKey, id>* quality = @{ |
| 90 | + NSImageCompressionFactor: @1.0f |
| 91 | + }; |
| 92 | + if ([[_path pathExtension] isEqualToString:@"jpg"]) { |
| 93 | + imageData = [newRep representationUsingType:NSJPEGFileType properties:quality]; |
| 94 | + } else { |
| 95 | + imageData = [newRep representationUsingType:NSPNGFileType properties:quality]; |
| 96 | + } |
| 97 | + #endif |
102 | 98 | CGImageRelease(cgImage);
|
103 |
| - |
104 |
| - return image; |
105 |
| -} |
106 |
| -#endif |
107 |
| - |
108 |
| -- (void)renderFrame:(nullable RTCVideoFrame *)frame |
109 |
| -{ |
110 |
| -#if TARGET_OS_IPHONE |
111 |
| - if (_gotFrame || frame == nil) return; |
112 |
| - _gotFrame = true; |
113 |
| - |
114 |
| - UIImage *uiImage = [FlutterRTCFrameCapturer convertFrameToUIImage:frame]; |
115 |
| - NSData *jpgData = UIImageJPEGRepresentation(uiImage, 0.9f); |
116 |
| - |
117 |
| - if ([jpgData writeToFile:_path atomically:NO]) { |
| 99 | + if (shouldRelease) |
| 100 | + CVPixelBufferRelease(pixelBufferRef); |
| 101 | + if (imageData && [imageData writeToFile:_path atomically:NO]) { |
118 | 102 | NSLog(@"File writed successfully to %@", _path);
|
119 | 103 | _result(nil);
|
120 | 104 | } else {
|
121 | 105 | NSLog(@"Failed to write to file");
|
122 | 106 | _result([FlutterError errorWithCode:@"CaptureFrameFailed"
|
123 |
| - message:@"Failed to write JPEG data to file" |
| 107 | + message:@"Failed to write image data to file" |
124 | 108 | details:nil]);
|
125 | 109 | }
|
126 |
| - |
127 | 110 | dispatch_async(dispatch_get_main_queue(), ^{
|
128 | 111 | [self->_track removeRenderer:self];
|
129 | 112 | self->_track = nil;
|
130 | 113 | });
|
131 |
| -#endif |
| 114 | +} |
| 115 | + |
| 116 | +-(CVPixelBufferRef)convertToCVPixelBuffer:(RTCVideoFrame *) frame |
| 117 | +{ |
| 118 | + id<RTCI420Buffer> i420Buffer = [frame.buffer toI420]; |
| 119 | + CVPixelBufferRef outputPixelBuffer; |
| 120 | + size_t w = (size_t) roundf(i420Buffer.width); |
| 121 | + size_t h = (size_t) roundf(i420Buffer.height); |
| 122 | + NSDictionary *pixelAttributes = @{(id)kCVPixelBufferIOSurfacePropertiesKey : @{}}; |
| 123 | + CVPixelBufferCreate(kCFAllocatorDefault, w, h, kCVPixelFormatType_32BGRA, (__bridge CFDictionaryRef)(pixelAttributes), &outputPixelBuffer); |
| 124 | + CVPixelBufferLockBaseAddress(outputPixelBuffer, 0); |
| 125 | + const OSType pixelFormat = CVPixelBufferGetPixelFormatType(outputPixelBuffer); |
| 126 | + if (pixelFormat == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange || |
| 127 | + pixelFormat == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) { |
| 128 | + // NV12 |
| 129 | + uint8_t* dstY = CVPixelBufferGetBaseAddressOfPlane(outputPixelBuffer, 0); |
| 130 | + const size_t dstYStride = CVPixelBufferGetBytesPerRowOfPlane(outputPixelBuffer, 0); |
| 131 | + uint8_t* dstUV = CVPixelBufferGetBaseAddressOfPlane(outputPixelBuffer, 1); |
| 132 | + const size_t dstUVStride = CVPixelBufferGetBytesPerRowOfPlane(outputPixelBuffer, 1); |
| 133 | + |
| 134 | + [RTCYUVHelper I420ToNV12:i420Buffer.dataY |
| 135 | + srcStrideY:i420Buffer.strideY |
| 136 | + srcU:i420Buffer.dataU |
| 137 | + srcStrideU:i420Buffer.strideU |
| 138 | + srcV:i420Buffer.dataV |
| 139 | + srcStrideV:i420Buffer.strideV |
| 140 | + dstY:dstY |
| 141 | + dstStrideY:(int)dstYStride |
| 142 | + dstUV:dstUV |
| 143 | + dstStrideUV:(int)dstUVStride |
| 144 | + width:i420Buffer.width |
| 145 | + width:i420Buffer.height]; |
| 146 | + } else { |
| 147 | + uint8_t* dst = CVPixelBufferGetBaseAddress(outputPixelBuffer); |
| 148 | + const size_t bytesPerRow = CVPixelBufferGetBytesPerRow(outputPixelBuffer); |
| 149 | + |
| 150 | + if (pixelFormat == kCVPixelFormatType_32BGRA) { |
| 151 | + // Corresponds to libyuv::FOURCC_ARGB |
| 152 | + [RTCYUVHelper I420ToARGB:i420Buffer.dataY |
| 153 | + srcStrideY:i420Buffer.strideY |
| 154 | + srcU:i420Buffer.dataU |
| 155 | + srcStrideU:i420Buffer.strideU |
| 156 | + srcV:i420Buffer.dataV |
| 157 | + srcStrideV:i420Buffer.strideV |
| 158 | + dstARGB:dst |
| 159 | + dstStrideARGB:(int)bytesPerRow |
| 160 | + width:i420Buffer.width |
| 161 | + height:i420Buffer.height]; |
| 162 | + } else if (pixelFormat == kCVPixelFormatType_32ARGB) { |
| 163 | + // Corresponds to libyuv::FOURCC_BGRA |
| 164 | + [RTCYUVHelper I420ToBGRA:i420Buffer.dataY |
| 165 | + srcStrideY:i420Buffer.strideY |
| 166 | + srcU:i420Buffer.dataU |
| 167 | + srcStrideU:i420Buffer.strideU |
| 168 | + srcV:i420Buffer.dataV |
| 169 | + srcStrideV:i420Buffer.strideV |
| 170 | + dstBGRA:dst |
| 171 | + dstStrideBGRA:(int)bytesPerRow |
| 172 | + width:i420Buffer.width |
| 173 | + height:i420Buffer.height]; |
| 174 | + } |
| 175 | + } |
| 176 | + CVPixelBufferUnlockBaseAddress(outputPixelBuffer, 0); |
| 177 | + return outputPixelBuffer; |
132 | 178 | }
|
133 | 179 |
|
134 | 180 | @end
|
0 commit comments