236 строки
8.0 KiB
Objective-C
236 строки
8.0 KiB
Objective-C
/*
|
|
* 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(void)
|
|
{
|
|
// 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;
|
|
}
|