124 lines
7.6 KiB
Objective-C
124 lines
7.6 KiB
Objective-C
#import <AVFoundation/AVFoundation.h>
|
|||
|
|
#import <CoreMedia/CoreMedia.h>
|
||
|
|
#import <Foundation/Foundation.h>
|
||
|
|
#import <ScreenCaptureKit/ScreenCaptureKit.h>
|
||
|
|
#import <fcntl.h>
|
||
|
|
#import <stdatomic.h>
|
||
|
|
#import <sys/mman.h>
|
||
|
|
#import <sys/stat.h>
|
||
|
|
#import <unistd.h>
|
||
|
|
|
||
|
|
enum { VCRingHeader = 64, VCRingCapacity = 96000 };
|
||
|
|
|
||
|
|
@interface VCScreenCaptureBridge : NSObject <SCContentSharingPickerObserver, SCStreamOutput, SCStreamDelegate>
|
||
|
|
@property(nonatomic) SCStream *stream;
|
||
|
|
@property(nonatomic) AVAudioConverter *converter;
|
||
|
|
@property(nonatomic) AVAudioFormat *inputFormat;
|
||
|
|
@property(nonatomic) AVAudioFormat *outputFormat;
|
||
|
|
@property(nonatomic) NSString *ringPath;
|
||
|
|
@property(nonatomic) int ringFd;
|
||
|
|
@property(nonatomic) void *ringMap;
|
||
|
|
@end
|
||
|
|
|
||
|
|
@implementation VCScreenCaptureBridge
|
||
|
|
|
||
|
|
static SCContentSharingPicker *VCSharedPicker(void) {
|
||
|
|
Class pickerClass = NSClassFromString(@"SCContentSharingPicker");
|
||
|
|
return pickerClass ? [pickerClass performSelector:@selector(sharedPicker)] : nil;
|
||
|
|
}
|
||
|
|
|
||
|
|
- (instancetype)init {
|
||
|
|
if ((self = [super init])) {
|
||
|
|
_ringFd = -1;
|
||
|
|
_outputFormat = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatInt16 sampleRate:48000 channels:2 interleaved:YES];
|
||
|
|
}
|
||
|
|
return self;
|
||
|
|
}
|
||
|
|
|
||
|
|
- (void)openRing {
|
||
|
|
if (_ringMap || !_ringPath) return;
|
||
|
|
[[NSFileManager defaultManager] createDirectoryAtPath:[_ringPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil];
|
||
|
|
_ringFd = open(_ringPath.fileSystemRepresentation, O_RDWR | O_CREAT, 0644);
|
||
|
|
size_t size = VCRingHeader + VCRingCapacity * sizeof(int16_t);
|
||
|
|
if (_ringFd < 0 || ftruncate(_ringFd, (off_t)size) != 0) return;
|
||
|
|
_ringMap = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, _ringFd, 0);
|
||
|
|
if (_ringMap == MAP_FAILED) _ringMap = NULL;
|
||
|
|
if (_ringMap && *(uint32_t *)_ringMap != 0x56434252) {
|
||
|
|
memset(_ringMap, 0, VCRingHeader); *(uint32_t *)((uint8_t *)_ringMap + 4) = 1; atomic_thread_fence(memory_order_seq_cst); *(uint32_t *)_ringMap = 0x56434252;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
- (void)setRingActive:(BOOL)active {
|
||
|
|
[self openRing]; if (!_ringMap) return;
|
||
|
|
if (active) { *(uint32_t *)((uint8_t *)_ringMap + 8) = 2; *(uint32_t *)((uint8_t *)_ringMap + 12) = 48000; }
|
||
|
|
atomic_thread_fence(memory_order_seq_cst); *(uint32_t *)((uint8_t *)_ringMap + 16) = active ? 1 : 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
- (void)push:(const int16_t *)samples count:(NSUInteger)count {
|
||
|
|
if (!_ringMap || count == 0 || count > VCRingCapacity) return;
|
||
|
|
uint8_t *base = _ringMap; uint64_t write = *(uint64_t *)(base + 24); atomic_thread_fence(memory_order_seq_cst); uint64_t read = *(uint64_t *)(base + 32);
|
||
|
|
if (VCRingCapacity - (write - read) < count) return;
|
||
|
|
int16_t *data = (int16_t *)(base + VCRingHeader); NSUInteger index = write % VCRingCapacity;
|
||
|
|
NSUInteger first = MIN(count, VCRingCapacity - index); memcpy(data + index, samples, first * sizeof(int16_t));
|
||
|
|
if (first < count) memcpy(data, samples + first, (count - first) * sizeof(int16_t));
|
||
|
|
atomic_thread_fence(memory_order_seq_cst); *(uint64_t *)(base + 24) = write + count;
|
||
|
|
}
|
||
|
|
|
||
|
|
- (void)present:(NSString *)path API_AVAILABLE(ios(27.0)) {
|
||
|
|
self.ringPath = path; [self openRing]; SCContentSharingPicker *picker = VCSharedPicker();
|
||
|
|
Class configurationClass = NSClassFromString(@"SCContentSharingPickerConfiguration");
|
||
|
|
SCContentSharingPickerConfiguration *configuration = [configurationClass new];
|
||
|
|
if (!picker || !configuration) return;
|
||
|
|
configuration.showsMicrophoneControl = NO; configuration.showsCameraControl = NO; picker.defaultConfiguration = configuration;
|
||
|
|
[picker addObserver:self]; picker.active = YES; [picker presentPickerUsingContentStyle:SCShareableContentStyleDisplay];
|
||
|
|
}
|
||
|
|
|
||
|
|
- (void)stop {
|
||
|
|
[self.stream stopCaptureWithCompletionHandler:^(__unused NSError *error) {}]; self.stream = nil; [self setRingActive:NO];
|
||
|
|
}
|
||
|
|
|
||
|
|
- (void)contentSharingPicker:(SCContentSharingPicker *)picker didUpdateWithFilter:(SCContentFilter *)filter forStream:(SCStream *)stream API_AVAILABLE(ios(27.0)) {
|
||
|
|
[self stop]; Class configurationClass = NSClassFromString(@"SCStreamConfiguration"); Class streamClass = NSClassFromString(@"SCStream");
|
||
|
|
SCStreamConfiguration *configuration = [configurationClass new]; if (!configuration || !streamClass) return; configuration.capturesAudio = YES;
|
||
|
|
configuration.excludesCurrentProcessAudio = YES; configuration.sampleRate = 48000; configuration.channelCount = 2;
|
||
|
|
configuration.width = 2; configuration.height = 2;
|
||
|
|
self.stream = [[streamClass alloc] initWithFilter:filter configuration:configuration delegate:self]; NSError *error = nil;
|
||
|
|
if (![self.stream addStreamOutput:self type:SCStreamOutputTypeAudio sampleHandlerQueue:dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0) error:&error]) { self.stream = nil; return; }
|
||
|
|
[self.stream startCaptureWithCompletionHandler:^(NSError *captureError) { [self setRingActive:captureError == nil]; }];
|
||
|
|
}
|
||
|
|
|
||
|
|
- (void)contentSharingPicker:(SCContentSharingPicker *)picker didCancelForStream:(SCStream *)stream API_AVAILABLE(ios(27.0)) { [self stop]; }
|
||
|
|
- (void)contentSharingPickerStartDidFailWithError:(NSError *)error API_AVAILABLE(ios(27.0)) { [self stop]; }
|
||
|
|
- (void)stream:(SCStream *)stream didStopWithError:(NSError *)error { [self stop]; }
|
||
|
|
|
||
|
|
- (void)stream:(SCStream *)stream didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer ofType:(SCStreamOutputType)type {
|
||
|
|
if (type != SCStreamOutputTypeAudio || !CMSampleBufferDataIsReady(sampleBuffer)) return;
|
||
|
|
CMAudioFormatDescriptionRef description = CMSampleBufferGetFormatDescription(sampleBuffer); const AudioStreamBasicDescription *asbd = CMAudioFormatDescriptionGetStreamBasicDescription(description);
|
||
|
|
if (!asbd) return; AVAudioFormat *format = [[AVAudioFormat alloc] initWithStreamDescription:asbd]; AVAudioFrameCount frames = (AVAudioFrameCount)CMSampleBufferGetNumSamples(sampleBuffer);
|
||
|
|
AVAudioPCMBuffer *input = [[AVAudioPCMBuffer alloc] initWithPCMFormat:format frameCapacity:frames]; input.frameLength = frames;
|
||
|
|
if (CMSampleBufferCopyPCMDataIntoAudioBufferList(sampleBuffer, 0, frames, input.mutableAudioBufferList) != noErr) return;
|
||
|
|
if (!self.converter || ![self.inputFormat isEqual:format]) { self.inputFormat = format; self.converter = [[AVAudioConverter alloc] initFromFormat:format toFormat:self.outputFormat]; }
|
||
|
|
AVAudioFrameCount capacity = (AVAudioFrameCount)ceil(frames * 48000.0 / format.sampleRate) + 64; AVAudioPCMBuffer *output = [[AVAudioPCMBuffer alloc] initWithPCMFormat:self.outputFormat frameCapacity:capacity];
|
||
|
|
__block BOOL supplied = NO; NSError *error = nil; AVAudioConverterOutputStatus status = [self.converter convertToBuffer:output error:&error withInputFromBlock:^AVAudioBuffer *(AVAudioPacketCount count, AVAudioConverterInputStatus *inputStatus) { if (supplied) { *inputStatus = AVAudioConverterInputStatus_NoDataNow; return nil; } supplied = YES; *inputStatus = AVAudioConverterInputStatus_HaveData; return input; }];
|
||
|
|
if (status == AVAudioConverterOutputStatus_Error || output.frameLength == 0 || !output.int16ChannelData) return;
|
||
|
|
[self push:output.int16ChannelData[0] count:output.frameLength * 2];
|
||
|
|
}
|
||
|
|
|
||
|
|
- (void)dealloc {
|
||
|
|
[self setRingActive:NO]; if (_ringMap) munmap(_ringMap, VCRingHeader + VCRingCapacity * sizeof(int16_t)); if (_ringFd >= 0) close(_ringFd);
|
||
|
|
}
|
||
|
|
@end
|
||
|
|
|
||
|
|
static VCScreenCaptureBridge *bridge;
|
||
|
|
|
||
|
|
int vc_ios_screen_capture_available(void) {
|
||
|
|
if (@available(iOS 27.0, *)) return VCSharedPicker().available ? 1 : 0;
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
void vc_ios_screen_capture_present(const char *ring_path) {
|
||
|
|
if (@available(iOS 27.0, *)) { if (!bridge) bridge = [VCScreenCaptureBridge new]; [bridge present:[NSString stringWithUTF8String:ring_path]]; }
|
||
|
|
}
|
||
|
|
|
||
|
|
void vc_ios_screen_capture_stop(void) { [bridge stop]; }
|