|
6 | 6 | //
|
7 | 7 |
|
8 | 8 | #include "RNFAppleFilamentRecorder.h"
|
| 9 | +#include <CoreFoundation/CoreFoundation.h> |
| 10 | +#include <CoreVideo/CoreVideo.h> |
9 | 11 | #include <VideoToolbox/VTCompressionProperties.h>
|
10 | 12 | #include <memory>
|
11 | 13 | #include <mutex>
|
12 | 14 |
|
13 | 15 | namespace margelo {
|
14 | 16 |
|
| 17 | +static int kCVPixelBufferLock_Write = 0; |
| 18 | + |
15 | 19 | AppleFilamentRecorder::AppleFilamentRecorder(std::shared_ptr<Dispatcher> renderThreadDispatcher, int width, int height, int fps,
|
16 | 20 | double bitRate)
|
17 | 21 | : FilamentRecorder(renderThreadDispatcher, width, height, fps, bitRate) {
|
18 | 22 | dispatch_queue_attr_t qos = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, -1);
|
19 | 23 | _queue = dispatch_queue_create("filament.recorder.queue", qos);
|
20 | 24 |
|
21 |
| - Logger::log(TAG, "Creating CVPixelBufferPool..."); |
22 |
| - int maxBufferCount = 30; |
23 |
| - NSDictionary* poolAttributes = @{(NSString*)kCVPixelBufferPoolMinimumBufferCountKey : @(maxBufferCount)}; |
| 25 | + Logger::log(TAG, "Creating CVPixelBuffer target texture..."); |
24 | 26 | NSDictionary* pixelBufferAttributes = @{
|
25 | 27 | (NSString*)kCVPixelBufferWidthKey : @(width),
|
26 | 28 | (NSString*)kCVPixelBufferHeightKey : @(height),
|
27 | 29 | (NSString*)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA),
|
28 | 30 | (NSString*)kCVPixelBufferMetalCompatibilityKey : @(YES)
|
29 | 31 | };
|
30 |
| - CVReturn result = CVPixelBufferPoolCreate(kCFAllocatorDefault, (__bridge CFDictionaryRef)poolAttributes, |
31 |
| - (__bridge CFDictionaryRef)pixelBufferAttributes, &_pixelBufferPool); |
32 |
| - if (result != kCVReturnSuccess) { |
33 |
| - throw std::runtime_error("Failed to create " + std::to_string(width) + "x" + std::to_string(height) + |
34 |
| - " CVPixelBufferPool! Status: " + std::to_string(result)); |
35 |
| - } |
36 |
| - |
37 |
| - Logger::log(TAG, "Creating CVPixelBuffer target texture..."); |
38 |
| - result = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, _pixelBufferPool, &_pixelBuffer); |
| 32 | + CVReturn result = |
| 33 | + CVPixelBufferCreate(nil, width, height, kCVPixelFormatType_32BGRA, (__bridge CFDictionaryRef)pixelBufferAttributes, &_pixelBuffer); |
39 | 34 | if (result != kCVReturnSuccess) {
|
40 |
| - throw std::runtime_error("Failed to create " + std::to_string(width) + "x" + std::to_string(height) + |
41 |
| - " CVPixelBuffer texture! Status: " + std::to_string(result)); |
| 35 | + throw std::runtime_error("Failed to create input texture CVPixelBuffer!"); |
42 | 36 | }
|
43 | 37 |
|
44 | 38 | Logger::log(TAG, "Creating temporary file...");
|
|
74 | 68 | AVVideoHeightKey : @(height)
|
75 | 69 | };
|
76 | 70 | _assetWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:outputSettings];
|
| 71 | + _assetWriterInput.expectsMediaDataInRealTime = NO; |
| 72 | + _assetWriterInput.performsMultiPassEncodingIfSupported = YES; |
77 | 73 | if (![_assetWriter canAddInput:_assetWriterInput]) {
|
78 | 74 | std::string settingsJson = outputSettings.description.UTF8String;
|
79 | 75 | throw std::runtime_error("Failed to add AVAssetWriterInput to AVAssetWriter! Settings used: " + settingsJson);
|
80 | 76 | }
|
81 | 77 |
|
82 |
| - _assetWriterInput.expectsMediaDataInRealTime = NO; |
83 |
| - _assetWriterInput.performsMultiPassEncodingIfSupported = YES; |
84 |
| - |
85 |
| - _pixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:_assetWriterInput |
86 |
| - sourcePixelBufferAttributes:nil]; |
87 |
| - |
88 | 78 | Logger::log(TAG, "Adding AVAssetWriterInput...");
|
89 | 79 | [_assetWriter addInput:_assetWriterInput];
|
| 80 | + |
| 81 | + Logger::log(TAG, "Creating AVAssetWriterInputPixelBufferAdaptor..."); |
| 82 | + _pixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:_assetWriterInput |
| 83 | + sourcePixelBufferAttributes:pixelBufferAttributes]; |
90 | 84 | }
|
91 | 85 |
|
92 | 86 | bool AppleFilamentRecorder::getSupportsHEVC() {
|
|
106 | 100 |
|
107 | 101 | Logger::log(TAG, "Rendering Frame with timestamp %f...", timestamp);
|
108 | 102 | if (!_assetWriterInput.isReadyForMoreMediaData) {
|
109 |
| - // TODO: Dropping this frame is probably not a good idea, as we are rendering from an offscreen context anyways |
110 |
| - // and could just wait until the input is ready for more data again. Maybe we can implement a mechanism |
111 |
| - // that only renders when isReadyForMoreMediaData turns true? |
| 103 | + // This should never happen because we only poll Frames from the AVAssetWriter. |
| 104 | + // Once it's ready, renderFrame will be called. But better safe than sorry. |
112 | 105 | throw std::runtime_error("AVAssetWriterInput was not ready for more data!");
|
113 | 106 | }
|
114 | 107 |
|
115 |
| - CVPixelBufferRef targetBuffer; |
116 |
| - CVReturn result = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, _pixelBufferPool, &targetBuffer); |
117 |
| - if (result != kCVReturnSuccess) { |
118 |
| - throw std::runtime_error("Failed to create CVPixelBuffer for writing! Status: " + std::to_string(result)); |
| 108 | + CVPixelBufferPoolRef pool = _pixelBufferAdaptor.pixelBufferPool; |
| 109 | + if (pool == nil) { |
| 110 | + // The pool should always be created once startSession has been called. So in theory that also shouldn't happen. |
| 111 | + throw std::runtime_error("AVAssetWriterInputPixelBufferAdaptor's pixel buffer pool was nil! Cannot write Frame."); |
119 | 112 | }
|
120 | 113 |
|
121 |
| - result = CVPixelBufferLockBaseAddress(targetBuffer, /* write flag */ 0); |
122 |
| - if (result != kCVReturnSuccess) { |
123 |
| - throw std::runtime_error("Failed to lock target buffer for write access!"); |
| 114 | + // 1. Get (or create) a pixel buffer from the cache pool |
| 115 | + CVPixelBufferRef targetBuffer; |
| 116 | + CVReturn result = CVPixelBufferPoolCreatePixelBuffer(nil, pool, &targetBuffer); |
| 117 | + if (result != kCVReturnSuccess || targetBuffer == nil) { |
| 118 | + throw std::runtime_error("Failed to get a new CVPixelBuffer from the CVPixelBufferPool!"); |
124 | 119 | }
|
| 120 | + |
| 121 | + // 2. Lock both pixel buffers for CPU access |
125 | 122 | result = CVPixelBufferLockBaseAddress(_pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
126 | 123 | if (result != kCVReturnSuccess) {
|
127 | 124 | throw std::runtime_error("Failed to lock input buffer for read access!");
|
128 | 125 | }
|
| 126 | + result = CVPixelBufferLockBaseAddress(targetBuffer, /* write flag */ 0); |
| 127 | + if (result != kCVReturnSuccess) { |
| 128 | + throw std::runtime_error("Failed to lock target buffer for write access!"); |
| 129 | + } |
129 | 130 |
|
| 131 | + // 3. Copy over Frame data |
130 | 132 | size_t bytesPerRow = CVPixelBufferGetBytesPerRow(_pixelBuffer);
|
131 | 133 | size_t height = CVPixelBufferGetHeight(_pixelBuffer);
|
132 |
| - |
133 | 134 | void* destination = CVPixelBufferGetBaseAddress(targetBuffer);
|
134 | 135 | void* source = CVPixelBufferGetBaseAddress(_pixelBuffer);
|
135 |
| - |
136 | 136 | memcpy(destination, source, bytesPerRow * height);
|
137 | 137 |
|
138 |
| - CVPixelBufferUnlockBaseAddress(targetBuffer, /* write flag */ 0); |
| 138 | + // 4. Unlock pixel buffers again |
| 139 | + CVPixelBufferUnlockBaseAddress(targetBuffer, kCVPixelBufferLock_Write); |
139 | 140 | CVPixelBufferUnlockBaseAddress(_pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
140 | 141 |
|
| 142 | + // 5. Append the new copy of the buffer to the pool |
141 | 143 | CMTime time = CMTimeMake(_frameCount++, getFps());
|
142 | 144 | BOOL success = [_pixelBufferAdaptor appendPixelBuffer:targetBuffer withPresentationTime:time];
|
143 | 145 | if (!success || _assetWriter.status != AVAssetWriterStatusWriting) {
|
|
148 | 150 | }
|
149 | 151 | throw std::runtime_error("Failed to append buffer to AVAssetWriter! " + errorMessage);
|
150 | 152 | }
|
| 153 | + |
| 154 | + // 6. Release the pixel buffer |
| 155 | + CFRelease(targetBuffer); |
151 | 156 | }
|
152 | 157 |
|
153 | 158 | void* AppleFilamentRecorder::getNativeWindow() {
|
|
187 | 192 | Logger::log(TAG, "Recorder is ready for more data.");
|
188 | 193 | auto self = weakSelf.lock();
|
189 | 194 | if (self != nullptr) {
|
190 |
| - self->_renderThreadDispatcher->runAsync([self]() { |
191 |
| - bool shouldContinueNext = self->onReadyForMoreData(); |
192 |
| - if (!shouldContinueNext) { |
193 |
| - // stop the render queue |
194 |
| - [self->_assetWriterInput markAsFinished]; |
195 |
| - } |
196 |
| - }); |
| 195 | + auto futurePromise = |
| 196 | + self->_renderThreadDispatcher->runAsyncAwaitable<void>([self]() { |
| 197 | + while ([self->_assetWriterInput isReadyForMoreMediaData]) { |
| 198 | + // This will cause our JS render callbacks to be called, which will call |
| 199 | + // renderFrame renderFrame will call appendPixelBuffer, and we should call |
| 200 | + // appendPixelBuffer as long as isReadyForMoreMediaData is true. |
| 201 | + bool shouldContinueNext = self->onReadyForMoreData(); |
| 202 | + if (!shouldContinueNext) { |
| 203 | + // stop the render queue |
| 204 | + [self->_assetWriterInput markAsFinished]; |
| 205 | + } |
| 206 | + } |
| 207 | + }); |
| 208 | + // The block in requestMediaDataWhenReadyOnQueue needs to call appendPixelBuffer |
| 209 | + // synchronously |
| 210 | + futurePromise.get(); |
197 | 211 | }
|
198 | 212 | }];
|
199 | 213 | });
|
|
235 | 249 | }];
|
236 | 250 |
|
237 | 251 | self->_isRecording = false;
|
238 |
| - CVPixelBufferPoolFlush(self->_pixelBufferPool, 0); |
239 | 252 | });
|
240 | 253 |
|
241 | 254 | return promise->get_future();
|
|
0 commit comments