FBControlCore/Utility/FBVideoStream.m (188 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "FBVideoStream.h" #import "FBCollectionInformation.h" #import "FBControlCoreError.h" #import "FBControlCoreLogger.h" #import "FBDataConsumer.h" static NSInteger const MaxAllowedUnprocessedDataCounts = 2; BOOL checkConsumerBufferLimit(id<FBDataConsumer> consumer, id<FBControlCoreLogger> logger) { if ([consumer conformsToProtocol:@protocol(FBDataConsumerAsync)]) { id<FBDataConsumerAsync> asyncConsumer = (id<FBDataConsumerAsync>)consumer; NSInteger framesInProcess = asyncConsumer.unprocessedDataCount; // drop frames if consumer is overflown if (framesInProcess > MaxAllowedUnprocessedDataCounts) { [logger logFormat:@"Consumer is overflown. Number of unsent frames: %@", @(framesInProcess)]; return NO; } } return YES; } static NSData *AnnexBNALUStartCodeData() { // https://www.programmersought.com/article/3901815022/ // Annex-B is simpler as it is purely based on a start code to denote the start of the NALU. static NSData *data; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ const uint8_t headerCode[] = {0x00, 0x00, 0x00, 0x01}; data = [NSData dataWithBytes:headerCode length:sizeof(headerCode)]; }); return data; } static const int AVCCHeaderLength = 4; BOOL WriteFrameToAnnexBStream(CMSampleBufferRef sampleBuffer, id<FBDataConsumer> consumer, id<FBControlCoreLogger> logger, NSError **error) { if (!CMSampleBufferDataIsReady(sampleBuffer)) { return [[FBControlCoreError describeFormat:@"Sample Buffer is not ready"] failBool:error]; } NSData *headerData = AnnexBNALUStartCodeData(); NSMutableData *consumableData = [NSMutableData alloc]; bool isKeyFrame = false; CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true); if (CFArrayGetCount(attachments)) { CFDictionaryRef attachment = (CFDictionaryRef)CFArrayGetValueAtIndex(attachments, 0); CFBooleanRef dependsOnOthers = (CFBooleanRef)CFDictionaryGetValue( attachment, kCMSampleAttachmentKey_DependsOnOthers); isKeyFrame = (dependsOnOthers == kCFBooleanFalse); } if (isKeyFrame) { CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer); size_t spsSize, spsCount; const uint8_t *spsParameterSet; OSStatus status = CMVideoFormatDescriptionGetH264ParameterSetAtIndex( format, 0, &spsParameterSet, &spsSize, &spsCount, 0 ); if (status != noErr) { return [[FBControlCoreError describeFormat:@"Failed to get SPS Params %d", status] failBool:error]; } size_t ppsSize, ppsCount; const uint8_t *ppsParameterSet; status = CMVideoFormatDescriptionGetH264ParameterSetAtIndex( format, 1, &ppsParameterSet, &ppsSize, &ppsCount, 0 ); if (status != noErr) { return [[FBControlCoreError describeFormat:@"Failed to get PPS Params %d", status] failBool:error]; } NSData *spsData = [NSData dataWithBytes:spsParameterSet length:spsSize]; NSData *ppsData = [NSData dataWithBytes:ppsParameterSet length:ppsSize]; [consumableData appendData:headerData]; [consumableData appendData:spsData]; [consumableData appendData:headerData]; [consumableData appendData:ppsData]; } // Get the underlying data buffer. CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer); size_t dataLength; char *dataPointer; OSStatus status = CMBlockBufferGetDataPointer( dataBuffer, 0, NULL, &dataLength, &dataPointer ); if (status != noErr) { return [[FBControlCoreError describeFormat:@"Failed to get Data Pointer %d", status] failBool:error]; } // Enumerate the data buffer size_t dataOffset = 0; while (dataOffset < dataLength - AVCCHeaderLength) { // Write start code to the elementary stream [consumableData appendData:headerData]; // Get our current position in the buffer void *currentDataPointer = dataPointer + dataOffset; // Get the length of the NAL Unit, this is contained in the current offset. // This will tell us how many bytes to write in the current NAL unit, contained in the buffer. uint32_t nalLength = 0; memcpy(&nalLength, currentDataPointer, AVCCHeaderLength); // Convert the length value from Big-endian to Little-endian. nalLength = CFSwapInt32BigToHost(nalLength); // Write the NAL unit without the AVCC length header to the elementary stream void *nalUnitPointer = currentDataPointer + AVCCHeaderLength; if ([consumer conformsToProtocol:@protocol(FBDataConsumerSync)]) { NSData *nalUnitData = [NSData dataWithBytesNoCopy:nalUnitPointer length:nalLength freeWhenDone:NO]; [consumableData appendData:nalUnitData]; } else { NSData *nalUnitData = [NSData dataWithBytes:nalUnitPointer length:nalLength]; [consumableData appendData:nalUnitData]; } // Increment the offset for the next iteration. dataOffset += AVCCHeaderLength + nalLength; } [consumer consumeData:consumableData]; return YES; } BOOL WriteJPEGDataToMJPEGStream(CMBlockBufferRef jpegDataBuffer, id<FBDataConsumer> consumer, id<FBControlCoreLogger> logger, NSError **error) { // Enumerate the data buffer size_t dataLength = CMBlockBufferGetDataLength(jpegDataBuffer); size_t offset = 0; while (offset < dataLength) { char *dataPointer; size_t lengthAtOffset; OSStatus status = CMBlockBufferGetDataPointer( jpegDataBuffer, offset, &lengthAtOffset, NULL, &dataPointer ); if (status != noErr) { return [[FBControlCoreError describeFormat:@"Failed to get Data Pointer %d", status] failBool:error]; } if ([consumer conformsToProtocol:@protocol(FBDataConsumerSync)]) { NSData *data = [NSData dataWithBytesNoCopy:dataPointer length:lengthAtOffset freeWhenDone:NO]; [consumer consumeData:data]; } else { NSData *data = [NSData dataWithBytes:dataPointer length:lengthAtOffset]; [consumer consumeData:data]; } // Increment the offset for the next iteration. offset += lengthAtOffset; } return YES; } BOOL WriteJPEGDataToMinicapStream(CMBlockBufferRef jpegDataBuffer, id<FBDataConsumer> consumer, id<FBControlCoreLogger> logger, NSError **error) { // Write the header length first size_t dataLength = CMBlockBufferGetDataLength(jpegDataBuffer); uint32 imageLength = OSSwapHostToLittleInt32(dataLength); NSData *lengthData = [[NSData alloc] initWithBytes:&imageLength length:sizeof(imageLength)]; [consumer consumeData:lengthData]; return WriteJPEGDataToMJPEGStream(jpegDataBuffer, consumer, logger, error); } // 1-byte alignment needed for the header to ensure correct sizing of the structure. #pragma pack(push, 1) // Should be 24 bytes long // All integers are little-endian (https://github.com/openstf/minicap#usage) struct MinicapHeader { unsigned char version; unsigned char headerSize; uint32 pid; uint32 displayWidth; uint32 displayHeight; uint32 virtualDisplayWidth; uint32 virtualDisplayHeight; unsigned char displayOrientation; unsigned char quirks; }; #pragma pack(pop) BOOL WriteMinicapHeaderToStream(uint32 width, uint32 height, id<FBDataConsumer> consumer, id<FBControlCoreLogger> logger, NSError **error) { struct MinicapHeader header = { .version = 1, .headerSize = sizeof(struct MinicapHeader), .pid = OSSwapHostToLittleInt32(NSProcessInfo.processInfo.processIdentifier), .displayWidth = OSSwapHostToLittleInt32(width), .displayHeight = OSSwapHostToLittleInt32(height), .virtualDisplayWidth = OSSwapHostToLittleInt32(width), .virtualDisplayHeight = OSSwapHostToLittleInt32(height), .displayOrientation = 0, .quirks = 0, }; NSData *data = [[NSData alloc] initWithBytes:&header length:header.headerSize]; [consumer consumeData:data]; return YES; }