#import #import #import #import #import #import #import #import #import enum { VCRingHeader = 64, VCRingCapacity = 96000 }; @interface VCScreenCaptureBridge : NSObject @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 removeObserver:self]; [picker addObserver:self]; picker.active = YES; [picker presentPickerUsingContentStyle:SCShareableContentStyleDisplay]; } - (void)stop { [self.stream stopCaptureWithCompletionHandler:^(__unused NSError *error) {}]; self.stream = nil; SCContentSharingPicker *picker = VCSharedPicker(); [picker removeObserver:self]; picker.active = NO; [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]; }